JavaScript中的調用對象 做用域 閉包

      在JavaScript中,在全部函數體以外聲明的變量爲全局變量,而在函數體內聲明的變量(經過var關鍵字)爲局部變量。事實上,全局變量是全局對象的屬性而已,好比在客戶端的JavaScript中,咱們聲明的變量實際上是window對象的屬性,如此而已。
javascript

      那麼,局部變量又隸屬於什麼對象呢?就是咱們要討論的調用對象。在執行一個函數時,函數的參數和其局部變量會做爲調用對象的屬性進行存儲。同時,解釋器會爲函數建立一個執行器上下文(Execution Context),與上下文對應起來的是一個做用域鏈。顧名思義,做用域鏈是關於做用域的鏈,一般實現爲一個鏈表,鏈表的每一個項都是一個對象,在全局做用域中,該鏈中有且只有一個對象,即全局對象。對應的,在一個函數中,做用域鏈上會有兩個對象,第一個(首先被訪問到的)爲調用對象,第二個爲全局對象。 java

若是函數須要用到某個變量,則解釋器會遍歷做用域鏈,好比: 程序員

var str = "global";
function scopeTest(){
    print(str);
    var str = "local";
    print(str);
}

當解釋器進入scopeTest函數的時候,一個調用對象就被建立了,其中包含了str變量做爲其中的一個屬性並被初始化爲undefined,當執行到第一個print(str)時,解釋器會在做用域鏈中查找str,找到以後,打印其值爲undefined,而後執行賦值語句,此時調用對象的屬性 str會被賦值爲local,所以第二個print(str)語句會打印local。 設計模式

      應該注意的是,做用域鏈隨着嵌套函數的層次會變的很長,可是查找變量的過程依舊是遍歷做用域鏈(鏈表),一直向上查找,直到找出該值,若是遍歷完做用域鏈仍然沒有找到對應的屬性,則變量未定義。(定義未賦值返回undefined,未定義會引發運行時的錯誤not defined)。 閉包

      嘗試讀一個未聲明的變量的值,Javascript會生成一個錯誤。若是嘗試給一個味用var聲明的變量賦值,Javascript會隱式聲明該變量,注意,隱式聲明的變量老是被建立爲全局變量,即便該變量只是在一個函數體內使用。防止在建立局部變量時建立全局變量,必須在函數體內部使用var語句。 函數

    一個函數的調用對象是動態的,它是在這個函數被調用時才被實例化的。咱們已經知道,當一個函數被定義的時候,已經肯定了它的做用域鏈。當 Javascript 解釋器調用一個函數的時候,它會添加一個新的對象(調用對象)到這個做用域鏈的前面。這個調用對象的一個屬性被初始化成一個名叫 arguments 的屬性,它引用了這個函數的 Arguments 對象,Arguments 對象是函數的實際參數。全部用 var 語句聲明的本地變量也被定義在這個調用對象裏。這個時候,調用對象處在做用域鏈的頭部,本地變量、函數形式參數和 Arguments 對象所有都在這個函數的範圍裏了。固然,這個時候本地變量、函數形式參數和 Arguments 對象就覆蓋了做用域鏈裏同名的屬性。 spa

做用域、做用域鏈和調用對象之間的關係

在函數被定義的時候,實際上也是它外層函數執行的時候,它肯定的做用域鏈其實是它外層函數的調用對象鏈;當函數被調用時,它的做用域鏈是根據定義的時候 肯定的做用域鏈(它外層函數的調用對象鏈)加上一個實例化的調用對象。因此函數的做用域鏈其實是調用對象鏈。在一個函數被調用的時候,它的做用域鏈(或 者稱調用對象鏈)其實是它在被定義的時候肯定的做用域鏈的一個超集。它們之間的關係能夠表示成:做用域⊃做用域鏈⊇調用對象設計

function f(x) {
    var g = function () { return x; }
    return g;
}
var g1 = f(1);
alert(g1());  //輸出 1
假設咱們把全局當作相似如下這樣的一個大匿名函數:

