Javascript基礎(chǔ)系列之作用域鏈

前言

本文翻譯自scope-chai

概要

通過第變量對象的學(xué)習(xí)我們知道蚊夫,執(zhí)行上下文的數(shù)據(jù)(變量、函數(shù)聲明、函數(shù)形參)都是以屬性的方式儲存在變量對象中

我們還知道燕酷,變量對象是在進(jìn)入執(zhí)行上下文階段被創(chuàng)建和初始化查库,隨后在執(zhí)行代碼階段會對屬性值進(jìn)行更新

本文將深入討論與執(zhí)行上下文密切相關(guān)的另外一個重要的概念 —— 作用域鏈(Scope Chain

定義

如果簡單扼要地講路媚,那么作用域鏈就是與內(nèi)部函數(shù)息息相關(guān)的一個概念

眾所周知,ECMAScript允許創(chuàng)建內(nèi)部函數(shù)樊销,甚至可以將這些內(nèi)部函數(shù)作為父函數(shù)的返回值

var x = 10;

function foo() {
  var y = 20;
  function bar() {
    alert(x + y);
  }
  return bar;
}

foo()(); // 30

每個上下文都有自己的變量對象整慎;對于全局變量脏款,其變量對象就是全局對象自己本身;對于函數(shù)而言裤园,其變量對象就是活動對象

作用域鏈是所以內(nèi)部上下文和變量對象的列表撤师,用于變量查詢。比如拧揽,在上述例子中丈氓,bar上下文的作用域鏈包含了AO(bar)、AO(foo)强法、VO(global)

作用域鏈?zhǔn)且粭l變量對應(yīng)的鏈万俗,它和執(zhí)行上下文有關(guān),用于處理標(biāo)識符時候進(jìn)行變量查詢

作用域鏈在函數(shù)調(diào)用時被創(chuàng)建饮怯,它包含了活動對象(AO)和該函數(shù)的內(nèi)部屬性[[scope]].關(guān)于[[scope]]會在后面做詳細(xì)介紹

activeExecutionContext = {
    VO: {...}, // 或者 AO
    this: thisValue,
    Scope: [   // 作用域鏈
      // 所有變量對象的列表
      // 用于標(biāo)識符查找
    ]
};

上述代碼中Scope定義如下:

Scope = AO + [[Scope]]

針對我們的例子闰歪,我們可以將Scope[[scope]]用普通的ECMAScript數(shù)組來表示:

var  Scope = [VO1, VO2, ...., VOn] //作用域鏈

除此之外,還可以用多級的對象鏈的數(shù)據(jù)結(jié)構(gòu)來表示蓖墅,鏈中每一個鏈接都有對父作用域(上層變量對象)的引用

var VO1 = {__parent__: null, ... other data}; -->
var VO2 = {__parent__: VO1, ... other data}; -->

然而库倘,使用數(shù)組來表示作用域鏈會更方便,因此论矾,我們這里就采用數(shù)組的表示方式教翩。 除此之外,不論在實現(xiàn)層是否采用包含__parent__特性的分層對象鏈的數(shù)據(jù)結(jié)構(gòu)贪壳,規(guī)范對其做了抽象的定義“作用域鏈?zhǔn)且粋€對象列表”饱亿。數(shù)組就是實現(xiàn)列表這一概念最好的選擇。

下面將要介紹的AO+[[Scope]]以及標(biāo)識符的處理方式闰靴,都和函數(shù)的生命周期有關(guān)彪笼。

函數(shù)生命周期

函數(shù)的生命分為創(chuàng)建激活(調(diào)用)階段,下面分別詳細(xì)介紹

創(chuàng)建階段

我們知道蚂且,進(jìn)入上下文階段時函數(shù)聲明被儲存在變量對象/活動對象中(VO/AO)配猫。讓我們看看在全局上下文中的變量和函數(shù)聲明的例子(這里變量對象是全局對象自身,還記得杏死,是吧泵肄?)

var x = 10;

function foo() {
  var y = 20;
  alert(x + y);
}

foo(); // 30

在函數(shù)激活(調(diào)用)后,我們得到了正確(預(yù)期)的結(jié)果——30淑翼。不過腐巢,這里有個非常重要的特性

此前,我們僅僅談到當(dāng)前上下文的變量對象窒舟。這里系忙,變量y在函數(shù)foo中定義(意味著它在foo上下文的AO中),但是變量x并未在foo上下文中定義,自然不會被添加到foo的AO中惠豺。乍一看银还,變量 x 相對于函數(shù) foo 根本就不存在风宁。

