JavaScript 作用域链 难不难?
介绍
在变量对象中已经介绍过,执行上下文(变量,函数声明和函数形式参数)的数据被存储为变量对象的属性
此外,我们知道每次进入上下文时都会创建变量对象并填充初始值,并且它的更新发生在代码执行阶段
举个栗子
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
本文来源掘金如需转载请紧急联系作者

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
杨老师课堂之JavaSe 部分面试题
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kese7952/article/details/80535100 Java基础面试题 Java基础面试题 1.简述 path 和 classpath 的区别 2.请说说你对 JVM 的理解 3.char 型变量中能不能存贮一个中文汉字?请说出理由 4.简述 break、continue 和 return 语句的区别。 5.请简述方法重写和方法重载的区别? 6.请简述 Error 和 Exception 有什么区别? 7.请简述 synchronized 和 java.util.concurrent.locks.Lock 的异同点 8.进程和线程之间有什么不同 9.请简述装箱和拆箱的概念。 10.请简述 Hashtable 和 HashMap 的区别。 11.请简述使用泛型的优点。 12.简述 TCP/IP 协议的层次结构 1.简述 path 和 classpath 的区别 path:path 环境变量是系统环境变量中的一种,它用于保存一系列可执行文件的路径, 每个路径之间以分号分隔。当在...
- 下一篇
JavaScript基础语法,对象,正则表达式
一.JavaScript基础语法 1.基础语法 2.函数 3.常见对话框 4.html引入js文件 二.JavaScript对象 1.prototype属性扩展类的属性和方法,以及实现类的继承 2.内置对象 3.document对象 4.window对象 三.正则表达式 一.JavaScript基础语法 1..基础语法 注释:// or /**/ 弱类型变量 显式声明:var usename=”blabla”; var age=20; 隐式声明:不使用var关键字,直接给变量赋值(默认为全局变量,即使位于函数中也是全局变量) sName="myname"; undefined 和 null 的 区分 空值:nullvar x=null; 未定义值:undefinedvar x; undefined是null派生来的,alert(null==undefined); //输出true typeof undefined返回的是字符串,if(typeof undefined ==“undefined”) //true typeof null 返回的也是字符串,但是 是“object” if(t...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7设置SWAP分区,小内存服务器的救世主
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Hadoop3单机部署,实现最简伪集群
- MySQL8.0.19开启GTID主从同步CentOS8