(function() { //這裏是全局範圍 })();
那麼例子就能夠當作是:

(function() {
    function f(x) {
        var g = function () { return x; }
        return g;
    }
    var g1 = f(1);
    alert(g1());  //輸出 1
})();
  1. 全局的大匿名函數被定義的時候,它沒有外層,因此它的做用域鏈是空的。
  2. 全局的大匿名函數直接被執行,全局的做用域鏈裏只有一個 '全局調用對象'。
  3. 函數 f 被定義,此時函數 f 的做用域鏈是它外層的做用域鏈,即 '全局調用對象'。
  4. 函數 f(1) 被執行,它的做用域鏈是新的 f(1) 調用對象加上函數 f 被定義的時候的做用域鏈,即 'f(1) 調用對象->全局調用對象'。
  5. 函數 g (它要被返回給 g1,就命名爲 g1吧)在 f(1) 中被定義,它的做用域鏈是它外層的函數 f(1) 的做用域鏈,即 'f(1) 調用對象->全局調用對象'。
  6. 函數 f(1) 返回函數 g 的定義給 g1。
  7. 函數 g1 被執行,它的做用域鏈是新的 g(1) 調用對象加上外層 f(1) 的做用域鏈,即 'g1 調用對象->f(1)調用對象->全局調用對象'。

閉包 Closuer

閉包的一個簡單的說法是,當嵌套函數在被嵌套函數以外調用的時候,就造成了閉包。 code

以前的那個例子其實就是一個閉包。g1 是在 f(1) 內部定義的,卻在 f(1) 返回後才被執行。能夠看出,閉包的一個效果就是被嵌套函數 f 返回後,它內部的資源不會被釋放。在外部調用 g 函數時,g 能夠訪問 f 的內部變量。根據這個特性,能夠寫出不少優雅的代碼。 對象

例如要在一個頁面上做一個統一的計數器,若是用閉包的寫法,能夠這麼寫:

var counter  = (function() {
    var i = 0;
    var fns = {"get": function() {return i;},
               "inc": function() {return ++i;}};
    return fns;
})();
//do something
counter.inc();
//do something else
counter.inc();
var c_value = counter.get();  //now c_value is 2

這樣,在內存中就維持了一個變量 i,整個程序中的其它地方都沒法直接操做 i 的值,只能經過 counter 的兩個操做。

在 setTimeout(fn, delay) 的時候,咱們不能給 fn 這個函數句柄傳參數,但能夠經過閉包的方法把須要的參數綁定到 fn 內部。

for(var i=0,delay=1000; i< 5; i++, delay +=1000) {
    setTimeout(function() {
        console.log('i:' + i + " delay:" + delay);
    }, delay);
}

這樣,打印出來的值都是

i:5 delay:6000 i:5 delay:6000 i:5 delay:6000 i:5 delay:6000 i:5 delay:6000

改用閉包的方式能夠很容易綁定要傳進去的參數:

for(var i=0, delay=1000; i < 5; i++, delay += 1000) {
    (function(a, _delay) { 
        setTimeout(function() { 
            console.log('i:'+a+" delay:"+_delay);
        }, _delay);
    })(i, delay);
}

輸出:

i:0 delay:1000 i:1 delay:2000 i:2 delay:3000 i:3 delay:4000 i:4 delay:5000

閉包還有一個很經常使用的地方,就是在綁定事件的回調函數的時候。也是一樣的道理,綁定的函數句柄不能作參數,但能夠經過閉包的形式把參數綁定進去。

總結

  1. 函數的詞法做用域和做用域鏈是不一樣的東西,詞法做用域是抽象概念,做用域鏈是實例化的調用對象鏈。
  2. 函數在被定義的時候,同時也是它外層的函數在被執行的時候。
  3. 函數在被定義的時候它的詞法做用域就已經肯定了,但它仍然是抽象的概念,沒有也不能被實例化。
  4. 函數在被定義的時候還肯定了一個東西,就是它外層函數的做用域鏈,這個是實例化的東西。
  5. 函數在被屢次調用的時候,它的做用域鏈都是不一樣的。
  6. 閉包很強大。犀牛書說得對,理解了這些東西,你就能夠自稱是高級 Javascript 程序員了。由於利用好這些概念,能夠玩轉 Javascript 的不少設計模式。
相關文章
相關標籤/搜索