1. 程式人生 > >[JS]理解閉包

[JS]理解閉包

執行環境和作用域

每個函式都有自己的執行環境。當執行流進入一個函式時,函式的環境就會被推入一個環境棧中。而在函式執行之後,棧將其環境彈出,把控制權返還給之前的執行環境。
當代碼在一個環境中執行時,會建立變數物件的一個作用域鏈,保證對執行環境有權訪問的所有變數和函式的有序訪問。作用域的前端,始終都是當前執行的程式碼所在環境的變數物件。如果這個環境是函式,則將其活動物件作為變數物件。活動物件在最開始時只包含一個變數,即arguments物件(這個物件在全域性環境中是不存在的)。作用域鏈中的下一個變數物件來自包含(外部)環境,而再下一個變數物件則來自下一個包含環境。這樣,一直延續到全域性執行環境;全域性執行環境的變數物件始終都是作用域鏈中的最後一個物件。[1]

var color0="blue";

function ChangeOnce(){
    var color1 = "yellow";

    function ChangeTwice(){
        var color2 = "red";
        color1 = color0;
        color0 = color2;
        //這裡能訪問color0\color1\color2
    }
    //這裡能訪問color0\color1
}
//這裡只能訪問color0

以上共涉及三個執行環境:全域性環境、 changeOnce() 的全域性環境和 changeTwice()

的全域性環境。內部環境可以通過作用域鏈訪問所有外部環境,但外部環境不能訪問內部的任何變數和函式。

函式內部宣告變數時,一定要使用 var 命令,否則,該變數是一個全域性變數!

延長作用域鏈

雖然執行環境的型別總共只有兩種:全域性和區域性,但可以通過其他辦法來延長作用域鏈。當執行流進入下列任一語句時:作用域鏈就會延長。

  • try-catch語句的catch塊;
  • with語句
function Name(){
    var firstName = "Smith";

    with(firstName){
        var name = firstName + " "
+" Whale" } return name; }

閉包

當外部需要訪問內部函式或者變數時該怎麼辦呢?這是就需要用到 閉包

閉包就是能夠讀取其他函式內部變數的函式。由於在Javascript語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成”定義在一個函式內部的函式”。
所以,在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。——阮一峰的部落格

閉包最大的用途有兩個:
- 讀取函式內部的變數
- 讓變數值始終保持在記憶體中

function f1(){
    var n = 999;
    nAdd = function(){ n+=1}
    function f2(){
        alert (n);
    }
    return f2;
}
var result = f1();
result();    //999
nAdd();
result();    //1000

在上面的程式碼中, result 實際上就是閉包函式 f2 。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證明了,函式f1中的區域性變數n一直儲存在記憶體中,並沒有在f1呼叫後被自動清除。

為什麼會這樣呢?原因就在於f1是f2的父函式,而f2被賦給了一個全域性變數,這導致f2始終在記憶體中,而f2的存在依賴於f1,因此f1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制(garbage collection)回收。

這段程式碼中另一個值得注意的地方,就是”nAdd=function(){n+=1}”這一行,首先在nAdd前面沒有使用var關鍵字,因此nAdd是一個全域性變數,而不是區域性變數。其次,nAdd的值是一個匿名函式(anonymous function),而這個匿名函式本身也是一個閉包,所以nAdd相當於是一個setter,可以在函式外部對函式內部的區域性變數進行操作。——阮一峰的部落格

閉包與變數

閉包只能取得包含函式中任何變數的最後一個值。如:

function createFunciton(){
    var result = new Array();

    for(var i=0; i < 10; i++>){
        result[i] = funciton (){
            return i;
        }
    }
    return result;
}

這個函式返回一個函式陣列。表面上看,似乎每個函式應該返回自己的索引值,即位置0的函式返回0,位置1的函式返回1,以此類推。但實際上,每個函式都返回10。因為每個函式的作用域中都儲存著 creationFunction() 函式的活動物件,所以它們引用的都是同一個變數 i 。當 creationFunction() 函式返回後,變數 i 的值是10,所以在每個函式內部i的值都是10.但是我們可以通過建立另一個匿名函式強制讓閉包的行為符合預期。如下:

function createFunciton(){
    var result = new Array();

    for(var i=0; i < 10; i++>){
        result[i] = funciton (num){
            return function(){
                return num;
            };
        }(i);
    }
    return result;
}

在這個函式中,定義了一個匿名函式,並立即執行匿名函式的結果賦給陣列。這裡的匿名函式有一個引數 num ,也就是最終的函式要返回的值。在呼叫匿名函式時,傳入的變數 i 的當前值按值傳遞賦值給引數 num 。在這個匿名函式內部,又建立並返回了一個訪問 num 的閉包。這樣一來, result 陣列中的每一個函式都有了自己的 num 變數的一個副本,因此可以返回各自不同的數值。

[1]:《Javascript高階程式設計》(第3版) [美]Nicholas C.Zakas著 李鬆峰 曹力譯