全面了解JavaScript的作用域鏈
JavaScript的作用域鏈
這是一個(gè)非常重要的知識(shí)點(diǎn)了,了解了JavaScript的作用域鏈的話,能幫助我們理解很多‘異常'問(wèn)題。
下面我們來(lái)看一個(gè)小例子,前面我說(shuō)過(guò)的聲明提前的例子。
var name = 'Skylor.min';
function echo() {
alert(name);
var name = 'mm';
alert(name);
alert(age);
}
echo();
對(duì)于這個(gè)例子,沒(méi)有接觸過(guò)這方面的時(shí)候,第一反應(yīng)是會(huì)糾結(jié)下,這第一個(gè)的name,到底調(diào)用全局變量的name,還是函數(shù)內(nèi)部的name呢,如果調(diào)用全局的,可是函數(shù)內(nèi)部也用定義和賦值啊, 如果調(diào)用函數(shù)內(nèi)部的局部變量的話,那么他的值是mm嗎?還是引用全局的'Skylor.min'呢?
于是這個(gè)小例子就會(huì)有這樣的錯(cuò)誤答案:
Skylor.min
mm
[腳本出錯(cuò)]
其實(shí)不然,知道函數(shù)內(nèi)的提前說(shuō)明,就知道這是不正確的。
undefined
mm
[腳本出錯(cuò)]
應(yīng)該是這樣的,那到底為什么是這個(gè)答案呢,提前聲明這又是什么呢?一切的一切,涉及到JavaScript的作用域鏈。
原理
首先來(lái)說(shuō)說(shuō),JavaScript的作用域的原理:
在JavaScript權(quán)威指南中有一句很精辟的描述: JavaScript中的函數(shù)運(yùn)行在它們被定義的作用域里,而不是它們被運(yùn)行的作用域里。
另外在JavaScript中有個(gè)很重要的概念,那就是: 在JavaScript中,一切皆對(duì)象,函數(shù)也是。
在JS中,作用域的概念和其他語(yǔ)言差不多, 在每次調(diào)用一個(gè)函數(shù)的時(shí)候 ,就會(huì)進(jìn)入一個(gè)函數(shù)內(nèi)的作用域,當(dāng)從函數(shù)返回以后,就返回調(diào)用前的作用域
JS的語(yǔ)法風(fēng)格和C/C++類似, 但作用域的實(shí)現(xiàn)卻和C/C++不同,并非用“堆?!狈绞?,而是使用列表,具體過(guò)程如下(ECMA262中所述):
- 任何執(zhí)行上下文時(shí)刻的作用域, 都是由作用域鏈(scope chain, 后面介紹)來(lái)實(shí)現(xiàn)
- 在一個(gè)函數(shù)被定義的時(shí)候, 會(huì)將它定義時(shí)刻的scope chain鏈接到這個(gè)函數(shù)對(duì)象的[[scope]]屬性
- 在一個(gè)函數(shù)對(duì)象被調(diào)用的時(shí)候,會(huì)創(chuàng)建一個(gè)活動(dòng)對(duì)象(也就是一個(gè)對(duì)象), 然后對(duì)于每一個(gè)函數(shù)的形參,都命名為該活動(dòng)對(duì)象的命名屬性, 然后將這個(gè)活動(dòng)對(duì)象做為此時(shí)的作用域鏈(scope chain)最前端, 并將這個(gè)函數(shù)對(duì)象的[[scope]]加入到scope chain中.
看個(gè)例子吧:
var func = function(lps, rps){
var name = 'Skylor.min';
........
}
func();
在執(zhí)行func的定義語(yǔ)句的時(shí)候, 會(huì)創(chuàng)建一個(gè)這個(gè)函數(shù)對(duì)象的[[scope]]屬性(內(nèi)部屬性,只有JS引擎可以訪問(wèn), 但FireFox的幾個(gè)引擎(SpiderMonkey和Rhino)提供了私有屬性__parent__來(lái)訪問(wèn)它), 并將這個(gè)[[scope]]屬性, 鏈接到定義它的作用域鏈上(后面會(huì)詳細(xì)介紹), 此時(shí)因?yàn)閒unc定義在全局環(huán)境, 所以此時(shí)的[[scope]]只是指向全局活動(dòng)對(duì)象window active object.
在調(diào)用func的時(shí)候, 會(huì)創(chuàng)建一個(gè)活動(dòng)對(duì)象(假設(shè)為aObj, 由JS引擎預(yù)編譯時(shí)刻創(chuàng)建, 后面會(huì)介紹),并創(chuàng)建arguments屬性, 然后會(huì)給這個(gè)對(duì)象添加倆個(gè)命名屬性aObj.lps, aObj.rps; 對(duì)于每一個(gè)在這個(gè)函數(shù)中申明的局部變量和函數(shù)定義, 都作為該活動(dòng)對(duì)象的同名命名屬性.
然后將調(diào)用參數(shù)賦值給形參數(shù),對(duì)于缺少的調(diào)用參數(shù),賦值為undefined。
然后將這個(gè)活動(dòng)對(duì)象做為scope chain的最前端, 并將func的[[scope]]屬性所指向的,定義func時(shí)候的頂級(jí)活動(dòng)對(duì)象, 加入到scope chain.
有了上面的作用域鏈, 在發(fā)生標(biāo)識(shí)符解析的時(shí)候, 就會(huì)逆向查詢當(dāng)前scope chain列表的每一個(gè)活動(dòng)對(duì)象的屬性,如果找到同名的就返回。找不到,那就是這個(gè)標(biāo)識(shí)符沒(méi)有被定義。
注意到, 因?yàn)?strong>函數(shù)對(duì)象的[[scope]]屬性是在定義一個(gè)函數(shù)的時(shí)候決定的, 而非調(diào)用的時(shí)候, 所以如下面的例子:
var name = 'Skylor.min';
function echo() {
alert(name);
}
function env() {
var name = 'mm';
echo();
}
env();
他的運(yùn)行結(jié)果是:Skylor.min
結(jié)合上面的知識(shí), 我們來(lái)看看下面這個(gè)例子,還記得那句JavaScript權(quán)威指南中的經(jīng)典,JavaScript中的函數(shù)運(yùn)行在它們被定義的作用域里,而不是它們被運(yùn)行的作用域里。
function factory() {
var name = 'Skylor.min';
var intro = function(){
alert('I am ' + name);
}
return intro;
}
function app(para){
var name = para;
var func = factory();
func();
}
app('mm');
當(dāng)調(diào)用app的時(shí)候, scope chain是由: {window活動(dòng)對(duì)象(全局)}->{app的活動(dòng)對(duì)象} 組成.
在剛進(jìn)入app函數(shù)體時(shí), app的活動(dòng)對(duì)象有一個(gè)arguments屬性, 其他倆個(gè)值為undefined的屬性: name和func. 和一個(gè)值為'mm'的屬性para;
此時(shí)的scope chain如下:
[[scope chain]] = [
{
para : 'mm',
name : undefined,
func : undefined,
arguments : []
}, {
window call object
}
]
當(dāng)調(diào)用進(jìn)入factory的函數(shù)體的時(shí)候, 此時(shí)的factory的scope chain為:
[[scope chain]] = [
{
name : undefined,
intor : undefined
}, {
window call object
}
]
注意到, 此時(shí)的作用域鏈中, 并不包含app的活動(dòng)對(duì)象.
在定義intro函數(shù)的時(shí)候, intro函數(shù)的[[scope]]為:
[[scope chain]] = [
{
name : 'Skylor.min',
intor : undefined
}, {
window call object
}
]
從factory函數(shù)返回以后,在app體內(nèi)調(diào)用intor的時(shí)候, 發(fā)生了標(biāo)識(shí)符解析, 而此時(shí)的sope chain是:
[[scope chain]] = [
{
intro call object
}, {
name : 'Skylor.min',
intor : undefined
}, {
window call object
}
]
因?yàn)閟cope chain中,并不包含factory活動(dòng)對(duì)象. 所以, name標(biāo)識(shí)符解析的結(jié)果應(yīng)該是factory活動(dòng)對(duì)象中的name屬性, 也就是'Skylor.min'.
所以運(yùn)行結(jié)果是: I am Skylor.min
至此,完整的一個(gè)運(yùn)行流程,很清晰的能讀懂“JavaScript中的函數(shù)運(yùn)行在它們被定義的作用域里,而不是它們被運(yùn)行的作用域里。”這句話講的是什么了。
為了解釋上面的一些問(wèn)題,還得說(shuō)說(shuō)JavaScript的預(yù)編譯。
JavaScriptの預(yù)編譯
預(yù)編譯,學(xué)過(guò)C等的我們都知道,可是問(wèn)題來(lái)了,JavaScript是腳本語(yǔ)言,JavaScript的執(zhí)行過(guò)程是一種翻譯執(zhí)行的過(guò)程,那在JavaScript的執(zhí)行中,有沒(méi)有類似編譯的過(guò)程呢?
如果不是很確定,先通過(guò)一個(gè)例子:
alert(typeof fun); //function
function fun() {
alert('I am Skylor.min');
};
這時(shí)候彈出來(lái)的是?-----我去,是“I am Skylor.min”然而這時(shí)為什么呢,為啥不是undefined呢。
恩, 對(duì), 在JS中, 是有預(yù)編譯的過(guò)程的, JS在執(zhí)行每一段JS代碼之前, 都會(huì)首先處理var關(guān)鍵字和function定義式(函數(shù)定義式和函數(shù)表達(dá)式).
如上文所說(shuō), 在調(diào)用函數(shù)執(zhí)行之前, 會(huì)首先創(chuàng)建一個(gè)活動(dòng)對(duì)象, 然后搜尋這個(gè)函數(shù)中的局部變量定義,和函數(shù)定義, 將變量名和函數(shù)名都做為這個(gè)活動(dòng)對(duì)象的同名屬性, 對(duì)于局部變量定義,變量的值會(huì)在真正執(zhí)行的時(shí)候才計(jì)算, 此時(shí)只是簡(jiǎn)單的賦為undefined.
而對(duì)于函數(shù)的定義,是一個(gè)要注意的地方:
alert(typeof fun); //結(jié)果:function
alert(typeof fn); //結(jié)果:undefined
function fun() { //函數(shù)定義式
alert('I am Skylor.min');
};
var fn = function() { //函數(shù)表達(dá)式
}
alert(typeof fn); //結(jié)果:function
這就是函數(shù)定義式和函數(shù)表達(dá)式的不同, 對(duì)于函數(shù)定義式, 會(huì)將函數(shù)定義提前. 而函數(shù)表達(dá)式, 會(huì)在執(zhí)行過(guò)程中才計(jì)算.
說(shuō)到這里, 順便說(shuō)一個(gè)問(wèn)題 :
var name = 'Skylor.min';
age = 25;
我們都知道不使用var關(guān)鍵字定義的變量, 相當(dāng)于是全局變量, 聯(lián)系到我們剛才的知識(shí):
在對(duì)age做標(biāo)識(shí)符解析的時(shí)候, 因?yàn)槭菍?xiě)操作, 所以當(dāng)找到到全局的window活動(dòng)對(duì)象的時(shí)候都沒(méi)有找到這個(gè)標(biāo)識(shí)符的時(shí)候, 會(huì)在window活動(dòng)對(duì)象的基礎(chǔ)上, 返回一個(gè)值為undefined的age屬性.
也就是說(shuō), age會(huì)被定義在頂級(jí)作用域中.
現(xiàn)在, 也許你注意到了我剛才說(shuō)的: JS在執(zhí)行每一段JS代碼之前, 都會(huì)首先處理var關(guān)鍵字和function定義式(函數(shù)定義式和函數(shù)表達(dá)式).
對(duì), 讓我們看看下面的例子:
<script >
alert(typeof mm); //結(jié)果:undefined
</script >
<script >
function mm() {
alert('I am Skylor.min');
}
</script >
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
原生js實(shí)現(xiàn)tab選項(xiàng)卡切換
這篇文章主要為大家詳細(xì)介紹了原生js實(shí)現(xiàn)tab選項(xiàng)卡,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-08-08
JavaScript實(shí)現(xiàn)的簡(jiǎn)單冪函數(shù)實(shí)例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)的簡(jiǎn)單冪函數(shù),實(shí)例分析了javascript實(shí)現(xiàn)冪運(yùn)算的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-04-04
超實(shí)用的全新JavaScript事件Scrollend實(shí)例詳解
這篇文章主要為大家介紹了超實(shí)用的全新JavaScript事件Scrollend實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
Layui table field初始化加載時(shí)進(jìn)行隱藏的方法
今天小編就為大家分享一篇Layui table field初始化加載時(shí)進(jìn)行隱藏的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-09-09
CocosCreator入門(mén)教程之網(wǎng)絡(luò)通信
這篇文章主要介紹了CocosCreator的網(wǎng)絡(luò)通信,內(nèi)容不多,涉及到的細(xì)節(jié),讀者可以根據(jù)實(shí)際情況,自己去延申2021-04-04
元素的內(nèi)聯(lián)事件處理函數(shù)的特殊作用域在各瀏覽器中存在差異
在一個(gè)元素的屬性中綁定事件,實(shí)際上就創(chuàng)建了一個(gè)內(nèi)聯(lián)事件處理函數(shù)(如<h1 onclick="alert(this);"...>...</h1>),內(nèi)聯(lián)事件處理函數(shù)有其特殊的作用域鏈,并且各瀏覽器的實(shí)現(xiàn)細(xì)節(jié)也有差異。2011-01-01

