搜尋
首頁web前端js教程深入理解JavaScript系列(14) 作用域链介绍(Scope Chain)_javascript技巧

前言
在第12章关于变量对象的描述中,我们已经知道一个执行上下文 的数据(变量、函数声明和函数的形参)作为属性存储在变量对象中。
同时我们也知道变量对象在每次进入上下文时创建,并填入初始值,值的更新出现在代码执行阶段。
这一章专门讨论与执行上下文直接相关的更多细节,这次我们将提及一个议题——作用域链。
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-4-scope-chain/
中文参考:http://www.denisdeng.com/?p=908
本文绝大部分内容来自上述地址,仅做少许修改,感谢作者
定义
如果要简要的描述并展示其重点,那么作用域链大多数与内部函数相关。
我们知道,ECMAScript 允许创建内部函数,我们甚至能从父函数中返回这些函数。
复制代码 代码如下:

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

这样,很明显每个上下文拥有自己的变量对象:对于全局上下文,它是全局对象自身;对于函数,它是活动对象。
作用域链正是内部上下文所有变量对象(包括父变量对象)的列表。此链用来变量查询。即在上面的例子中,“bar”上下文的作用域链包括AO(bar)、AO(foo)和VO(global)。
但是,让我们仔细研究这个问题。
让我们从定义开始,并进深一步的讨论示例。
作用域链与一个执行上下文相关,变量对象的链用于在标识符解析中变量查找。
函数上下文的作用域链在函数调用时创建的,包含活动对象和这个函数内部的[[scope]]属性。下面我们将更详细的讨论一个函数的[[scope]]属性。
在上下文中示意如下:
复制代码 代码如下:

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

其scope定义如下:
Scope = AO + [[Scope]]
这种联合和标识符解析过程,我们将在下面讨论,这与函数的生命周期相关。
函数的生命周期
函数的的生命周期分为创建和激活阶段(调用时),让我们详细研究它。
函数创建
众所周知,在进入上下文时函数声明放到变量/活动(VO/AO)对象中。让我们看看在全局上下文中的变量和函数声明(这里变量对象是全局对象自身,我们还记得,是吧?)
复制代码 代码如下:

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

在函数激活时,我们得到正确的(预期的)结果--30。但是,有一个很重要的特点。
此前,我们仅仅谈到有关当前上下文的变量对象。这里,我们看到变量“y”在函数“foo”中定义(意味着它在foo上下文的AO中),但是变量“x”并未在“foo”上下文中定义,相应地,它也不会添加到“foo”的AO中。乍一看,变量“x”相对于函数“foo”根本就不存在;但正如我们在下面看到的——也仅仅是“一瞥”,我们发现,“foo”上下文的活动对象中仅包含一个属性--“y”。
复制代码 代码如下:

fooContext.AO = {
y: undefined // undefined – 进入上下文的时候是20 – at activation
};