fooContext.AO = {
  y: undefined // undefined – 在進(jìn)入上下文時, 20 – 在激活階段
};

那么,foo函數(shù)是如何訪問到x變量的蛹疯?一個順其自然的想法是:函數(shù)應(yīng)當(dāng)有訪問更高層上下文變量對象的權(quán)限戒财。而事實也恰是如此,就是通過函數(shù)的內(nèi)部屬性 [[Scope]]來實現(xiàn)這一機制的捺弦。

[[Scope]] 是一個包含了所有上層變量對象的分層鏈饮寞,它屬于當(dāng)前函數(shù)上下文,并在函數(shù)創(chuàng)建的時候列吼,保存在函數(shù)中幽崩。

這里要注意的很重要的一點是:[[Scope]]是在函數(shù)創(chuàng)建的時候保存起來的——靜態(tài)的(不變的),永遠(yuǎn)永遠(yuǎn)——直到函數(shù)銷毀寞钥。也就是說慌申,哪怕函數(shù)永遠(yuǎn)都不能被調(diào)用到,[[Scope]]屬性也已經(jīng)保存在函數(shù)對象上了

另外要注意的一點是:[[Scope]]Scope (作用域鏈)是不同的理郑,前者是函數(shù)的屬性蹄溉,后者是上下文的屬性。 以上述例子來說您炉,foo 函數(shù)的 [[Scope]] 如下所示:

foo.[[Scope]] = [
  globalContext.VO // === Global
];

當(dāng)函數(shù)被調(diào)用的時候柒爵,就進(jìn)入函數(shù)執(zhí)行上下文,此時活動對象唄創(chuàng)建赚爵,this作用域(作用域鏈被確定棉胀。下面我們詳細(xì)討論這個時刻。

激活階段

正如上面定義的那樣囱晴,在進(jìn)入上下文膏蚓,AO/VO 創(chuàng)建之后,上下文的Scope 屬性(作用域鏈畸写,用于變量查詢)會定義為如下所示:

Scope = AO|VO + [[Scope]]

特別注意的是活動對象是Scope數(shù)組元素的第一個元素,添加在作用域的最前端

Scope = [AO].concat([[Scope]]);

這個特性對處理標(biāo)識符非常重要

處理標(biāo)識符其實就是一個確定變量(或者函數(shù)聲明)屬于作用域鏈中哪個變量對象的過程氓扛。

此算法返回的總是一個引用類型的值枯芬,其base屬性就是對應(yīng)的變量對象(或者變量對象不存在的時候則返回null),其propertyname屬性的名字就是要查詢的標(biāo)識符采郎。

標(biāo)識符處理過程包括了對應(yīng)的變量名的屬性查詢千所,即在作用域鏈中會進(jìn)行一系列的變量對象的檢測,從作用域鏈的最底層上下文一直到最上層上下文

因此蒜埋,在查詢過程中上下文中的局部變量比上層上下文的變量會優(yōu)先被查詢到淫痰,換句話說,如果兩個相同名字的變量存在于不同的上下文中時整份,處于底層上下文的變量會優(yōu)先被找到

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); // 60

全局上下文的變量對象如下所示:

globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};

全局上下文的變量對象如下所示:

globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};

在 foo 函數(shù)創(chuàng)建的時候待错,其 [[Scope]] 屬性如下所示:

foo.[[Scope]] = [
  globalContext.VO
];

在 foo 函數(shù)激活的時候(進(jìn)入上下文時)籽孙,foo 函數(shù)上下文的活躍對象如下所示:

fooContext.AO = {
  y: 20,
  bar: <reference to function>
};

同時,foo 函數(shù)上下文的作用域鏈如下所示:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:

fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

在內(nèi)部bar函數(shù)創(chuàng)建的時候火俄,其 [[Scope]] 屬性如下所示:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];

在 bar 函數(shù)激活的時候犯建,其對應(yīng)的活躍對象如下所示:

barContext.AO = {
  z: 30
};

同時,bar 函數(shù)上下文的作用域鏈如下所示:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:

barContext.Scope = [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

如下是 x瓜客,y 和 z 標(biāo)識符的查詢過程:

- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
-- globalContext.VO // found - 10
- "y"
-- barContext.AO // not found
-- fooContext.AO // found - 20
- "z"
-- barContext.AO // found - 30

作用域的特性

下面讓我們看看與作用域鏈和函數(shù)[[scope]]屬性相關(guān)的一些重要特征适瓦。

閉包

在 ECMAScript 中,閉包和函數(shù)的[[Scope]] 屬性息息相關(guān)谱仪。正如此前介紹的玻熙,[[Scope]]是在函數(shù)創(chuàng)建的時候就保存在函數(shù)對象上了,并且直到函數(shù)銷毀的時候才消失疯攒。事實上嗦随,閉包就是函數(shù)代碼和其 [[Scope]] 屬性的組合。因此卸例,[[Scope]] 包含了函數(shù)創(chuàng)建所在的詞法環(huán)境(上層變量對象)称杨。上層上下文中的變量,可以在函數(shù)激活的時候筷转,通過變量對象的詞法鏈(函數(shù)創(chuàng)建的時候就保存起來了)查詢到

var x = 10;
function foo() {
  alert(x);
}
(function () {
  var x = 20;
  foo(); // 10, but not 20
})();

變量 x 是在 foo 函數(shù)的 [[Scope]] 中找到的姑原。對于變量查詢而言,詞法鏈?zhǔn)窃诤瘮?shù)創(chuàng)建的時候就定義的呜舒,而不是在調(diào)用函數(shù)時動態(tài)確定的(這個時候锭汛,變量 x 才會是 20)。

下面是另一個典型的閉包的例子:

function foo() {
  var x = 10;
  var y = 20;
  return function () {
    alert([x, y]);
  };
}
var x = 30;
var bar = foo(); // 返回一個匿名函數(shù)
bar(); // [10, 20]

上述例子再一次證明了處理標(biāo)識符的時候袭蝗,詞法作用域鏈?zhǔn)窃诤瘮?shù)創(chuàng)建的時候定義的 —— 變量x的值是10唤殴,而不是30。并且到腥,上述例子清楚的展示了函數(shù)(上述例子中指的是函數(shù) foo 返回的匿名函數(shù))的[[Scope]] 屬性朵逝,即使在創(chuàng)建該函數(shù)的上下文結(jié)束的時候依然存在

通過 Function 構(gòu)造器創(chuàng)建的函數(shù)的 [[Scope]]屬性

**屬性,并且通過該屬性可以獲取所有上層上下文中的變量乡范。然而配名,這里有個例外,就是當(dāng)函數(shù)通過Function構(gòu)造器創(chuàng)建的時候

var x = 10;
function foo() {
  var y = 20;
  function barFD() { // FunctionDeclaration
    alert(x);
    alert(y);
  }
  var barFE = function () { // FunctionExpression
    alert(x);
    alert(y);
  };
  var barFn = Function('alert(x); alert(y);');
  barFD(); // 10, 20
  barFE(); // 10, 20
  barFn(); // 10, "y" is not defined
}
foo();

上述例子中晋辆,函數(shù)barFn就是通過Fuction構(gòu)造器來創(chuàng)建的渠脉,這個時候變量y 就無法訪問到了。但這并不意味著函數(shù)barFn就沒有內(nèi)部的[[Scope]]屬性(否則它連變量 x 都無法訪問到)瓶佳。問題就在于當(dāng)函數(shù)通過Function構(gòu)造器來創(chuàng)建的時候芋膘,其[[Scope]]屬性永遠(yuǎn)都只包含全局對象。哪怕在上層上下文中(非全局上下文)創(chuàng)建一個閉包都是無濟于事的

二維作用域鏈查找

在作用域鏈查找的時候還有很重要的一點:需要考慮變量對象的原型(如果存在的話) -- 源于原型鏈的特性:如果一個屬性在對象中沒有直接找到,查詢將在原型鏈中繼續(xù)为朋。即常說的二維鏈查找臂拓。(1)作用域鏈環(huán)節(jié);(2)每個作用域鏈 -- 深入到原型鏈環(huán)節(jié)潜腻。如果在 Object.prototype 中定義了屬性埃儿,我們能看到這種效果。

function foo() {
  alert(x);
}
Object.prototype.x = 10;
foo(); // 10

活動對象是沒有原型的融涣,我們可以在下面的例子中看出:

function foo() {
  var x = 20;
  function bar() {
    alert(x);
  }
  bar();
}
Object.prototype.x = 10;
foo(); // 20

試想下童番,如果 bar 函數(shù)的活動對象有原型的話,屬性 x 則應(yīng)當(dāng)在Object.prototype中找到威鹿,因為它在 AO 中根本不存在剃斧。然而,上面第一個例子中忽你,在標(biāo)識符處理階段遍歷了整個作用域鏈幼东,到了全局對象(部分實現(xiàn)是這樣的),它繼承自 Object.prototype科雳,因此根蟹,最終變量 x 的值就變成了 10。

執(zhí)行代碼階段對作用域的影響

在代碼執(zhí)行階段有兩個語句能修改作用域鏈糟秘,那就是 with 聲明和 catch 語句简逮。在標(biāo)識符查詢階段,這兩者都會被添加到作用域鏈的最前面尿赚。也就是說散庶,當(dāng)有 with 或 catch 的時候,作用域鏈就會被修改如下形式:

Scope = withObject|catchObject + AO|VO + [[Scope]]

如下例子中凌净,with 語句添加了 foo 對象悲龟,使得它的屬性可以不需要前綴直接訪問。

var foo = {x: 10, y: 20};

with (foo) {
  alert(x); // 10
  alert(y); // 20
}

對應(yīng)的作用域鏈修改為如下所示:

Scope = foo + AO|VO + [[Scope]]

再看下面例子冰寻,with 對象被添加到作用域鏈的最前端:

var x = 10, y = 10;

with ({x: 20}) {
  var x = 30, y = 30;
  alert(x); // 30
  alert(y); // 30
}
alert(x); // 10
alert(y); // 30

這里發(fā)生了什么须教?在進(jìn)入上下文階段,x和y被添加到變量對象中斩芭,在代碼執(zhí)行階段没卸,發(fā)生了如下修改:

x = 10, y = 10 {x: 20} 被添加到作用域鏈的最前端
在with內(nèi)部,遇到了var聲明秒旋,當(dāng)然什么也沒創(chuàng)建,因為在進(jìn)入上下文時诀拭,所有變量已被解析添加

這里只修改了x的值迁筛,此時的x被解析后是第二步中添加到作用域鏈最前的的那個對象中的 x,x的值由20變?yōu)?0

這里也修改了 y 的值,y 是上層作用域變量對象的屬性细卧,相應(yīng)地尉桩,由 10 修改為 30
當(dāng) with 語句結(jié)束后,這個特殊對象從作用域鏈中移除(被修改后的 x - 30 也隨著對象被移除了)贪庙,也就是說蜘犁,作用域鏈回到執(zhí)行 with 語句之前的狀態(tài)
正如在最后兩個 alert 中看到的,x 的值恢復(fù)到了原先的 10止邮,而 y 的值因為在 with 語句的時候被修改過了这橙,因此變?yōu)榱?30
同樣,catch 語句會創(chuàng)建一個只包含一個屬性(異常參數(shù)名)的新對象导披。如下所示:

try {
  ...
} catch (ex) {
  alert(ex);
}

作用域鏈修改為:

var catchObject = {
  ex: 
};

Scope = catchObject + AO|VO + [[Scope]]

在 catch 從句結(jié)束后屈扎,作用域鏈同樣也會恢復(fù)到之前的狀態(tài)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市撩匕,隨后出現(xiàn)的幾起案子鹰晨,更是在濱河造成了極大的恐慌,老刑警劉巖止毕,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件模蜡,死亡現(xiàn)場離奇詭異,居然都是意外死亡扁凛,警方通過查閱死者的電腦和手機忍疾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來令漂,“玉大人膝昆,你說我怎么就攤上這事〉兀” “怎么了荚孵?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長纬朝。 經(jīng)常有香客問我收叶,道長,這世上最難降的妖魔是什么共苛? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任判没,我火速辦了婚禮,結(jié)果婚禮上隅茎,老公的妹妹穿的比我還像新娘澄峰。我一直安慰自己,他們只是感情好辟犀,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布俏竞。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪魂毁。 梳的紋絲不亂的頭發(fā)上玻佩,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音席楚,去河邊找鬼咬崔。 笑死,一個胖子當(dāng)著我的面吹牛烦秩,可吹牛的內(nèi)容都是我干的垮斯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼闻镶,長吁一口氣:“原來是場噩夢啊……” “哼甚脉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起铆农,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤牺氨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后墩剖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猴凹,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年岭皂,在試婚紗的時候發(fā)現(xiàn)自己被綠了郊霎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡爷绘,死狀恐怖书劝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情土至,我是刑警寧澤购对,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站陶因,受9級特大地震影響骡苞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜楷扬,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一解幽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烘苹,春花似錦躲株、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吞琐。三九已至,卻和暖如春然爆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背黍图。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工曾雕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人助被。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓剖张,卻偏偏與公主長得像,于是被迫代替她去往敵國和親揩环。 傳聞我的和親對象是個殘疾皇子搔弄,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容