Description
前言
在《JavaScript深入之执行上下文栈》中讲到,当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
今天重点讲讲作用域链。
作用域链
在《JavaScript深入之变量对象》中讲到,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。
函数创建
在《JavaScript深入之词法作用域和动态作用域》中讲到,函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
举个例子:
function foo() {
function bar() {
...
}
}
函数创建时,各自的[[scope]]为:
foo.[[scope]] = [
globalContext.VO
];
bar.[[scope]] = [
fooContext.AO,
globalContext.VO
];
函数激活
当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。
这时候执行上下文的作用域链,我们命名为 Scope:
Scope = [AO].concat([[Scope]]);
至此,作用域链创建完毕。
捋一捋
以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
执行过程如下:
1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]
checkscope.[[scope]] = [
globalContext.VO
];
2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [
checkscopeContext,
globalContext
];
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
5.第三步:将活动对象压入 checkscope 作用域链顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];
下一篇文章
《JavaScript深入之从ECMAScript规范解读this》
本文相关链接
深入系列
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
Activity
[-]JavaScript深入之从ECMAScript规范解读this[/-][+]JavaScript深入之作用域链[/+]menglingfei commentedon May 5, 2017
大神你好,问你一个问题,checkscope函数被创建时,保存到[[scope]]的作用域链和checkscope执行前的准备工作中,复制函数[[scope]]属性创建的作用域链有什么不同么?为什么会有两个作用域链?
mqyqingfeng commentedon May 5, 2017
checkscope函数创建的时候,保存的是根据词法所生成的作用域链,checkscope执行的时候,会复制这个作用域链,作为自己作用域链的初始化,然后根据环境生成变量对象,然后将这个变量对象,添加到这个复制的作用域链,这才完整的构建了自己的作用域链。至于为什么会有两个作用域链,是因为在函数创建的时候并不能确定最终的作用域的样子,为什么会采用复制的方式而不是直接修改呢?应该是因为函数会被调用很多次吧。
suoz commentedon Jun 8, 2017
@menglingfei 在js中复制有分两种,比如说基本类型的复制,就是直接的赋值,两个变量以后互不影响。而引用类型的复制,是指两个变量同时指向一个对象。我觉得这里应该说的是后者吧。
yh284914425 commentedon Jun 23, 2017
函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,想问一下变量对象是创建上下文的时候才有的吧
function foo() {
function bar() {
...
}
}
要是foo没有创建上下文,那bar怎么保存foo的变量对象啊
mqyqingfeng commentedon Jun 23, 2017
@yh284914425 以你举的例子为例的话,当 foo 函数的执行上下文初始化的时候,才会创建 bar 函数。
yh284914425 commentedon Jun 24, 2017
@mqyqingfeng 好吧,当函数创建的时候,就会保存所有父变量对象到其中这个过程感觉不是很明白,能说的清楚一些吗,以我举的例子为例的话,当 foo 函数的执行上下文初始化的时候,才会创建 bar 函数,bar函数保存foo的变量对象,那更外层的变量对象呢
173 remaining items
jayguojianhai commentedon Apr 2, 2022
adjfks commentedon May 9, 2022
在《javascript忍者秘籍》里说到的词法环境[[Environment]]与这里的[[Scope]]和作用域链有什么关系?感觉书里讲的[[Environment]]很像作用域链
zengmanying commentedon May 9, 2022
caozhongran commentedon Aug 24, 2022
大佬 可以举例说明下 什么情况下会遇到函数预解析时的:《 第二次复制函数[[scope]]属性创建作用域链》的逻辑吗 这块还是没有理解透彻
zengmanying commentedon Aug 24, 2022
returnMaize commentedon Jun 20, 2023
函数执行上下文中的对象是不是就等于这个函数的作用域
也就是说,每个可执行代码块的执行上下文 = 代码块作用域 + 作用域链 + this
zengmanying commentedon Jun 20, 2023
wjdnb commentedon Nov 13, 2024
这里应该想说的是完整的构建了自己的执行上下文吧?而不是作用域链
zengmanying commentedon Nov 13, 2024
rainbowyy commentedon Nov 13, 2024