JavaScript 作用域链 难不难?

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介:

介绍

变量对象中已经介绍过,执行上下文(变量,函数声明和函数形式参数)的数据被存储为变量对象的属性

此外,我们知道每次进入上下文时都会创建变量对象并填充初始值,并且它的更新发生在代码执行阶段

举个栗子

function test(a, b) {
 console.log(c); // function c() {}
 var c = 10;
 function c() {};
 console.log(c); // 10
 c = 1;
 console.log(c); // 1
 var e = function _e() {};
 (function x() {});
}
test(10);

这次我们讨论作用域链 Scope Chain 

定义

如果要简要说明,作用域链主要与内部函数有关

正如我们所知,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]]属性组成

用伪代码可以表示为:

activeExecutionContext = {
 VO: {...}, // or AO
 this: thisValue,
 Scope: [ // Scope chain
 // list of all variable objects
 // for identifiers lookup
 ] 
};

根据定义,Scope可以表示为:

Scope = AO + [[Scope]]

我们可以将Scope和[[Scope]]表示为ECMAScript数组:

var Scope = [VO1, VO2, ..., VOn]; // scope chain

我们下面将讨论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

乍一看,“foo”函数根本不存在“x”变量,正如我们将在下面看到的,“foo”上下文的活动对象只包含一个属性“y”:

fooContext.AO = {
 y: undefined // undefined – on entering the context, 20 – at activation
};

函数“foo”如何访问“x”变量呢?函数应该可以访问更高层上下文的变量对象,实际上,确实如此,这个机制是通过函数的内部[[Scope]]属性来实现的

[[Scope]]是包含了所有父级变量对象的层级链,它位于当前函数上下文中,在函数创建时被保存到函数中 [[Scope]]是在创建函数时保存的,静态的(不变的),只有一次并且一直都存在,直到函数销毁

注意一点,[[Scope]]与Scope(作用域链)是不同的,前者是函数的属性,后者是上下文的属性 以上述例子为例,“foo”函数的[[Scope]]如下所示:

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

之后,函数调用时,会进入一个函数上下文,其中活动对象被创建,并且this值和Scope(作用域链)被确定

函数激活

正如定义中提到的那样,在进入上下文并且在创建AO / VO之后,上下文的Scope属性(作用域链,用于变量查找)定义为:

Scope = AO|VO + [[Scope]]

这里要强调活动对象是Scope数组的第一个元素,即添加到作用域链的最前面:

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

这个特征对标识符解析过程非常重要

标识符解析是确定变量(或函数声明)属于作用域链中哪个变量对象的过程

这个算法返回的是一个Reference类型的值,其base属性是相应的变量对象(如果没有找到变量,则为null),其property name属性的名字是查找到的标识符的名称,细节可参考 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: <reference to function>
};

在创建foo时,foo的[[Scope]]属性为:

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

在foo函数调用中,foo函数上下文的活动对象是:

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

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
})();

