閉包的由來
說的閉包,首先就要知道作用域和作用域鏈。
作用域
作用域是一個變量和函數的作用范圍。
分為全局作用域和局部作用域,在ES6之前,是沒有塊級作用域概念的,只有函數作用域(個人認為私有作用域更符合)。
函數作用域都是相對獨立的,外部是訪問不到函數作用域中的變量的。
比如
function fn1() {
var name = 'nini';
}
console.log(name)
此時,我們在外部是訪問不到fn1中的name變量的。
作用域鏈
作用域鏈其實就是一個對象列表或者對象鏈。
在javascript中,每個函數都有自己的執行上下文環境。當我們要查找一個變量時,首先會從當前作用域開始查找,如果當前查找不到就到父級作用域查找,直至找到該變量。如果作用域頂端(作用域頂端是全局變量)也查找不到的話,會拋出異常ReferenceError(ReferenceError同作用域判別失敗有關)。
何為閉包
閉包,通俗的來說,就是在當前作用域能訪問到外部作用域中的對象。實際上,閉包的存在是為了保護私有變量不被污染,形成不銷毀的棧內存,里面的私有變量等信息保存下來。
就像在上述中,要在外部訪問到fn1中的name,就可以使用閉包來實現。
function fn1() {
var name = 'nini';
function getName() {
console.log(name)
}
return getName
}
var getName = fn1()
getName()
MDN中對閉包的定義是:
閉包是指可以訪問到自由變量的函數
那什么是自由變量呢?其實就是指既不是當前函數的參數也不是當前函數的內部變量的變量。
其實這是理論上的閉包,實踐上的閉包要滿足以下兩點:
- 即使創建它的上下文已經銷毀,它仍然存在;
- 代碼中引用了自由變量;
閉包的原理
實際上是利用了函數作用域鏈的特性。
如果函數內部定義的函數引入了外部函數的活動對象,就會把它添加到函數的作用域鏈中,函數執行完畢,其執行作用域鏈銷毀,但因內部函數的作用域鏈仍然在引用這個活動對象,所以其活動對象不會被銷毀,直到內部函數被燒毀后才被銷毀。
理解上述這句話的前提是理解執行上下文。
執行上下文有3個屬性,this、變量對象VO(進入執行上下文之后是AO)、作用域鏈。而函數都有一個屬性[[scope]],會保存所有父變量到其中,當函數激活進入函數上下文,創建 VO/AO 后,就會將活動對象添加到作用鏈的前端。
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
我們來具體分析一下執行過程:
f函數執行上下文維護了一個作用域鏈,依然可以讀取到 checkscopeContext.AO 的值。當f 函數引用了 checkscopeContext.AO 中的值的時候,checkscopeContext已經被銷毀了,但是checkscopeContext.AO 仍然活在內存中,依然可以通過 f 函數的作用域鏈找到它。
使用閉包的好處
- 可作為私有成員存在
- 可使變量長期保持在內存中
- 避免全局污染
閉包的缺點
javascript中如果對象不再被引用,那么這些對象將會被JS引擎的垃圾回收器回收;反之,這些對象一直會保存在內存中。
- 對內存消耗有負面影響。因內部函數保存了對外部變量的引用,導致無法被垃圾回收,增大內存使用量
- 使用不當會導致內存泄漏
閉包的應用場景
拿到正確的值(模仿塊級作用域)
for(var i = 0; i < 6; i++) {
(function(j){
setTimeout(() => {
console.log(j);
}, j * 1000);
})(i)
}
設置私有變量
let name = Symbol();
class Private {
constructor(s) {
this[name] = s
}
foo() {
console.log(this[name])
}
}
函數防抖
function debounce(fn, wait=50) {
let timer;
return function(...arguments) {
if(timer){
clearTimeout(timer);
}
timer = setTimeout(()=>{
fn.apply(this, arguments);
}, wait)
}
}