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