函数“foo”如何访问到变量“x”?理论上函数应该能访问一个更高一层上下文的变量对象。实际上它正是这样,这种机制是通过函数内部的[[scope]]属性来实现的。
[[scope]]是所有父变量对象的层级链,处于当前函数上下文之上,在函数创建时存于其中。
注意这重要的一点--[[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。
另外一个需要考虑的是--与作用域链对比,[[scope]]是函数的一个属性而不是上下文。考虑到上面的例子,函数“foo”的[[scope]]如下:
复制代码 代码如下:

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

举例来说,我们用通常的ECMAScript 数组展现作用域和[[scope]]。
继续,我们知道在函数调用时进入上下文,这时候活动对象被创建,this和作用域(作用域链)被确定。让我们详细考虑这一时刻。
函数激活
正如在定义中说到的,进入上下文创建AO/VO之后,上下文的Scope属性(变量查找的一个作用域链)作如下定义:
Scope = AO|VO + [[Scope]]
上面代码的意思是:活动对象是作用域数组的第一个对象,即添加到作用域的前端。
Scope = [AO].concat([[Scope]]);
这个特点对于标示符解析的处理来说很重要。
标示符解析是一个处理过程,用来确定一个变量(或函数声明)属于哪个变量对象。
这个算法的返回值中,我们总有一个引用类型,它的base组件是相应的变量对象(或若未找到则为null),属性名组件是向上查找的标示符的名称。引用类型的详细信息在第13章.this中已讨论。
标识符解析过程包含与变量名对应属性的查找,即作用域中变量对象的连续查找,从最深的上下文开始,绕过作用域链直到最上层。
这样一来,在向上查找中,一个上下文中的局部变量较之于父作用域的变量拥有较高的优先级。万一两个变量有相同的名称但来自不同的作用域,那么第一个被发现的是在最深作用域中。
我们用一个稍微复杂的例子描述上面讲到的这些。
复制代码 代码如下:

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

对此,我们有如下的变量/活动对象,函数的的[[scope]]属性以及上下文的作用域链:
全局上下文的变量对象是:
复制代码 代码如下:

globalContext.VO === Global = {
x: 10
foo:
};

在“foo”创建时,“foo”的[[scope]]属性是:
复制代码 代码如下:

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

在“foo”激活时(进入上下文),“foo”上下文的活动对象是:
复制代码 代码如下:

fooContext.AO = {
y: 20,
bar:
};

“foo”上下文的作用域链为:
复制代码 代码如下:

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

内部函数“bar”创建时,其[[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”的标识符解析如下:
- "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
作用域特征
让我们看看与作用域链和函数[[scope]]属性相关的一些重要特征。
闭包
在ECMAScript中,闭包与函数的[[scope]]直接相关,正如我们提到的那样,[[scope]]在函数创建时被存储,与函数共存亡。实际上,闭包是函数代码和其[[scope]]的结合。因此,作为其对象之一,[[Scope]]包括在函数内创建的词法作用域(父变量对象)。当函数进一步激活时,在变量对象的这个词法链(静态的存储于创建时)中,来自较高作用域的变量将被搜寻。
例如:
复制代码 代码如下:

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

我们再次看到,在标识符解析过程中,使用函数创建时定义的词法作用域--变量解析为10,而不是30。此外,这个例子也清晰的表明,一个函数(这个例子中为从函数“foo”返回的匿名函数)的[[scope]]持续存在,即使是在函数创建的作用域已经完成之后。
关于ECMAScript中闭包的理论和其执行机制的更多细节,阅读16章闭包。
通过构造函数创建的函数的[[scope]]
在上面的例子中,我们看到,在函数创建时获得函数的[[scope]]属性,通过该属性访问到所有父上下文的变量。但是,这个规则有一个重要的例外,它涉及到通过函数构造函数创建的函数。
复制代码 代码如下:

var x = 10;
function foo() {
var y = 20;
function barFD() { // 函数声明
alert(x);
alert(y);
}
var barFE = function () { // 函数表达式
alert(x);
alert(y);
};
var barFn = Function('alert(x); alert(y);');
barFD(); // 10, 20
barFE(); // 10, 20
barFn(); // 10, "y" is not defined
}
foo();

我们看到,通过函数构造函数(Function constructor)创建的函数“bar”,是不能访问变量“y”的。但这并不意味着函数“barFn”没有[[scope]]属性(否则它不能访问到变量“x”)。问题在于通过函构造函数创建的函数的[[scope]]属性总是唯一的全局对象。考虑到这一点,如通过这种函数创建除全局之外的最上层的上下文闭包是不可能的。
二维作用域链查找
在作用域链中查找最重要的一点是变量对象的属性(如果有的话)须考虑其中--源于ECMAScript 的原型特性。如果一个属性在对象中没有直接找到,查询将在原型链中继续。即常说的二维链查找。(1)作用域链环节;(2)每个作用域链--深入到原型链环节。如果在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”上下文的激活对象有一个原型,那么“x”将在Object.prototype 中被解析,因为它在AO中不被直接解析。但在上面的第一个例子中,在标识符解析中,我们到达全局对象(在一些执行中并不全是这样),它从Object.prototype继承而来,响应地,“x”解析为10。
同样的情况出现在一些版本的SpiderMokey 的命名函数表达式(缩写为NFE)中,在那里特定的对象存储从Object.prototype继承而来的函数表达式的可选名称,在Blackberry中的一些版本中,执行时激活对象从Object.prototype继承。但是,关于该特色的更多细节在第15章函数讨论。
全局和eval上下文中的作用域链
这里不一定很有趣,但必须要提示一下。全局上下文的作用域链仅包含全局对象。代码eval的上下文与当前的调用上下文(calling context)拥有同样的作用域链。
复制代码 代码如下:

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

代码执行时对作用域链的影响
在ECMAScript 中,在代码执行阶段有两个声明能修改作用域链。这就是with声明和catch语句。它们添加到作用域链的最前端,对象须在这些声明中出现的标识符中查找。如果发生其中的一个,作用域链简要的作如下修改:
Scope = withObject|catchObject + AO|VO + [[Scope]]
在这个例子中添加对象,对象是它的参数(这样,没有前缀,这个对象的属性变得可以访问)。
复制代码 代码如下:

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

作用域链修改成这样:
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

在进入上下文时发生了什么?标识符“x”和“y”已被添加到变量对象中。此外,在代码运行阶段作如下修改:
x = 10, y = 10;
对象{x:20}添加到作用域的前端;
在with内部,遇到了var声明,当然什么也没创建,因为在进入上下文时,所有变量已被解析添加;
在第二步中,仅修改变量“x”,实际上对象中的“x”现在被解析,并添加到作用域链的最前端,“x”为20,变为30;
同样也有变量对象“y”的修改,被解析后其值也相应的由10变为30;
此外,在with声明完成后,它的特定对象从作用域链中移除(已改变的变量“x”--30也从那个对象中移除),即作用域链的结构恢复到with得到加强以前的状态。
在最后两个alert中,当前变量对象的“x”保持同一,“y”的值现在等于30,在with声明运行中已发生改变。
同样,catch语句的异常参数变得可以访问,它创建了只有一个属性的新对象--异常参数名。图示看起来像这样:
复制代码 代码如下:

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

作用域链修改为:
复制代码 代码如下:

var catchObject = {
ex:
};
Scope = catchObject + AO|VO + [[Scope]]

在catch语句完成运行之后,作用域链恢复到以前的状态。
结论
在这个阶段,我们几乎考虑了与执行上下文相关的所有常用概念,以及与它们相关的细节。按照计划--函数对象的详细分析:函数类型(函数声明,函数表达式)和闭包。顺便说一下,在这篇文章中,闭包直接与[[scope]]属性相关,但是,关于它将在合适的篇章中讨论。我很乐意在评论中回答你的问题。
其它参考
陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
在JavaScript中替換字符串字符在JavaScript中替換字符串字符Mar 11, 2025 am 12:07 AM

JavaScript字符串替換方法詳解及常見問題解答 本文將探討兩種在JavaScript中替換字符串字符的方法:在JavaScript代碼內部替換和在網頁HTML內部替換。 在JavaScript代碼內部替換字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 該方法僅替換第一個匹配項。要替換所有匹配項,需使用正則表達式並添加全局標誌g: str = str.replace(/fi

自定義Google搜索API設置教程自定義Google搜索API設置教程Mar 04, 2025 am 01:06 AM

本教程向您展示瞭如何將自定義的Google搜索API集成到您的博客或網站中,提供了比標準WordPress主題搜索功能更精緻的搜索體驗。 令人驚訝的是簡單!您將能夠將搜索限制為Y

示例顏色json文件示例顏色json文件Mar 03, 2025 am 12:35 AM

本文系列在2017年中期進行了最新信息和新示例。 在此JSON示例中,我們將研究如何使用JSON格式將簡單值存儲在文件中。 使用鍵值對符號,我們可以存儲任何類型的

構建您自己的Ajax Web應用程序構建您自己的Ajax Web應用程序Mar 09, 2025 am 12:11 AM

因此,在這裡,您準備好了解所有稱為Ajax的東西。但是,到底是什麼? AJAX一詞是指用於創建動態,交互式Web內容的一系列寬鬆的技術。 Ajax一詞,最初由Jesse J創造

8令人驚嘆的jQuery頁面佈局插件8令人驚嘆的jQuery頁面佈局插件Mar 06, 2025 am 12:48 AM

利用輕鬆的網頁佈局:8 ESTISSEL插件jQuery大大簡化了網頁佈局。 本文重點介紹了簡化該過程的八個功能強大的JQuery插件,對於手動網站創建特別有用

什麼是這個'在JavaScript?什麼是這個'在JavaScript?Mar 04, 2025 am 01:15 AM

核心要點 JavaScript 中的 this 通常指代“擁有”該方法的對象,但具體取決於函數的調用方式。 沒有當前對象時,this 指代全局對象。在 Web 瀏覽器中,它由 window 表示。 調用函數時,this 保持全局對象;但調用對象構造函數或其任何方法時,this 指代對象的實例。 可以使用 call()、apply() 和 bind() 等方法更改 this 的上下文。這些方法使用給定的 this 值和參數調用函數。 JavaScript 是一門優秀的編程語言。幾年前,這句話可

通過來源查看器提高您的jQuery知識通過來源查看器提高您的jQuery知識Mar 05, 2025 am 12:54 AM

jQuery是一個很棒的JavaScript框架。但是,與任何圖書館一樣,有時有必要在引擎蓋下發現發生了什麼。也許是因為您正在追踪一個錯誤,或者只是對jQuery如何實現特定UI感到好奇

10張移動秘籍用於移動開發10張移動秘籍用於移動開發Mar 05, 2025 am 12:43 AM

該帖子編寫了有用的作弊表,參考指南,快速食譜以及用於Android,BlackBerry和iPhone應用程序開發的代碼片段。 沒有開發人員應該沒有他們! 觸摸手勢參考指南(PDF)是Desig的寶貴資源

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
2 週前By尊渡假赌尊渡假赌尊渡假赌
倉庫:如何復興隊友
1 個月前By尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒險:如何獲得巨型種子
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器