歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> JavaScript 中的閉包和作用域鏈

JavaScript 中的閉包和作用域鏈

日期:2017/3/1 9:29:00   编辑:Linux編程

  要想理解閉包,應當先理解JavaScript的作用域和作用域鏈。

  JavaScript有一個特性被稱之為“聲明提前(hoisting)”,即JavaScript函數裡聲明的所有變量(但不涉及賦值)都被“提前”至函數體的頂部,“聲明提前”這步操作是在JavaScript引擎的“預編譯”時進行的,是在代碼開始運行之前,看一看下面的例子:

var name = "YY";
function getName(){
    console.log(name);      //輸出undefine,而不是“YY”
    var name = "Crucify";
    console.log(name);      //輸出“Crucify”
}

  首先局部變量定義了一個和全局變量相同名字的變量,則在函數體內部局部變量遮蓋了同名的全局變量,然後在函數體內部變量name的聲明被提前至函數體頂部但並沒有賦值,所以此時name是一個只被聲明但並沒有初始化的變量,我們知道變量只進行聲明但並不初始化則它的值為undefine,所以第一行打印時undefine,下一行開始為變量name進行賦值,所以第二行打印的輸出是我們所期望的。

  當某個函數被調用時,會創建一個執行環境及相應的作用域鏈。

  執行環境(execution context)定義了變量或函數有權訪問的其他數據,決定了它們各自的行為。每個函數都有自己的執行環境。當執行流進入一個函數時,函數的環境就會被推入一個環境棧中。而在函數執行之後,棧將其環境彈出,把控制權返回給之前的執行環境。當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈(scope chain)。作用域鏈的用途,是保證對執行環境有權訪問的所有變量和函數的有序訪問。

  作用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。當JavaScript需要查找變量x的值的時候(這個過程稱作“變量解析”(variable resolution)),他會從列表之中的第一個對象開始查找直到最後一個對象,如果某個對象有一個名為x的屬性,則會直接食用這個屬性的值。如果作用域鏈上沒有任何一個對象含有屬性x,那麼就認為這段代碼的作用域上不存在x,並最終拋出一個引用錯誤異常。

  所謂的“變量對象的指針列表”很好理解。在JavaScript的最頂層代碼中(也就是不包含在任何函數定義內的代碼),作用域鏈是由一個全局對象組成。在不包含嵌套的函數體內,作用域鏈上有兩個對象,第一個是定義函數參數和局部變量的對象,第二個是全局對象。在一個嵌套的函數體內,作用域鏈上則至少有三個對象,看下面的例子:

var name = "YY";
function getName(){
    var name = "Crucify";
    function f(){
        return name;          
    }
    return f();
}

  函數f()的作用域鏈上有三個對象,第一個是定義函數f()參數和局部變量的對象,第二個是定義函數getName()參數和局部變量的對象,第三個是全局對象。

  每個環境都可以向上搜索作用域鏈,以查詢變量和函數名,但任何環境都不能通過向下搜索作用域鏈而進入另一個執行環境,即函數f()可以向上搜索函數getName()和全局對象中的屬性,但是全局對象不能向下搜索getName()和f()中的值。

  而創建閉包的常見方式,就是在一個函數內部創建另一個函數。閉包是指有權訪問另一個函數作用域中的變量的函數。

  一般來講,當函數執行完畢後,局部活動對象就會被銷毀,內存中僅保存全局作用域(全局執行環境的變量對象)。但是,閉包的情況有所不同,因為在另一個函數內部定義的函數會將包含函數(即外部函數)的活動對象添加到它的作用域鏈中,參考下面的代碼:

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        
        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
}

當下列代碼執行時,包含函數與內部匿名函數的作用域鏈如圖所示:

var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });

  當createComparisonFunction()函數在執行完畢後,其活動對象也不會被銷毀,因為匿名函數的作用域鏈仍然在引用這個活動對象。直到匿名函數被銷毀後, createComparisonFunction()的活動對象才會被銷毀:

compare = null;  //解除對匿名函數的引用(以便釋放內存)

  由於閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能會導致內存占用過多,所以在絕對必要時再考慮使用閉包。

  作用域鏈的這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最後一個值。因為閉包所保存的是整個變量對象,而不是某個特殊的變量:

function createFunctions(){
    var result = new Array();
    for (var i=0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}

  這個函數會返回一個函數數組,且每個函數都返回 10。

  在閉包中使用 this 對象也可能會導致一些問題。我們知道, this 對象是在運行時基於函數的執行環境綁定的,而匿名函數的執行環境具有全局性,因此其 this 對象通常指向 window(在通過 call()或 apply()改變函數執行環境的情況下, this 就會指向其他對象),看下面的例子:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        return function(){
            return this.name;
        };
    }
};
alert(object.getNameFunc()()); //"The Window"(在非嚴格模式下)

  把外部作用域中的 this 對象保存在一個閉包能夠訪問到的變量裡,就可以讓閉包訪問該對象了,如下所示:

var name = "The Window";
var object = {
    name : "My Object",
    getNameFunc : function(){
        var that = this;
        return function(){
            return that.name;
        };
    }
};
alert(object.getNameFunc()()); //"My Object"

arguments 也存在同樣的問題。如果想訪問作用域中的 arguments 對象,必須將對該對象的引用保存到另一個閉包能夠訪問的變量中。

JavaScript高級程序設計(第3版)高清完整PDF中文+英文+源碼 http://www.linuxidc.com/Linux/2014-09/107426.htm

如何使用JavaScript書寫遞歸函數 http://www.linuxidc.com/Linux/2015-01/112000.htm

JavaScript核心概念及實踐 高清PDF掃描版 (邱俊濤) http://www.linuxidc.com/Linux/2014-10/108083.htm

理解JavaScript中的事件流 http://www.linuxidc.com/Linux/2014-10/108104.htm

Copyright © Linux教程網 All Rights Reserved