JavaScript詞法作用域與調(diào)用對象深入理解
更新時間:2012年11月29日 14:25:37 作者:
關(guān)于 Javascript 的函數(shù)作用域、調(diào)用對象和閉包之間的關(guān)系很微妙,關(guān)于它們的文章已經(jīng)有很多,本文做了一些總結(jié),需要的朋友可以參考下
關(guān)于 Javascript 的函數(shù)作用域、調(diào)用對象和閉包之間的關(guān)系很微妙,關(guān)于它們的文章已經(jīng)有很多,但不知道為什么很多新手都難以理解。我就嘗試用比較通俗的語言來表達我自己的理解吧。
作用域 Scope
Javascript 中的函數(shù)屬于詞法作用域,也就是說函數(shù)在它被定義時的作用域中運行而不是在被執(zhí)行時的作用域內(nèi)運行。這是犀牛書上的說法。但"定義時"和"執(zhí)行(被調(diào)用)時"這兩個東西有些人搞不清楚。簡單來說,一個函數(shù)A在"定義時"就是 function A(){} 這個語句執(zhí)行的時候就是定義這個函數(shù)的時候,而A被調(diào)用的時候是 A() 這個語句執(zhí)行的時候。這兩個概念一定要分清楚。
那詞法作用域(以下稱之為"作用域",除非特別指明)到底是什么呢?它是個抽象的概念,說白了它就是一個"范圍",scope 在英文里就是范圍的意思。一個函數(shù)的作用域是它被定義時它所處的"范圍",也就是它外層的"范圍",這個"范圍"包含了外層的變量屬性,這個"范圍"被設(shè)置成這個函數(shù)的一個內(nèi)部狀態(tài)。一個全局函數(shù)被定義的時候,全局(這個函數(shù)的外層)的"范圍"就被設(shè)置成這個全局函數(shù)的一個內(nèi)部狀態(tài)。一個嵌套函數(shù)被定義的時候,被嵌套函數(shù)(外層函數(shù))的"范圍"就被設(shè)置成這個嵌套函數(shù)的一個內(nèi)部狀態(tài)。這個"內(nèi)部狀態(tài)"實際上可以理解成作用域鏈,見下文。
照以上說法,一個函數(shù)的作用域是它被定義的時候所處的"范圍",那么 Javascript 里的函數(shù)作用域是在函數(shù)被定義的時候就確定了,所以它是靜態(tài)的作用域,詞法作用域又稱為靜態(tài)作用域。
調(diào)用對象 Call Object
一個函數(shù)的調(diào)用對象是動態(tài)的,它是在這個函數(shù)被調(diào)用時才被實例化的。我們已經(jīng)知道,當一個函數(shù)被定義的時候,已經(jīng)確定了它的作用域鏈。當 Javascript 解釋器調(diào)用一個函數(shù)的時候,它會添加一個新的對象(調(diào)用對象)到這個作用域鏈的前面。這個調(diào)用對象的一個屬性被初始化成一個名叫 arguments 的屬性,它引用了這個函數(shù)的 Arguments 對象,Arguments 對象是函數(shù)的實際參數(shù)。所有用 var 語句聲明的本地變量也被定義在這個調(diào)用對象里。這個時候,調(diào)用對象處在作用域鏈的頭部,本地變量、函數(shù)形式參數(shù)和 Arguments 對象全部都在這個函數(shù)的范圍里了。當然,這個時候本地變量、函數(shù)形式參數(shù)和 Arguments 對象就覆蓋了作用域鏈里同名的屬性。
作用域、作用域鏈和調(diào)用對象之間的關(guān)系
我的理解是,作用域是是抽象的,而調(diào)用對象是實例化的。
在函數(shù)被定義的時候,實際上也是它外層函數(shù)執(zhí)行的時候,它確定的作用域鏈實際上是它外層函數(shù)的調(diào)用對象鏈;當函數(shù)被調(diào)用時,它的作用域鏈是根據(jù)定義的時候確定的作用域鏈(它外層函數(shù)的調(diào)用對象鏈)加上一個實例化的調(diào)用對象。所以函數(shù)的作用域鏈實際上是調(diào)用對象鏈。在一個函數(shù)被調(diào)用的時候,它的作用域鏈(或者稱調(diào)用對象鏈)實際上是它在被定義的時候確定的作用域鏈的一個超集。
它們之間的關(guān)系可以表示成:作用域?作用域鏈?調(diào)用對象。
太繞口了,舉例說明吧:
function f(x) {
var g = function () { return x; }
return g;
}
var g1 = f(1);
alert(g1()); //輸出 1
假設(shè)我們把全局看成類似以下這樣的一個大匿名函數(shù):
(function() {
//這里是全局范圍
})();
那么例子就可以看成是:
(function() {
function f(x) {
var g = function () { return x; }
return g;
}
var g1 = f(1);
alert(g1()); //輸出 1
})();
全局的大匿名函數(shù)被定義的時候,它沒有外層,所以它的作用域鏈是空的。
全局的大匿名函數(shù)直接被執(zhí)行,全局的作用域鏈里只有一個 '全局調(diào)用對象'。
函數(shù) f 被定義,此時函數(shù) f 的作用域鏈是它外層的作用域鏈,即 '全局調(diào)用對象'。
函數(shù) f(1) 被執(zhí)行,它的作用域鏈是新的 f(1) 調(diào)用對象加上函數(shù) f 被定義的時候的作用域鏈,即 'f(1) 調(diào)用對象->全局調(diào)用對象'。
函數(shù) g (它要被返回給 g1,就命名為 g1吧)在 f(1) 中被定義,它的作用域鏈是它外層的函數(shù) f(1) 的作用域鏈,即 'f(1) 調(diào)用對象->全局調(diào)用對象'。
函數(shù) f(1) 返回函數(shù) g 的定義給 g1。
函數(shù) g1 被執(zhí)行,它的作用域鏈是新的 g(1) 調(diào)用對象加上外層 f(1) 的作用域鏈,即 'g1 調(diào)用對象->f(1)調(diào)用對象->全局調(diào)用對象'。
這樣看就很清楚了吧。
閉包 Closuer
閉包的一個簡單的說法是,當嵌套函數(shù)在被嵌套函數(shù)之外調(diào)用的時候,就形成了閉包。
之前的那個例子其實就是一個閉包。g1 是在 f(1) 內(nèi)部定義的,卻在 f(1) 返回后才被執(zhí)行。可以看出,閉包的一個效果就是被嵌套函數(shù) f 返回后,它內(nèi)部的資源不會被釋放。在外部調(diào)用 g 函數(shù)時,g 可以訪問 f 的內(nèi)部變量。根據(jù)這個特性,可以寫出很多優(yōu)雅的代碼。
例如要在一個頁面上作一個統(tǒng)一的計數(shù)器,如果用閉包的寫法,可以這么寫:
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
這樣,在內(nèi)存中就維持了一個變量 i,整個程序中的其它地方都無法直接操作 i 的值,只能通過 counter 的兩個操作。
在 setTimeout(fn, delay) 的時候,我們不能給 fn 這個函數(shù)句柄傳參數(shù),但可以通過閉包的方法把需要的參數(shù)綁定到 fn 內(nèi)部。
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
改用閉包的方式可以很容易綁定要傳進去的參數(shù):
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
閉包還有一個很常用的地方,就是在綁定事件的回調(diào)函數(shù)的時候。也是同樣的道理,綁定的函數(shù)句柄不能做參數(shù),但可以通過閉包的形式把參數(shù)綁定進去。
總結(jié)
函數(shù)的詞法作用域和作用域鏈是不同的東西,詞法作用域是抽象概念,作用域鏈是實例化的調(diào)用對象鏈。
函數(shù)在被定義的時候,同時也是它外層的函數(shù)在被執(zhí)行的時候。
函數(shù)在被定義的時候它的詞法作用域就已經(jīng)確定了,但它仍然是抽象的概念,沒有也不能被實例化。
函數(shù)在被定義的時候還確定了一個東西,就是它外層函數(shù)的作用域鏈,這個是實例化的東西。
函數(shù)在被多次調(diào)用的時候,它的作用域鏈都是不同的。
閉包很強大。犀牛書說得對,理解了這些東西,你就可以自稱是高級 Javascript 程序員了。因為利用好這些概念,可以玩轉(zhuǎn) Javascript 的很多設(shè)計模式。
作用域 Scope
Javascript 中的函數(shù)屬于詞法作用域,也就是說函數(shù)在它被定義時的作用域中運行而不是在被執(zhí)行時的作用域內(nèi)運行。這是犀牛書上的說法。但"定義時"和"執(zhí)行(被調(diào)用)時"這兩個東西有些人搞不清楚。簡單來說,一個函數(shù)A在"定義時"就是 function A(){} 這個語句執(zhí)行的時候就是定義這個函數(shù)的時候,而A被調(diào)用的時候是 A() 這個語句執(zhí)行的時候。這兩個概念一定要分清楚。
那詞法作用域(以下稱之為"作用域",除非特別指明)到底是什么呢?它是個抽象的概念,說白了它就是一個"范圍",scope 在英文里就是范圍的意思。一個函數(shù)的作用域是它被定義時它所處的"范圍",也就是它外層的"范圍",這個"范圍"包含了外層的變量屬性,這個"范圍"被設(shè)置成這個函數(shù)的一個內(nèi)部狀態(tài)。一個全局函數(shù)被定義的時候,全局(這個函數(shù)的外層)的"范圍"就被設(shè)置成這個全局函數(shù)的一個內(nèi)部狀態(tài)。一個嵌套函數(shù)被定義的時候,被嵌套函數(shù)(外層函數(shù))的"范圍"就被設(shè)置成這個嵌套函數(shù)的一個內(nèi)部狀態(tài)。這個"內(nèi)部狀態(tài)"實際上可以理解成作用域鏈,見下文。
照以上說法,一個函數(shù)的作用域是它被定義的時候所處的"范圍",那么 Javascript 里的函數(shù)作用域是在函數(shù)被定義的時候就確定了,所以它是靜態(tài)的作用域,詞法作用域又稱為靜態(tài)作用域。
調(diào)用對象 Call Object
一個函數(shù)的調(diào)用對象是動態(tài)的,它是在這個函數(shù)被調(diào)用時才被實例化的。我們已經(jīng)知道,當一個函數(shù)被定義的時候,已經(jīng)確定了它的作用域鏈。當 Javascript 解釋器調(diào)用一個函數(shù)的時候,它會添加一個新的對象(調(diào)用對象)到這個作用域鏈的前面。這個調(diào)用對象的一個屬性被初始化成一個名叫 arguments 的屬性,它引用了這個函數(shù)的 Arguments 對象,Arguments 對象是函數(shù)的實際參數(shù)。所有用 var 語句聲明的本地變量也被定義在這個調(diào)用對象里。這個時候,調(diào)用對象處在作用域鏈的頭部,本地變量、函數(shù)形式參數(shù)和 Arguments 對象全部都在這個函數(shù)的范圍里了。當然,這個時候本地變量、函數(shù)形式參數(shù)和 Arguments 對象就覆蓋了作用域鏈里同名的屬性。
作用域、作用域鏈和調(diào)用對象之間的關(guān)系
我的理解是,作用域是是抽象的,而調(diào)用對象是實例化的。
在函數(shù)被定義的時候,實際上也是它外層函數(shù)執(zhí)行的時候,它確定的作用域鏈實際上是它外層函數(shù)的調(diào)用對象鏈;當函數(shù)被調(diào)用時,它的作用域鏈是根據(jù)定義的時候確定的作用域鏈(它外層函數(shù)的調(diào)用對象鏈)加上一個實例化的調(diào)用對象。所以函數(shù)的作用域鏈實際上是調(diào)用對象鏈。在一個函數(shù)被調(diào)用的時候,它的作用域鏈(或者稱調(diào)用對象鏈)實際上是它在被定義的時候確定的作用域鏈的一個超集。
它們之間的關(guān)系可以表示成:作用域?作用域鏈?調(diào)用對象。
太繞口了,舉例說明吧:
復(fù)制代碼 代碼如下:
function f(x) {
var g = function () { return x; }
return g;
}
var g1 = f(1);
alert(g1()); //輸出 1
假設(shè)我們把全局看成類似以下這樣的一個大匿名函數(shù):
(function() {
//這里是全局范圍
})();
那么例子就可以看成是:
(function() {
function f(x) {
var g = function () { return x; }
return g;
}
var g1 = f(1);
alert(g1()); //輸出 1
})();
全局的大匿名函數(shù)被定義的時候,它沒有外層,所以它的作用域鏈是空的。
全局的大匿名函數(shù)直接被執(zhí)行,全局的作用域鏈里只有一個 '全局調(diào)用對象'。
函數(shù) f 被定義,此時函數(shù) f 的作用域鏈是它外層的作用域鏈,即 '全局調(diào)用對象'。
函數(shù) f(1) 被執(zhí)行,它的作用域鏈是新的 f(1) 調(diào)用對象加上函數(shù) f 被定義的時候的作用域鏈,即 'f(1) 調(diào)用對象->全局調(diào)用對象'。
函數(shù) g (它要被返回給 g1,就命名為 g1吧)在 f(1) 中被定義,它的作用域鏈是它外層的函數(shù) f(1) 的作用域鏈,即 'f(1) 調(diào)用對象->全局調(diào)用對象'。
函數(shù) f(1) 返回函數(shù) g 的定義給 g1。
函數(shù) g1 被執(zhí)行,它的作用域鏈是新的 g(1) 調(diào)用對象加上外層 f(1) 的作用域鏈,即 'g1 調(diào)用對象->f(1)調(diào)用對象->全局調(diào)用對象'。
這樣看就很清楚了吧。
閉包 Closuer
閉包的一個簡單的說法是,當嵌套函數(shù)在被嵌套函數(shù)之外調(diào)用的時候,就形成了閉包。
之前的那個例子其實就是一個閉包。g1 是在 f(1) 內(nèi)部定義的,卻在 f(1) 返回后才被執(zhí)行。可以看出,閉包的一個效果就是被嵌套函數(shù) f 返回后,它內(nèi)部的資源不會被釋放。在外部調(diào)用 g 函數(shù)時,g 可以訪問 f 的內(nèi)部變量。根據(jù)這個特性,可以寫出很多優(yōu)雅的代碼。
例如要在一個頁面上作一個統(tǒng)一的計數(shù)器,如果用閉包的寫法,可以這么寫:
復(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
這樣,在內(nèi)存中就維持了一個變量 i,整個程序中的其它地方都無法直接操作 i 的值,只能通過 counter 的兩個操作。
在 setTimeout(fn, delay) 的時候,我們不能給 fn 這個函數(shù)句柄傳參數(shù),但可以通過閉包的方法把需要的參數(shù)綁定到 fn 內(nèi)部。
復(fù)制代碼 代碼如下:
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
改用閉包的方式可以很容易綁定要傳進去的參數(shù):
復(fù)制代碼 代碼如下:
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
閉包還有一個很常用的地方,就是在綁定事件的回調(diào)函數(shù)的時候。也是同樣的道理,綁定的函數(shù)句柄不能做參數(shù),但可以通過閉包的形式把參數(shù)綁定進去。
總結(jié)
函數(shù)的詞法作用域和作用域鏈是不同的東西,詞法作用域是抽象概念,作用域鏈是實例化的調(diào)用對象鏈。
函數(shù)在被定義的時候,同時也是它外層的函數(shù)在被執(zhí)行的時候。
函數(shù)在被定義的時候它的詞法作用域就已經(jīng)確定了,但它仍然是抽象的概念,沒有也不能被實例化。
函數(shù)在被定義的時候還確定了一個東西,就是它外層函數(shù)的作用域鏈,這個是實例化的東西。
函數(shù)在被多次調(diào)用的時候,它的作用域鏈都是不同的。
閉包很強大。犀牛書說得對,理解了這些東西,你就可以自稱是高級 Javascript 程序員了。因為利用好這些概念,可以玩轉(zhuǎn) Javascript 的很多設(shè)計模式。
相關(guān)文章
優(yōu)化 JavaScript 代碼的方法小結(jié)
客戶端腳本能讓你的應(yīng)用更加地動態(tài)和活躍, 但是瀏覽器對代碼的解析可能造成效率問題, 而這種性能差異在客戶端之間也不盡相同。 這里我們討論和給出一些優(yōu)化你的 JavaScript 代碼的提示和最佳實踐。2009-07-07KnockoutJS 3.X API 第四章之數(shù)據(jù)控制流if綁定和ifnot綁定
這篇文章主要介紹了KnockoutJS 3.X API 第四章之數(shù)據(jù)控制流if綁定和ifnot綁定的相關(guān)資料,需要的朋友可以參考下2016-10-10