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

前言

本文翻譯自scope-chai

概要

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

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

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

定義

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

眾所周知,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

每個(gè)上下文都有自己的變量對(duì)象;對(duì)于全局變量,其變量對(duì)象就是全局對(duì)象自己本身;對(duì)于函數(shù)而言,其變量對(duì)象就是活動(dòng)對(duì)象

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

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

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

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

上述代碼中Scope定義如下:

Scope = AO + [[Scope]]

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

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

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

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

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

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

函數(shù)生命周期

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

創(chuàng)建階段

我們知道,進(jìn)入上下文階段時(shí)函數(shù)聲明被儲(chǔ)存在變量對(duì)象/活動(dòng)對(duì)象中(VO/AO)。讓我們看看在全局上下文中的變量和函數(shù)聲明的例子(這里變量對(duì)象是全局對(duì)象自身,還記得,是吧?)

var x = 10;

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

foo(); // 30

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

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

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

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

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

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

另外要注意的一點(diǎn)是:[[Scope]]Scope (作用域鏈)是不同的,前者是函數(shù)的屬性,后者是上下文的屬性。 以上述例子來(lái)說(shuō),foo 函數(shù)的 [[Scope]] 如下所示:

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

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

激活階段

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

Scope = AO|VO + [[Scope]]

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

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

這個(gè)特性對(duì)處理標(biāo)識(shí)符非常重要

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

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

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

因此,在查詢過(guò)程中上下文中的局部變量比上層上下文的變量會(huì)優(yōu)先被查詢到,換句話說(shuō),如果兩個(gè)相同名字的變量存在于不同的上下文中時(shí),處于底層上下文的變量會(huì)優(yōu)先被找到

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

全局上下文的變量對(duì)象如下所示:

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

全局上下文的變量對(duì)象如下所示:

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

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

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

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

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

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

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

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

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

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

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

barContext.AO = {
  z: 30
};

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

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

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

如下是 x,y 和 z 標(biāo)識(shí)符的查詢過(guò)程:

- "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ù)對(duì)象上了,并且直到函數(shù)銷(xiāo)毀的時(shí)候才消失。事實(shí)上,閉包就是函數(shù)代碼和其 [[Scope]] 屬性的組合。因此,[[Scope]] 包含了函數(shù)創(chuàng)建所在的詞法環(huán)境(上層變量對(duì)象)。上層上下文中的變量,可以在函數(shù)激活的時(shí)候,通過(guò)變量對(duì)象的詞法鏈(函數(shù)創(chuàng)建的時(shí)候就保存起來(lái)了)查詢到

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

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

下面是另一個(gè)典型的閉包的例子:

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

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

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

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

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就是通過(guò)Fuction構(gòu)造器來(lái)創(chuàng)建的,這個(gè)時(shí)候變量y 就無(wú)法訪問(wèn)到了。但這并不意味著函數(shù)barFn就沒(méi)有內(nèi)部的[[Scope]]屬性(否則它連變量 x 都無(wú)法訪問(wèn)到)。問(wèn)題就在于當(dāng)函數(shù)通過(guò)Function構(gòu)造器來(lái)創(chuàng)建的時(shí)候,其[[Scope]]屬性永遠(yuǎn)都只包含全局對(duì)象。哪怕在上層上下文中(非全局上下文)創(chuàng)建一個(gè)閉包都是無(wú)濟(jì)于事的

二維作用域鏈查找

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

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

活動(dòng)對(duì)象是沒(méi)有原型的,我們可以在下面的例子中看出:

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

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

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

在代碼執(zhí)行階段有兩個(gè)語(yǔ)句能修改作用域鏈,那就是 with 聲明和 catch 語(yǔ)句。在標(biāo)識(shí)符查詢階段,這兩者都會(huì)被添加到作用域鏈的最前面。也就是說(shuō),當(dāng)有 with 或 catch 的時(shí)候,作用域鏈就會(huì)被修改如下形式:

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

如下例子中,with 語(yǔ)句添加了 foo 對(duì)象,使得它的屬性可以不需要前綴直接訪問(wèn)。

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

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

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

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

再看下面例子,with 對(duì)象被添加到作用域鏈的最前端:

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被添加到變量對(duì)象中,在代碼執(zhí)行階段,發(fā)生了如下修改:

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

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

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

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

作用域鏈修改為:

var catchObject = {
  ex: 
};

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容