在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ HTML/ 作用域鏈(Scope Chain)
代碼復(fù)用模式(避免篇)
S.O.L.I.D 五大原則之接口隔離原則 ISP
設(shè)計模式之狀態(tài)模式
JavaScript 核心(晉級高手必讀篇)
設(shè)計模式之建造者模式
JavaScript 與 DOM(上)——也適用于新手
設(shè)計模式之中介者模式
設(shè)計模式之裝飾者模式
設(shè)計模式之模板方法
設(shè)計模式之外觀模式
強(qiáng)大的原型和原型鏈
設(shè)計模式之構(gòu)造函數(shù)模式
揭秘命名函數(shù)表達(dá)式
深入理解J avaScript 系列(結(jié)局篇)
執(zhí)行上下文(Execution Contexts)
函數(shù)(Functions)
《你真懂 JavaScript 嗎?》答案詳解
設(shè)計模式之適配器模式
設(shè)計模式之組合模式
設(shè)計模式之命令模式
S.O.L.I.D 五大原則之單一職責(zé) SRP
編寫高質(zhì)量 JavaScript 代碼的基本要點
求值策略
閉包(Closures)
對象創(chuàng)建模式(上篇)
This? Yes,this!
設(shè)計模式之代理模式
變量對象(Variable Object)
S.O.L.I.D 五大原則之里氏替換原則 LSP
面向?qū)ο缶幊讨话憷碚?/span>
設(shè)計模式之單例模式
Function 模式(上篇)
S.O.L.I.D 五大原則之依賴倒置原則 DIP
設(shè)計模式之迭代器模式
立即調(diào)用的函數(shù)表達(dá)式
設(shè)計模式之享元模式
設(shè)計模式之原型模式
根本沒有“JSON 對象”這回事!
JavaScript 與 DOM(下)
面向?qū)ο缶幊讨?ECMAScript 實現(xiàn)
全面解析 Module 模式
對象創(chuàng)建模式(下篇)
設(shè)計模式之職責(zé)鏈模式
S.O.L.I.D 五大原則之開閉原則 OCP
設(shè)計模式之橋接模式
設(shè)計模式之策略模式
設(shè)計模式之觀察者模式
代碼復(fù)用模式(推薦篇)
作用域鏈(Scope Chain)
Function 模式(下篇)
設(shè)計模式之工廠模式

作用域鏈(Scope Chain)

前言

在第 12 章關(guān)于變量對象的描述中,我們已經(jīng)知道一個執(zhí)行上下文的數(shù)據(jù)(變量、函數(shù)聲明和函數(shù)的形參)作為屬性存儲在變量對象中。

同時我們也知道變量對象在每次進(jìn)入上下文時創(chuàng)建,并填入初始值,值的更新出現(xiàn)在代碼執(zhí)行階段。

這一章專門討論與執(zhí)行上下文直接相關(guān)的更多細(xì)節(jié),這次我們將提及一個議題——作用域鏈。

定義

如果要簡要的描述并展示其重點,那么作用域鏈大多數(shù)與內(nèi)部函數(shù)相關(guān)。

我們知道,ECMAScript 允許創(chuàng)建內(nèi)部函數(shù),我們甚至能從父函數(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)。

但是,讓我們仔細(xì)研究這個問題。

讓我們從定義開始,并進(jìn)深一步的討論示例。

作用域鏈與一個執(zhí)行上下文相關(guān),變量對象的鏈用于在標(biāo)識符解析中變量查找。

函數(shù)上下文的作用域鏈在函數(shù)調(diào)用時創(chuàng)建的,包含活動對象和這個函數(shù)內(nèi)部的[[scope]]屬性。下面我們將更詳細(xì)的討論一個函數(shù)的[[scope]]屬性。

在上下文中示意如下:

activeExecutionContext = {
    VO: {...}, // or AO
    this: thisValue,
    Scope: [ // Scope chain
      // 所有變量對象的列表
      // for identifiers lookup
    ]
};

其 scope 定義如下:

Scope = AO + [[Scope]]

這種聯(lián)合和標(biāo)識符解析過程,我們將在下面討論,這與函數(shù)的生命周期相關(guān)。

函數(shù)的生命周期

函數(shù)的的生命周期分為創(chuàng)建和激活階段(調(diào)用時),讓我們詳細(xì)研究它。

函數(shù)創(chuàng)建

眾所周知,在進(jìn)入上下文時函數(shù)聲明放到變量/活動(VO/AO)對象中。讓我們看看在全局上下文中的變量和函數(shù)聲明(這里變量對象是全局對象自身,我們還記得,是吧?)

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

在函數(shù)激活時,我們得到正確的(預(yù)期的)結(jié)果--30。但是,有一個很重要的特點。

此前,我們僅僅談到有關(guān)當(dāng)前上下文的變量對象。這里,我們看到變量“y”在函數(shù)“foo”中定義(意味著它在 foo 上下文的 AO 中),但是變量“x”并未在“foo”上下文中定義,相應(yīng)地,它也不會添加到“foo”的 AO 中。乍一看,變量“x”相對于函數(shù)“foo”根本就不存在;但正如我們在下面看到的——也僅僅是“一瞥”,我們發(fā)現(xiàn),“foo”上下文的活動對象中僅包含一個屬性--“y”。

fooContext.AO = {
  y: undefined // undefined – 進(jìn)入上下文的時候是20 – at activation
};

函數(shù)“foo”如何訪問到變量“x”?理論上函數(shù)應(yīng)該能訪問一個更高一層上下文的變量對象。實際上它正是這樣,這種機(jī)制是通過函數(shù)內(nèi)部的[[scope]]屬性來實現(xiàn)的。

[[scope]]是所有父變量對象的層級鏈,處于當(dāng)前函數(shù)上下文之上,在函數(shù)創(chuàng)建時存于其中。

注意這重要的一點--[[scope]]在函數(shù)創(chuàng)建時被存儲--靜態(tài)(不變的),永遠(yuǎn)永遠(yuǎn),直至函數(shù)銷毀。即:函數(shù)可以永不調(diào)用,但[[scope]]屬性已經(jīng)寫入,并存儲在函數(shù)對象中。

另外一個需要考慮的是--與作用域鏈對比,[[scope]]是函數(shù)的一個屬性而不是上下文。考慮到上面的例子,函數(shù)“foo”的[[scope]]如下:

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

舉例來說,我們用通常的 ECMAScript 數(shù)組展現(xiàn)作用域和[[scope]]。

繼續(xù),我們知道在函數(shù)調(diào)用時進(jìn)入上下文,這時候活動對象被創(chuàng)建,this 和作用域(作用域鏈)被確定。讓我們詳細(xì)考慮這一時刻。

函數(shù)激活

正如在定義中說到的,進(jìn)入上下文創(chuàng)建 AO/VO 之后,上下文的 Scope 屬性(變量查找的一個作用域鏈)作如下定義:

Scope = AO|VO + [[Scope]]

上面代碼的意思是:活動對象是作用域數(shù)組的第一個對象,即添加到作用域的前端。

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

這個特點對于標(biāo)示符解析的處理來說很重要。

標(biāo)示符解析是一個處理過程,用來確定一個變量(或函數(shù)聲明)屬于哪個變量對象。

這個算法的返回值中,我們總有一個引用類型,它的 base 組件是相應(yīng)的變量對象(或若未找到則為 null),屬性名組件是向上查找的標(biāo)示符的名稱。引用類型的詳細(xì)信息在第 13 章 .this 中已討論。

標(biāo)識符解析過程包含與變量名對應(yīng)屬性的查找,即作用域中變量對象的連續(xù)查找,從最深的上下文開始,繞過作用域鏈直到最上層。

這樣一來,在向上查找中,一個上下文中的局部變量較之于父作用域的變量擁有較高的優(yōu)先級。萬一兩個變量有相同的名稱但來自不同的作用域,那么第一個被發(fā)現(xiàn)的是在最深作用域中。

我們用一個稍微復(fù)雜的例子描述上面講到的這些。

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

對此,我們有如下的變量/活動對象,函數(shù)的的[[scope]]屬性以及上下文的作用域鏈:

全局上下文的變量對象是:

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

在“foo”創(chuàng)建時,“foo”的[[scope]]屬性是:

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

在“foo”激活時(進(jìn)入上下文),“foo”上下文的活動對象是:

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

“foo”上下文的作用域鏈為:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

內(nèi)部函數(shù)“bar”創(chuàng)建時,其[[scope]]為:

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

在“bar”激活時,“bar”上下文的活動對象為:

barContext.AO = {
  z: 30
};

“bar”上下文的作用域鏈為:

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ù)代碼和其[[scope]]的結(jié)合。因此,作為其對象之一,[[Scope]]包括在函數(shù)內(nèi)創(chuàng)建的詞法作用域(父變量對象)。當(dāng)函數(shù)進(jìn)一步激活時,在變量對象的這個詞法鏈(靜態(tài)的存儲于創(chuàng)建時)中,來自較高作用域的變量將被搜尋。

例如:

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

我們再次看到,在標(biāo)識符解析過程中,使用函數(shù)創(chuàng)建時定義的詞法作用域--變量解析為 10,而不是 30。此外,這個例子也清晰的表明,一個函數(shù)(這個例子中為從函數(shù)“foo”返回的匿名函數(shù))的[[scope]]持續(xù)存在,即使是在函數(shù)創(chuàng)建的作用域已經(jīng)完成之后。

關(guān)于 ECMAScript 中閉包的理論和其執(zhí)行機(jī)制的更多細(xì)節(jié),閱讀 16 章閉包。

通過構(gòu)造函數(shù)創(chuàng)建的函數(shù)的[[scope]]

在上面的例子中,我們看到,在函數(shù)創(chuàng)建時獲得函數(shù)的[[scope]]屬性,通過該屬性訪問到所有父上下文的變量。但是,這個規(guī)則有一個重要的例外,它涉及到通過函數(shù)構(gòu)造函數(shù)創(chuàng)建的函數(shù)。

var x = 10;
function foo() {
  var y = 20;
  function barFD() { // 函數(shù)聲明
    alert(x);
    alert(y);
  }
  var barFE = function () { // 函數(shù)表達(dá)式
    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ù)構(gòu)造函數(shù)(Function constructor)創(chuàng)建的函數(shù)“bar”,是不能訪問變量“y”的。但這并不意味著函數(shù)“barFn”沒有[[scope]]屬性(否則它不能訪問到變量“x”)。問題在于通過函構(gòu)造函數(shù)創(chuàng)建的函數(shù)的[[scope]]屬性總是唯一的全局對象。考慮到這一點,如通過這種函數(shù)創(chuàng)建除全局之外的最上層的上下文閉包是不可能的。

二維作用域鏈查找

在作用域鏈中查找最重要的一點是變量對象的屬性(如果有的話)須考慮其中--源于 ECMAScript 的原型特性。如果一個屬性在對象中沒有直接找到,查詢將在原型鏈中繼續(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

如果函數(shù)“bar”上下文的激活對象有一個原型,那么“x”將在 Object.prototype 中被解析,因為它在 AO 中不被直接解析。但在上面的第一個例子中,在標(biāo)識符解析中,我們到達(dá)全局對象(在一些執(zhí)行中并不全是這樣),它從 Object.prototype 繼承而來,響應(yīng)地,“x”解析為 10。

同樣的情況出現(xiàn)在一些版本的 SpiderMokey 的命名函數(shù)表達(dá)式(縮寫為 NFE)中,在那里特定的對象存儲從 Object.prototype 繼承而來的函數(shù)表達(dá)式的可選名稱,在 Blackberry 中的一些版本中,執(zhí)行時激活對象從 Object.prototype 繼承。但是,關(guān)于該特色的更多細(xì)節(jié)在第 15 章函數(shù)討論。

全局和 eval 上下文中的作用域鏈

這里不一定很有趣,但必須要提示一下。全局上下文的作用域鏈僅包含全局對象。代碼 eval 的上下文與當(dāng)前的調(diào)用上下文(calling context)擁有同樣的作用域鏈。

globalContext.Scope = [
  Global
];
evalContext.Scope === callingContext.Scope;

代碼執(zhí)行時對作用域鏈的影響

在 ECMAScript 中,在代碼執(zhí)行階段有兩個聲明能修改作用域鏈。這就是 with 聲明和 catch 語句。它們添加到作用域鏈的最前端,對象須在這些聲明中出現(xiàn)的標(biāo)識符中查找。如果發(fā)生其中的一個,作用域鏈簡要的作如下修改:

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

在這個例子中添加對象,對象是它的參數(shù)(這樣,沒有前綴,這個對象的屬性變得可以訪問)。

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

作用域鏈修改成這樣:

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

我們再次看到,通過 with 語句,對象中標(biāo)識符的解析添加到作用域鏈的最前端:

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

在進(jìn)入上下文時發(fā)生了什么?標(biāo)識符“x”和“y”已被添加到變量對象中。此外,在代碼運行階段作如下修改:

  1. x = 10, y = 10;
  2. 對象{x:20}添加到作用域的前端;
  3. 在 with 內(nèi)部,遇到了 var 聲明,當(dāng)然什么也沒創(chuàng)建,因為在進(jìn)入上下文時,所有變量已被解析添加;
  4. 在第二步中,僅修改變量“x”,實際上對象中的“x”現(xiàn)在被解析,并添加到作用域鏈的最前端,“x”為20,變?yōu)?30;
  5. 同樣也有變量對象“y”的修改,被解析后其值也相應(yīng)的由 10 變?yōu)?30;
  6. 此外,在 with 聲明完成后,它的特定對象從作用域鏈中移除(已改變的變量“x”--30也從那個對象中移除),即作用域鏈的結(jié)構(gòu)恢復(fù)到 with 得到加強(qiáng)以前的狀態(tài)。
  7. 在最后兩個 alert 中,當(dāng)前變量對象的“x”保持同一,“y”的值現(xiàn)在等于30,在with聲明運行中已發(fā)生改變。

同樣,catch 語句的異常參數(shù)變得可以訪問,它創(chuàng)建了只有一個屬性的新對象--異常參數(shù)名。圖示看起來像這樣:

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

作用域鏈修改為:

var catchObject = {
  ex: <exception object>
};
Scope = catchObject + AO|VO + [[Scope]]

在 catch 語句完成運行之后,作用域鏈恢復(fù)到以前的狀態(tài)。

結(jié)論

在這個階段,我們幾乎考慮了與執(zhí)行上下文相關(guān)的所有常用概念,以及與它們相關(guān)的細(xì)節(jié)。按照計劃--函數(shù)對象的詳細(xì)分析:函數(shù)類型(函數(shù)聲明,函數(shù)表達(dá)式)和閉包。順便說一下,在這篇文章中,閉包直接與[[scope]]屬性相關(guān),但是,關(guān)于它將在合適的篇章中討論。

其它參考

  1. [[Scope]]
  2. Scope Chain and Identifier Resolution