前言
閉包可以說是最最最最最最常見的面試題之一了,很多人面試的時候都被問到過閉包的知識,閉包的概念是什么?閉包的好處和壞處是什么?閉包有哪些應用場景了?
前兩天朋友面試的時候也被問到過這個問題,網上的答案也有很多:有的說能夠讀取其他函式內部作用域的函式就是閉包,也有的說所有的函式都是閉包,哪怕是全域作用域上的,因為它能獲取到全域作用域上的變數…答案也是很不統一,這就讓還是新手的我對于答案有點舉棋不定了,于是重溫了一下《你不知道的javascript上卷》和《javascript高級程式第四版》,看了一下這兩本書對于閉包的解釋,記錄一下相關知識,希望能夠幫助到大家,
初始閉包
《你不知道的javascript》 javascript中閉包無處不在,你只需要能夠識別并擁抱它, 閉包就是基于詞法作用域的,你在書寫代碼的時候產生的一種現象,
當函式能夠記住并訪問自己所在的詞法作用域的時候就產生了閉包
function foo(){
var a = 2;
function bar(){
console.log(a) //2
}
}
foo()
上述的這段代碼中,bar可以說也是一個閉包,因為它能夠訪問到他所在的作用域里面的變數,但是他卻是封閉在foo里面的,就相當于一個詞法作用域的查找規則而已,自己的作用域沒找到,去上一個作用域找,也是閉包的一部分
function foo(){
var a = 2;
function bar(){
console.log(a)
}
return bar
}
var baz = foo()
baz() //2 -----------這才是閉包的效果
我們將bar函式傳遞出去,然后呼叫foo函式的時候賦值給baz,呼叫baz就相當于呼叫了內部的bar函式了,而且bar相當于在自己所在的詞法作用域之外執行了,foo函式執行之后,正常的話內部的作用域會被銷毀,垃圾回識訓制也會釋放不再使用的記憶體空間,但是閉包可以阻止這種事情,讓foo內部的作用域依然存在著,這樣以供bar函式在后面的任何地方,任何時間參考
bar函式依然持有對這個作用域的參考,這個參考就叫閉包,
無論我們以何種的方式將bar這個函式傳遞出去
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
baz( bar);
}
function baz(fn) {
fn(); // 作為引數傳遞出去 這也是閉包
}
var fn;
function foo() {
var a = 2;
function bar() {
console.log( a );
}
fn = baz; // 直接賦值給全域變數fn
}
function bar() {
fn(); // 這也是閉包
}
foo();
baz(); // 2
上面這幾組代碼,有將函式作為引數傳遞出去的,也有將函式賦值給一個全域變數的,都讓這個函式可以在詞法作用域之外呼叫了,都是閉包,包括我們平時使用的一些計時器,定時器,事件監聽器這些使用了回呼 函式,都是閉包的應用
function getName(name){
setTimeout( function timer() {
console.log(name);
}, 1000 );
}
getName('小明')
//將一個內部函式timer傳遞給setTimeout,timer具有涵蓋getName的閉包,還有這對name的參考
for回圈與閉包
我們經常使用for回圈
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
上述這段代碼中,正常情況下我們想列印 1,2,3,4,5,但是事實上卻以每秒一次的頻率列印了五次6,6是從哪來的那?這個回圈的終止條件是 i 不在<=5,所以首次成立條件的時候 i 就是6,列印的就是回圈結束時候的值?這是為什么?
雖然這個timer函式在每次迭代的時候都定義一次,相當于定義了五個,但是都封閉在一個共享的全域作用域下面,就相當于只有一個i,根據js中setTimeout的運行機制來看,也是正常的,
那我們得如何處理那?我們可以在每次迭代的時候都創建一個作用域,這個樣的話就不是在一個全域作用域了,
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
我們先嘗試使用了一下自執行函式來創建作用域,
還是不行…
我們可以在每次迭代的時候創建一個塊級作用域
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
我們使用到了es6 中的let定義變數,在每次迭代的時候都創建一個塊級作用域,這樣是可以的
閉包的應用場景
閉包有什么應用場景?比如定時器,回呼函式,防抖函式....很多都是用到了閉包
我們還可以封裝私有變數
(function() {
// 私有變數和私有函式
let privateVariable = 10;
function privateFunction() {
return false;
}
// 建構式
MyObject = function() {};
// 公有和特權方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
var obj = new MyObject()
obj.publicMethod () //false
比如上面這種,我們使用一個自執行函式封裝私有變數,創建一個建構式運算式,我們沒有用var定義,說明是一個全域的,在建構式的原型上創建了一個publicMethod函式方法,這個函式就可以訪問這個函式的詞法作用域,就是一個閉包
我們也可以使用模塊模式創建,回傳一個單例物件
function singleton() {
// 私有變數和私有函式
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特權/公有方法和屬性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}
var foo = singleton();
foo.publicMethod() //false
singleton函式回傳了一個物件,相當于一個模塊實體,這個物件里面的publicMethod函式具有涵蓋模塊實體內部作用域的閉包,singleton函式也可以是自執行函式
小結
當函式可以記住并訪問所在的詞法作用域,即使函式是在當前詞法作用域之外執行,這時就產生了閉包,閉包也是一個非常強大的工具,可以用多種形式來實作模塊等模式,
閉包也會出現也寫問題,比如將把HTML元素保存在某個閉包的作用域中,就相當于宣布該元素不能被銷毀,或者是你把函式return出來后 他就給 window了所以一直存在記憶體中,會造成記憶體泄露,
function Handler() {
let element = document.getElementById('element');
let id = element.id;
element.onclick = () => console.log(id);
element = null;// 將element設定為null,解除對這個物件的參考,其參考計數也會減少,從而確保其記憶體可以在適當的時候被回收,
}
參考資料:
《你不知道的javascript》上卷
《javascript高級程式設計第四版》
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/274754.html
標籤:其他
下一篇:拿捏鏈表(一)—— 移除鏈表元素
