DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> JavaScript詞法作用域與調用對象深入理解
JavaScript詞法作用域與調用對象深入理解
編輯:關於JavaScript     
關於 Javascript 的函數作用域、調用對象和閉包之間的關系很微妙,關於它們的文章已經有很多,但不知道為什麼很多新手都難以理解。我就嘗試用比較通俗的語言來表達我自己的理解吧。
作用域 Scope
Javascript 中的函數屬於詞法作用域,也就是說函數在它被定義時的作用域中運行而不是在被執行時的作用域內運行。這是犀牛書上的說法。但"定義時"和"執行(被調用)時"這兩個東西有些人搞不清楚。簡單來說,一個函數A在"定義時"就是 function A(){} 這個語句執行的時候就是定義這個函數的時候,而A被調用的時候是 A() 這個語句執行的時候。這兩個概念一定要分清楚。
那詞法作用域(以下稱之為"作用域",除非特別指明)到底是什麼呢?它是個抽象的概念,說白了它就是一個"范圍",scope 在英文裡就是范圍的意思。一個函數的作用域是它被定義時它所處的"范圍",也就是它外層的"范圍",這個"范圍"包含了外層的變量屬性,這個"范圍"被設置成這個函數的一個內部狀態。一個全局函數被定義的時候,全局(這個函數的外層)的"范圍"就被設置成這個全局函數的一個內部狀態。一個嵌套函數被定義的時候,被嵌套函數(外層函數)的"范圍"就被設置成這個嵌套函數的一個內部狀態。這個"內部狀態"實際上可以理解成作用域鏈,見下文。
照以上說法,一個函數的作用域是它被定義的時候所處的"范圍",那麼 Javascript 裡的函數作用域是在函數被定義的時候就確定了,所以它是靜態的作用域,詞法作用域又稱為靜態作用域。
調用對象 Call Object
一個函數的調用對象是動態的,它是在這個函數被調用時才被實例化的。我們已經知道,當一個函數被定義的時候,已經確定了它的作用域鏈。當 Javascript 解釋器調用一個函數的時候,它會添加一個新的對象(調用對象)到這個作用域鏈的前面。這個調用對象的一個屬性被初始化成一個名叫 arguments 的屬性,它引用了這個函數的 Arguments 對象,Arguments 對象是函數的實際參數。所有用 var 語句聲明的本地變量也被定義在這個調用對象裡。這個時候,調用對象處在作用域鏈的頭部,本地變量、函數形式參數和 Arguments 對象全部都在這個函數的范圍裡了。當然,這個時候本地變量、函數形式參數和 Arguments 對象就覆蓋了作用域鏈裡同名的屬性。
作用域、作用域鏈和調用對象之間的關系
我的理解是,作用域是是抽象的,而調用對象是實例化的。
在函數被定義的時候,實際上也是它外層函數執行的時候,它確定的作用域鏈實際上是它外層函數的調用對象鏈;當函數被調用時,它的作用域鏈是根據定義的時候確定的作用域鏈(它外層函數的調用對象鏈)加上一個實例化的調用對象。所以函數的作用域鏈實際上是調用對象鏈。在一個函數被調用的時候,它的作用域鏈(或者稱調用對象鏈)實際上是它在被定義的時候確定的作用域鏈的一個超集。
它們之間的關系可以表示成:作用域?作用域鏈?調用對象。
太繞口了,舉例說明吧:
復制代碼 代碼如下:
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
})();

全局的大匿名函數被定義的時候,它沒有外層,所以它的作用域鏈是空的。
全局的大匿名函數直接被執行,全局的作用域鏈裡只有一個 '全局調用對象'。
函數 f 被定義,此時函數 f 的作用域鏈是它外層的作用域鏈,即 '全局調用對象'。
函數 f(1) 被執行,它的作用域鏈是新的 f(1) 調用對象加上函數 f 被定義的時候的作用域鏈,即 'f(1) 調用對象->全局調用對象'。
函數 g (它要被返回給 g1,就命名為 g1吧)在 f(1) 中被定義,它的作用域鏈是它外層的函數 f(1) 的作用域鏈,即 'f(1) 調用對象->全局調用對象'。
函數 f(1) 返回函數 g 的定義給 g1。
函數 g1 被執行,它的作用域鏈是新的 g(1) 調用對象加上外層 f(1) 的作用域鏈,即 'g1 調用對象->f(1)調用對象->全局調用對象'。
這樣看就很清楚了吧。
閉包 Closuer
閉包的一個簡單的說法是,當嵌套函數在被嵌套函數之外調用的時候,就形成了閉包。
之前的那個例子其實就是一個閉包。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
閉包還有一個很常用的地方,就是在綁定事件的回調函數的時候。也是同樣的道理,綁定的函數句柄不能做參數,但可以通過閉包的形式把參數綁定進去。
總結
函數的詞法作用域和作用域鏈是不同的東西,詞法作用域是抽象概念,作用域鏈是實例化的調用對象鏈。
函數在被定義的時候,同時也是它外層的函數在被執行的時候。
函數在被定義的時候它的詞法作用域就已經確定了,但它仍然是抽象的概念,沒有也不能被實例化。
函數在被定義的時候還確定了一個東西,就是它外層函數的作用域鏈,這個是實例化的東西。
函數在被多次調用的時候,它的作用域鏈都是不同的。
閉包很強大。犀牛書說得對,理解了這些東西,你就可以自稱是高級 Javascript 程序員了。因為利用好這些概念,可以玩轉 Javascript 的很多設計模式。
XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved