1. 程式人生 > >【進階2-3期】JavaScript深入之閉包面試題解

【進階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會被垃圾回收嗎?為什麼?

參考

深入javascript——作用域和閉包

ES6之let(理解閉包)和const命令