何為閉包?
閉包(Closure)是一個封閉的作用域,它可以訪問外部作用域的變量。
說起來比較抽象,實際上閉包就是一個函數(shù),函數(shù)內(nèi)部可以訪問外部的變量,比如下面這個例子:
function sayHello() {
const greet = 'hello world';
function helloFunc() {
console.log(greet);
}
helloFunc();
}
sayHello(); // hello world
helloFunc函數(shù)內(nèi)雖然沒有定義greet變量,但是它的外層函數(shù)sayHello函數(shù)里定義了greet變量,所以最后成功輸出hello world,這個例子里面helloFunc就是一個閉包,尋找變量的過程是按照作用域鏈來尋找的。
閉包的作用
我們知道javascript外部作用域無法訪問內(nèi)部作用的值。
function func() {
var name = 'jack';
}
console.log(name); // undefined
但是函數(shù)內(nèi)部卻可以訪問外部變量。
var name = 'jack';
function func() {
console.log(name);
}
func(); // jack
所以函數(shù)內(nèi)部可以修改外部狀態(tài),我們可以讓函數(shù)擁有狀態(tài),比如迭代器或生成器。
var add = (function() {
var counter = 0;
return function() {
counter++;
return counter;
};
})();
add(); // 1
add(); // 2
add(); // 3
這個例子我們使用了一個立即執(zhí)行函數(shù)表達式(IIFE)創(chuàng)建了一個匿名函數(shù)也就是一個閉包,函數(shù)返回的函數(shù)會引用這個匿名函數(shù)的局部變量counter,所以我們每次調(diào)用add函數(shù)輸出的值都不一樣,add函數(shù)因為閉包有了狀態(tài)。
這里使用立即執(zhí)行函數(shù)主要是為了直接返回最終函數(shù),也可以返回個普通函數(shù)像下面這樣:
var genAddFunc = function() {
var counter = 0;
return function() {
counter++;
return counter;
}
}
var add = genAddFunc();
add(); // 1
add(); // 2
add(); // 3
閉包的應(yīng)用
比如我們需要對多個li綁定點擊事件如下:
<html>
<body>
<ul id="itemList">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
</body>
</html>
const itemList = document.getElementById('itemList');
const items = itemList.getElementsByTagName('li');
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${i} clicked`);
}
}
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
我們發(fā)現(xiàn)雖然li的點擊事件都已經(jīng)創(chuàng)建了閉包記住了i的值,但是都是輸出3而不是0,1,2這是為什么呢?
原因是for循環(huán)中大括號包含該的部分并不是一個封閉的作用域,通過下面代碼可以驗證:
for (var i = 0; i < 3; i++) {}
console.log(i); // 3
console.log(window.i); // 3
當for循環(huán)結(jié)束后變量i并沒有被回收,實際上我們是創(chuàng)建了一個全局變量i,3個item的點擊事件函數(shù)綁定的是全局變量i的值,所以最后都輸出的是item 3 clicked,通過下面代碼可以驗證:
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${i} clicked`);
}
}
i = 99;
items[0].click(); // item 99 clicked
items[1].click(); // item 99 clicked
items[2].click(); // item 99 clicked
當我們修改了i的值發(fā)現(xiàn)3個點擊事件的結(jié)果也都變成了item 99 clicked,說明它們輸出的i綁定的都是最新i的值而不是當時循環(huán)時i的值。
于是我們嘗試一下使用一個局部變量看看能否保存i的值,這樣我們就可以輸出正確的i的值:
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
var j = i;
console.log(`item ${j} clicked`);
}
}
i = 99;
items[0].click(); // item 99 clicked
items[1].click(); // item 99 clicked
items[2].click(); // item 99 clicked
使用局部變量j保存i還是沒有成功,我們修改了i的值后j的值也發(fā)生了變化,說明js在執(zhí)行過程中j還是引用i的值。
解決方法1:函數(shù)傳參
for (var i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${j} clicked`);
} (i)
}
i = 99;
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
通過傳遞參數(shù)我們得到了正確的結(jié)果,修改了i的值也沒有影響點擊事件的結(jié)果。
解決方法2: 使用閉包
for (var i = 0; i < items.length; i++) {
items[i].onclick = (function() {
var j = i;
return function() {
console.log(`item ${j} clicked`);
}
})();
}
i = 99;
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
在閉包中我們用j來保存i的值,這樣我們就能記住當時循環(huán)時i的值了,和之前的單獨一個function區(qū)別是閉包可產(chǎn)生獨立的作用域這樣j就是i的值的拷貝而不是i的引用,我是這么理解的。
解決方法2: 使用let
使用let應(yīng)該是最優(yōu)雅的解決辦法了,let是ES6的關(guān)鍵字它能夠產(chǎn)生封閉的作用域:
for (let i = 0; i < items.length; i++) {
items[i].onclick = function() {
console.log(`item ${i} clicked`);
}
}
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked