閉包是JS中一個很重要的概念,閉包其實是基于詞法作用域規則實現的,詞法作用域規則會使函數在查找變量時從函數內部再到函數定義時的作用域,而不是從函數內部到函數使用時的作用域。所以無論函數在哪里被調用,也無論它如何被調用,它的詞法作用域都只由函數被聲明時所處的位置決定。
基于這個規則,那么函數在當前詞法作用域之外執行,也可以記住并訪問函數聲明時所在的詞法作用域,這時就產生了閉包。
高程定義閉包:閉包是指有權訪問另一個函數作用域中的變量的函數。
function f1() {
var a = 1; // 3.調用的函數內部使用了父級作用域的內部變量
function f2() { // 1.調用的函數是父級作用域內部聲明的
console.log(a);
}
return f2;
}
var f3 = f1(); // 2.調用的函數是在父級作用域之外進行調用,foo()執行后將bar 函數本身當作一個值類型進行傳遞給baz。
f3(); // 這就是閉包的效果。執行之后,輸出f1中的a,因為不論何時何處調用f2都能訪問f1的變量所以f1不會被回收
閉包產生條件
通過以上代碼,我們可以得到閉包產生的條件:
- 調用的函數是父級作用域內部聲明的;
- 調用的函數是在父級作用域之外進行調用;
- 調用的函數內部使用了父級作用域的內部變量;
總結便是:無論使用何種方式對函數類型的值進行傳遞,當函數在別處被調用時都可以觀察到閉包。
// 無論通過何種手段將內部函數傳遞到所在的詞法作用域以外, 它都會持有對原始定義作用域的引用,無論在何處執行這個函數都會使用閉包。
function foo1() {
var a = 1;
function baz1() {
console.log(a); // 1
}
bar1(baz1); // baz1被作為參數傳遞到外部函數bar1中
}
function bar1(fn) {
fn(); // 這就是閉包!
}
foo1();
var fn2;
function foo2() {
var a = 2;
function baz2() {
console.log(a);
}
fn2 = baz2; // 將 baz2分配給全局變量,也相當于傳遞到外部
}
function bar2() {
fn2(); // 這就是閉包!
}
foo2();
bar2(); // 2
// 主要看看是否是外部調用。因為用戶點擊時觸發事件,不是在foo3中內部調用的。
var foo3 = function () {
var btn = document.querySelector("#myBtn");
var a = 3;
btn.onclick = function () {
alert(a);
}
}
foo3();
下面是一個關于閉包的金典例子:
for (var i = 1; i <= 5; i++) { // 只有一個全局作用域,運行timer是尋找變量i只有全局的i = 6
setTimeout(function timer() {
console.log(i); // 運行時會以每秒一次的頻率輸出五次 6
}, i * 1000);
}
// 首先解釋6是從哪里來的。 這個循環的終止條件是i不再<=5。 條件首次成立時i的值是6。因此,輸出顯示的是循環結束時i的最終值。延遲函數的回調會在循環結束時才執行。事實上,當定時器運行時即使每個迭代中執行的是setTimeout(.., 0),所有的回調函數依然是在循環結束后才會被執行,因此會每次輸出一個6出來。
for (var i = 1; i <= 5; i++) {
// 每次循環創建一個立即函數,產生一個新的作用域
(function (j) { // 利用立即函數,每次循環創建單獨的函數作用域并捕獲每次循環的i作為參數傳入,timer函數是一個閉包,它在立即函數中聲明,在setTimeOut回調使用,它會保留傳入的參數i的值,當延遲函數在作用域之外調用時,仍能訪問到i
setTimeout(function timer() {
console.log(j); // 能夠正常輸出1, 2, 3, 4, 5
}, j * 1000);
})(i);
}
閉包作用
閉包的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中。函數的執行上下文,在執行完畢之后,生命周期結束,那么該函數的執行上下文就會失去引用。其占用的內存空間很快就會被垃圾回收器釋放。可是閉包的存在,會阻止這一過程雖然例子中的閉包被保存在了全局變量中,但是閉包的作用域鏈并不會發生任何改變。在閉包中,能訪問到的變量,仍然是作用域鏈上能夠查詢到的變量即閉包可以使得它誕生環境一直存在。請看下面的例子,閉包使得內部變量記住上一次調用時的運算結果:
function addNum(num) {
return function () {
return num++;
};
}
var add = addNum(1);
add() // 1
add() // 2
add() // 3
// 上面代碼中,num是函數addNum的內部變量。通過閉包,start的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中可以看到,閉包add使得函數addNum的內部環境,一直存在。所以,閉包可以看作是函數內部作用域的一個接口