作用域的嵌套將形成作用域鏈,函數的嵌套將形成閉包。閉包與作用域鏈是 JavaScript 區別於其它語言的重要特性之一。
作用域
JavaScript 中有兩種作用域:函數作用域和全局作用域。
在一個函數中聲明的變量以及該函數的參數享有同一個作用域,即函數作用域。一個簡單的函數作用域的例子:
function foo() { var bar = 1; { var bar = 2; } return bar; // 2 }
不同於C等其它有塊作用域的語言,這里將始終返回 2
。
全局作用域,對於瀏覽器來說可以理解為 window
對象(Node.js則是 global
):
var bar = 1; function foo() {} alert(window.bar); // 1 alert(window.foo); // "function foo() {}"
對於變量 bar
和函數 foo
都屬於全局作用域,都是 window
的一個屬性。
作用域鏈
在 JavaScript 中訪問一個變量時,將從本地變量和參數開始,逐級向上遍歷作用域直到全局作用域。
var scope = 0, zero = "global-scope"; (function(){ var scope = 1, one = "scope-1"; (function(){ var scope = 2, two = "scope-2"; (function(){ var scope = 3, three = "scope-3"; // scope-3 scope-2 scope-1 global-scope console.log([three, two, one, zero].join(" ")); console.log(scope); // 3 })(); console.log(typeof three); // undefined console.log(scope); // 2 })(); console.log(typeof two); // undefined console.log(scope); // 1 })(); console.log(typeof one); // undefined console.log(scope); // 0
在最里層的函數中,各個變量都能被逐級遍歷並輸出。而倒數第二層的函數中,變量 three
無法遍歷找到,所以輸出了 undefined
。
舉一個通俗點的例子,你准備要花錢買點東西時,會先摸摸自己的錢包,沒了你可以找你爸要,你爸也沒有就再找你爺爺,... 。而你爸沒錢買東西時,他並不會來找你要。
閉包
在一個函數中,定義另一個函數,稱為函數嵌套。函數的嵌套將形成一個閉包。
閉包與作用域鏈相輔相成,函數的嵌套在產生了鏈式關系的多個作用域的同時,也形成了一個閉包。
function bind(func, target) { return function() { func.apply(target, arguments); }; }
那么怎么理解閉包呢?
- 外部函數不能訪問內嵌函數
- 外部函數也不能訪問內嵌函數的參數和變量
- 而內嵌函數可以訪問外部函數的參數和變量
- 換一個說法:內嵌函數包含了外部函數的作用域
我們再看看之前講述的作用域鏈的例子,這次從閉包的角度來理解下:
var scope = 0, zero = "global-scope"; (function(){ var scope = 1, one = "scope-1"; (function(){ var scope = 2, two = "scope-2"; (function(){ var scope = 3, three = "scope-3"; // scope-3 scope-2 scope-1 global-scope console.log([three, two, one, zero].join(" ")); console.log(scope); // 3 })(); console.log(typeof three); // undefined console.log(scope); // 2 })(); console.log(typeof two); // undefined console.log(scope); // 1 })(); console.log(typeof one); // undefined console.log(scope); // 0
最里層的函數能訪問到其內部和外部定義的所有變量。而倒數第二層的函數無法訪問到最里層的變量,同時,最里層的 scope = 3
這個賦值操作並沒有對其外部的同名變量產生影響。
再換個角度來理解閉包:
- 每次外部函數的調用,內嵌函數都會被創建一次
- 在它被創建時,外部函數的作用域(包括任何本地變量、參數等上下文), 會成為每個內嵌函數對象的內部狀態的一部分,即使在外部函數執行完並退出后
看下面的例子:
var i, list = []; for (i = 0; i < 2; i += 1) { list.push(function(){ console.log(i); }); } list.forEach(function(func){ func(); });
我們將得到兩次 "2" ,而不是預期的 "1" 和 "2" ,這是因為在 list
中的兩個函數訪問的變量 i
都是其上一層作用域的同一個變量。
我們改動下代碼,以利用閉包來解決這個問題:
var i, list = []; for (i = 0; i < 2; i += 1) { list.push((function(j){ return function(){ console.log(j); }; })(i)); } list.forEach(function(func){ func(); });
外層的“立即執行函數”接收了一個參數變量 i
,在其函數內以參數 j
的形式存在,它與被返回的內層函數中的名稱 j
指向同一個引用。外層函數執行並退出后,參數 j
(此時它的值為 i
的當前值)成為了其內層函數的狀態的一部分被保存了下來。