閉包 先上 "維基百科" )的定義 在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變數的函數。這個被引用的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認 ...
閉包
先上維基百科的定義
在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變數的函數。這個被引用的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。
簡單理解這句話,有兩個要點:
1. 自由變數 2. (引用自由變數的)函數。
先說自由變數:
當我們定義一個變數時,如果不對它指定約束條件,它就是自由變數。 舉個例子:
x ∈ (0,99)
f(x,y)
在函數f(x,y)中,x就是約束變數,y是自由變數。
具體到JavaScript中,看一個例子:
var x = 0;
function foo (y) {
var z = 2;
return x + y + z;
}
foo (3); // 3
轉換成數學思維的話,函數foo其實應該是這樣的foo(x,y),但是我們知道函數的參數其實是受到函數的約束的,也就是說,真正的自由變數只有x一個。
這樣可以引出一個簡單的定義,在函數中,如果存在一個既不是局部變數,也不是形參的變數,我們可以認為形成了閉包。
自由變數從哪兒來?
幾乎所有的語言中,對於同名變數都是就近尋找,先在本作用域內尋找,找不到就去父作用域找。我們稱之為作用域鏈。
在一個閉包函數中,自由變數通常是由父級提供。看下麵的例子:
function foo(x) {
var tmp = 3;
function bar(y) {
console.log(x + y + (++tmp));
}
bar(10);
}
foo(2)
根據我們上面的定義,bar擁有自由變數,是閉包,而foo不是。
那麼怎樣才能讓foo變成閉包呢?
var x = 0;
function foo() {
var tmp = 3;
function bar(y) {
console.log(x + y + (++tmp));
}
bar(10);
}
// 其實轉換一下,形如
function foo2() {
var tmp = 3;
//function bar(y) {
console.log(x + 10 + (++tmp));
//}
// bar(10);
}
此時,可以認為foo是一個閉包。
到這裡,可能有朋友覺得這和平時看到的js閉包不一樣啊,我們平時看到的閉包,都是這樣的:例子來自這篇博客
function foo(x) {
var tmp = new Number(3);
return function (y) {
alert(x + y + (++tmp));
}
}
var bar = foo(2); // bar 現在是一個閉包
bar(10);
這個函數其實可以改寫成下麵的樣子:
bar = function (y) {
// foo(2)
alert(2 + y + (++tmp))
}
很明顯,tmp是自由變數,符合我們起初的定義,bar是擁有自由變數的函數。
那麼tmp存在哪兒呢?
在執行foo(2)時,就會產生一個tmp=3的變數。這個變數被return的函數所引用,所以不會被回收。而return的函數中的自由變數,根據作用域鏈去尋找值。bar函數,是在foo(2)中定義的,所以,變數tmp先在foo(2)的變數區中去尋找,並對其操作。
註:有關作用域鏈的問題,我會在下一篇做解析。
說到這裡,插一下module模式。
閉包使用之module模式
var Module = (function () {
var aaa = 0;
var foo = function () {
console.log(aaa);
}
return {
Foo: foo
}
})();
// 或者
(function () {
var aaa = 0;
var foo = function () {
console.log(aaa);
}
window.Module = {
Foo: foo
}
})();
註意上面的兩個例子,Module本身只是一個對象,但是return的函數本身形成了閉包,保證了作用域的乾凈,不會污染到其他函數。
說到這裡,想必有朋友覺得這不就是個另類的類嗎?擁有局部變數,還有可訪問的函數。沒錯,就外現而言,我認為閉包和類是非常相似的。
類
以Java舉例:
class Foo {
private int a;
int Say( int b ) {
return a + b;
}
}
上面的Foo中,函數Say中的a是函數作用域外的,屬於自由變數。可以認為Say形成了函數閉包。但是與js不同的地方就在於,實例方法需要通過類的實例也就是對象來調用。
在java的設計里,明確了訪問許可權,private,protect,default,package,這是規範調用的創舉。這也使得java程式員很少會考慮閉包這種實現,因為變數和函數都有關鍵字來定義訪問許可權,歸屬於一個個類中,明確且清晰。
閉包的壞處
如果把閉包按照類的實現來理解的話,很容易就明白為什麼不建議使用閉包。
每次調用閉包,就會生成一個作用域來存放一些閉包函數需要的自由變數。這很容易造成記憶體浪費。即使在Java編程中,也不建議隨便就新建對象。
題外話
在前一篇bind、call、apply中,我提到了一個觀點,因為是面向對象,所以存在綁定this的需要。
關於面向對象,我認為,面向對象的好處就在於,易於理解,方便維護和復用。這在多人開發大型項目時,是遠遠超過對性能的要求的。
即使在摩爾定律放緩的現在,相對於以前,記憶體也是非常便宜的,所以從遠古時代對於性能要求到極致,到現在普遍提倡代碼可讀性。
有超級大牛創建了這個繽紛的代碼世界,為了讓更多人體會到編程的樂趣,他們設計了更易理解的編程語言,發明瞭各種編譯器、解析器……
如果只是寫一個1+1的程式,是不需要面向對象的,如果人類能和機器擁有超強的邏輯和記憶,也是不需要面向對象的。