最近遇到React Hook的過時閉包問題,重溫了下紅寶書,這裏整理之前積累的一些基礎。
1. 函數聲明和函數表達式
函數聲明:
function say(){
xxxx
}
函數表達式:
賦值變量的方式可以使用匿名函數(lambda函數)
var say = function(){ xxx }
注意函數聲明在編譯期間把聲明提前,函數表達式不會。
2. 執行環境,活動對象,變量對象
這兩個概念很重要:
當某個函數被調用時,會創建一個執行環境及相應的作用域鏈。
然後,使用arguments和其他命名參數的值來初始化函數的活動對象。
每個執行環境都有一個表示變量的對象,變量對象,變量對象是活動對象的具體的每個表示變量的對於下部分
作用域鏈本質上是一個指針列表,指向變量對象的指針列表,只是引用實際不包含變量對象(可看下圖)
在作用域鏈中,外部函數的活動對象始終處於內部的上一位,直至爲作用域鏈終點的全局執行環境。
全局環境的變量對象始終存在,像函數聲明的函數的局部環境的變量對象,只有在函數執行的過程中存在。
舉個栗子:
function compare(v1,v2) {
xxx
}
var res = compare(1,2);
這裏的說明直接看紅寶書,紅寶書講的很好:
創建函數的時候,就會預先創建包含全局變量對象的作用域鏈,但只有調用的時候,纔會創建執行環境,並複製Scope中的作用域鏈,纔會產生活動對象。
這樣我們可以解釋這個現象:
function createIncrement(i) {
let value = 0;
function increment() {
value += i;
console.log(value);
}
return increment;
}
let inc = createIncrement(1);
inc(); //1
// 匿名函數銷燬,纔會銷燬value
// inc = null;
inc(); //2
inc是createIncrement返回的increment函數,createIncrement在執行完畢後,其活動對象不會被銷燬,也就是value不會被銷燬,因爲increment(也可改爲匿名函數)的作用域鏈仍然在引用這個活動對象。當createIncrement函數返回後(被賦值給inc),其執行環境的作用域鏈會被銷燬,但它的活動對象仍然會留在內存中。除非increment即inc被銷燬,createIncrement的活動對象纔會被銷燬,被銷燬之前,value在內存中都是一個變量對象,所以纔會一直被累加。
銷燬很簡單,根據垃圾回收的計數引用機制,inc = null即可。
由於閉包會攜帶包含它的函數的作用域,就比如上例中的increment包含了createIncrement變量對象,因此閉包會比其他函數佔用更多的內存。過度使用閉包可能會導致內存佔用過多。但現在V8很智能,會嘗試回收閉包占用的內存,下次有機會整理下這裏的內容。
3. 再解釋閉包經典題
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
}
}
return result;
}
var res = createFunctions();
// 10 10 10 10 10
console.log(res[0](), res[1](), res[2](), res[3](), res[4]());
js最經典的入門題,當初剛開始接觸js的時候也在社區裏問過這個問題,現在結合紅寶書的閉包相關概念理論基礎來回答這個問題。
createFunctions函數最終返回一個函數數組,期望函數數組返回循環中不同的變量i。但是注意,這裏的i是在
createFunctions的作用域中,當createFunctions聲明的時候,創建包括全局變量對象的作用域鏈已經保存在scope屬性中,當調用createFunctions的時候,執行環境被創建,並複製scope對象的屬性,連接起作用域鏈。result函數表達式同理,由於result函數只是聲明,最後的每個result函數的作用域鏈中都是保存着createFunctions的活動對象,而i即是活動對象中的一個變量對象。當createFunctions返回後,即執行到
return result
這條語句,i的值即是10,result函數的函數的作用域鏈此時在1優先級的位置(參考上面2標題的圖)連接
着createFunctions的作用域,且所有函數都引用指向createFunctions作用域中保存變量i的同一個變量對象。i在createFunctions返回後值已經是10了。
經典的解決辦法:
function createFunctions() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
return function () {
return num;
}
})(i)
}
return result;
}
var res = createFunctions();
// 解釋在:
console.log(res[0](), res[1](), res[2](), res[3](), res[4]());
給result函數中返回num的函數外再加一個匿名函數,這樣我們並沒有直接把閉包複製給result數組,而是把匿名函數執行的結果賦給數組,這個匿名函數有個參數值也是要返回的值,函數參數時按照值傳遞的,這樣就將變量i當前值複製給參數num。而在這個匿名函數中,又創建並返回了一個訪問num的閉包(函數中嵌套函數即產生閉包,且num是外部函數作用域的變量對象,那麼內部函數即是訪問num的閉包)。因爲執行了10次,創建了10個不同執行環境的閉包(起碼在匿名函數層的作用域鏈中的變量對象的i值是不同的),結果就是result數組中每個函數都創建擁有自己num變量的一個副本。
(此處不談ES6的做法。)
4.this對象
理解了上面的執行環境,this就好理解,只用記住,this對象是運行時基於函數的執行環境綁定的。
一個需要注意的是每個函數在調用的時候都會自動獲取兩個特殊變量: this和arguments。但內部函數在搜索這兩個變量時,只會搜索到其活動對象位置,因此永遠不可能直接訪問外部函數中的兩個變量。
因此在:
var name = 'window';
var obj = {
name: 'obj',
getName: function () {
return function () {
return this.name;
}
}
}
console.log(obj.getName()());
最內部的匿名函數並沒有取得外部作用於的this對象。所以最經典的做法就是把外部作用域中的this對象保存在一個閉包能夠訪問到的變量裏,讓閉包通過該變量訪問對象:
var name = 'window';
var obj = {
name: 'obj',
getName: function () {
var that = this;
return function () {
return that.name;
}
}
}
console.log(obj.getName()());