我们看到x变量在foo函数的[[Scope]中被找到,也就是说,变量的查找是在函数创建时定义的词法(闭包)链,而不是调用的动态链(否则x变量将被解析为20)

闭包的另一个经典例子:

function foo() {
 var x = 10;
 var y = 20;
 return function () {
 alert([x, y]);
 };
}
var x = 30;
var bar = foo(); // anonymous function is returned
bar(); // [10, 20]

我们再次看到,对于标识符解析,使用函数创建时定义的词法作用域链,变量x被解析为10,而不是30 此外,这个例子清楚地表明函数的[[Scope]]属性,即使在函数上下文已经结束,也会继续存在

通过Function构造器创建的函数的[[Scope]]属性

在上面的例子中,我们看到函数创建时就获得[[Scope]]属性,并通过此属性访问所有父上下文的变量,但这有一个重要的例外,就是通过Function构造器创建的函数

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();

正如我们所看到的,对于通过Function构造器创建的barFn函数,变量y不可访问,但它并不意味着barFn函数没有内部的[[Scope]]属性(否则它将无法访问变量x)

问题是通过Function构造器创建的函数的[[Scope]]属性始终只包含全局对象

二维作用域链查找

在作用域链查找中的一个重点是变量对象的原型,因为ECMAScript的原型特性: 如果在对象中没有直接找到属性,则查找会在原型链中进行

  • 在作用域链的链接上
  • 在每个作用域链接上,深入原型链链接
    如果在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

全局和eval上下文的作用域链

全局上下文的作用域链中只包含全局对象 “eval”代码的上下文和调用上下文(calling context)有相同的作用域链

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


引用

ECMA-262-3 in detail. Chapter 4. Scope chain.


原文发布时间:2018-03-06

本文来源掘金如需转载请紧急联系作者

相关文章
|
6月前
|
JavaScript 前端开发
js变量的作用域、作用域链、数据类型和转换应用案例
【4月更文挑战第27天】JavaScript 中变量有全局和局部作用域,全局变量在所有地方可访问,局部变量只限其定义的代码块。作用域链允许变量在当前块未定义时向上搜索父级作用域。语言支持多种数据类型,如字符串、数字、布尔值,可通过 `typeof` 检查类型。转换数据类型用 `parseInt` 或 `parseFloat`,将字符串转为数值。
42 1
|
6月前
|
存储 JavaScript 前端开发
解释 JavaScript 中的作用域和作用域链的概念。
【4月更文挑战第4天】JavaScript作用域定义了变量和函数的可见范围,静态决定于编码时。每个函数作为对象拥有`scope`属性,关联运行期上下文集合。执行上下文在函数执行时创建,定义执行环境,每次调用函数都会生成独特上下文。作用域链是按层级组织的作用域集合,自内向外查找变量。变量查找遵循从当前执行上下文到全局上下文的顺序,若找不到则抛出异常。
57 6
|
自然语言处理 JavaScript 前端开发
带你读《现代Javascript高级教程》一、作用域和作用域链(2)
带你读《现代Javascript高级教程》一、作用域和作用域链(2)
126 0
|
16天前
|
JavaScript 前端开发
js的作用域作用域链
【10月更文挑战第29天】理解JavaScript的作用域和作用域链对于正确理解变量的访问和生命周期、避免变量命名冲突以及编写高质量的JavaScript代码都具有重要意义。在实际开发中,需要合理地利用作用域和作用域链来组织代码结构,提高代码的可读性和可维护性。
|
6月前
|
自然语言处理 前端开发 JavaScript
深入理解JavaScript中的闭包与作用域链
在JavaScript编程中,闭包和作用域链是两个非常重要的概念,它们对于理解代码的执行过程和解决一些特定问题至关重要。本文将深入探讨JavaScript中闭包和作用域链的原理和应用,帮助读者更好地理解这些概念并能够在实际项目中灵活运用。
|
6月前
|
JavaScript 前端开发
JS作用域与作用域链
JS作用域与作用域链
|
6月前
|
缓存 前端开发 JavaScript
深入理解JavaScript闭包:解锁神秘的作用域链
深入理解JavaScript闭包:解锁神秘的作用域链
69 0
|
存储 JavaScript 前端开发
JS进阶(三) 闭包,作用域链,垃圾回收,内存泄露
闭包,作用域链,垃圾回收,内存泄露 1、函数创建 创建函数 1、开辟一个堆内存(16进制的内存地址) 2、声明当前函数的作用域(再哪个上下文创建的,它的作用域就是谁) 3、把函数体内的代码当作字符串存储在堆内存当中(所以不执行没有意义) 4、把函数的堆内存地址类似对象一样放到栈中供对象调用 执行函数 1、会形成一个全新的私有上下文(目的是供函数中的代码执行),然后进栈执行 2、在私有上下文中有一个存放私有变量的变量对象 AO(xx) 3、在代码执行之前要做的事情 - 初始化它的作用域链<自己的上下文,函数的作用域> - 初始化this (箭头函数没有this) - 初始化Arguments实参
103 0
|
6月前
|
自然语言处理 前端开发 JavaScript
【前端|Javascript第3篇】探秘JavaScript的作用域与作用域链:小白也能轻松搞懂!
【前端|Javascript第3篇】探秘JavaScript的作用域与作用域链:小白也能轻松搞懂!
|
6月前
|
JavaScript 前端开发 开发者
深入理解JavaScript作用域与作用域链
深入理解JavaScript作用域与作用域链
131 0