【進階2-3期】JavaScript深入之閉包面試題解
這是我在公眾號(高階前端進階)看到的文章,現在做筆記 https://github.com/yygmind/blog/issues/19
作用域指的是一個變數和函式的作用範圍,JS中函式內宣告的所有變數在函式體內始終是可見的,在ES6前有全域性作用域和區域性作用域,但是沒有塊級作用域(catch只在其內部生效),區域性變數的優先順序高於全域性變數。
作用域
變數提升
var scope="global"; function scopeTest(){ console.log(scope); var scope="local" } scopeTest(); //undefined
上面的程式碼輸出是undefined
,這是因為區域性變數scope
變數提升了,等效於下面
var scope="global";
function scopeTest(){
var scope;
console.log(scope);
scope="local"
}
scopeTest(); //undefined
注意,如果在區域性作用域中忘記var,那麼變數就被宣告為全域性變數。
沒有塊級作用域
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); // 3 data[1](); // 3 data[2](); // 3
上篇文章已經介紹過了,【進階2-2期】JavaScript深入之從作用域鏈理解閉包
作用域鏈
每個函式都有自己的執行上下文環境,當代碼在這個環境中執行時,會建立變數物件的作用域鏈,作用域鏈是一個物件列表或物件鏈,它保證了變數物件的有序訪問。
作用域鏈的開始是當前程式碼執行環境的變數物件,常被稱之為“活躍物件”(AO),變數的查詢會從第一個鏈的物件開始,如果物件中包含變數屬性,那麼就停止查詢,如果沒有就會繼續向上級作用域鏈查詢,直到找到全域性物件中
閉包
function createClosure(){ var name = "jack"; return { setStr:function(){ name = "rose"; }, getStr:function(){ return name + ":hello"; } } } var builder = new createClosure(); builder.setStr(); console.log(builder.getStr()); //rose:hello
上面在函式中返回了兩個閉包,這兩個閉包都維持著對外部作用域的引用。閉包中會將外部函式的自由物件新增到自己的作用域鏈中,所以可以通過內部函式訪問外部函式的屬性,這也是javascript模擬私有變數的一種方式。
閉包面試題解
由於作用域鏈機制的影響,閉包只能取得內部函式的最後一個值,這引起的一個副作用就是如果內部函式在一個迴圈中,那麼變數的值始終為最後一個值。
這個程式碼已經貼過了,怕你們忘記,就再貼一遍
var data = [];
for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); // 3 data[1](); // 3 data[2](); // 3
如果要強制返回預期的結果,怎麼辦???
方法1:立即執行函式
for (var i = 0; i < 3; i++) { (function(num) { setTimeout(function() { console.log(num); }, 1000); })(i); } // 0 // 1 // 2
方法2:返回一個匿名函式賦值
var data = [];
for (var i = 0; i < 3; i++) { data[i] = (function (num) { return function(){ console.log(num); } })(i); } data[0](); // 0 data[1](); // 1 data[2](); // 2
無論是立即執行函式還是返回一個匿名函式賦值,原理上都是因為變數的按值傳遞,所以會將變數i
的值複製給實參num
,在匿名函式的內部又建立了一個用於訪問num
的匿名函式,這樣每個函式都有了一個num
的副本,互不影響了。
方法3:使用ES6中的let
var data = [];
for (let i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();
解釋下原理:
var data = [];// 建立一個數組data;
// 進入第一次迴圈
{
let i = 0; // 注意:因為使用let使得for迴圈為塊級作用域
// 此次 let i = 0 在這個塊級作用域中,而不是在全域性環境中
data[0] = function() {
console.log(i);
};
}
迴圈時,let
宣告i
,所以整個塊是塊級作用域,那麼data[0]這個函式就成了一個閉包。這裡用{}表達並不符合語法,只是希望通過它來說明let存在時,這個for迴圈塊是塊級作用域,而不是全域性作用域。
上面的塊級作用域,就像函式作用域一樣,函式執行完畢,其中的變數會被銷燬,但是因為這個程式碼塊中存在一個閉包,閉包的作用域鏈中引用著塊級作用域,所以在閉包被呼叫之前,這個塊級作用域內部的變數不會被銷燬。
// 進入第二次迴圈
{
let i = 1; // 因為 let i = 1 和上面的 let i = 0
// 在不同的作用域中,所以不會相互影響
data[1] = function(){
console.log(i);
};
}
當執行data[1]()
時,進入下面的執行環境。
{
let i = 1;
data[1] = function(){
console.log(i);
};
}
在上面這個執行環境中,它會首先尋找該執行環境中是否存在i
,沒有找到,就沿著作用域鏈繼續向上到了其所在的塊作用域執行環境,找到了i = 1
,於是輸出了1
。
思考題
程式碼1:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
程式碼2:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope();
上面的兩個程式碼中,checkscope()
執行完成後,閉包f
所引用的自由變數scope
會被垃圾回收嗎?為什麼?
參考