JavaScript 深入之执行上下文
对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO)
- 作用域链(Scope chain)
- this
思考题
这样一道思考题:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope();
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();
两段代码都会打印 local scope。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢?
两者的区别在于执行上下文栈的变化不一样,然而,如果是这样笼统的回答,依然显得不够详细,本篇就会详细的解析执行上下文栈和执行上下文的具体变化过程。
具体执行分析
我们分析第一段代码:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope();
执行过程如下:
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [ globalContext ];
2.全局上下文初始化
globalContext = { VO: [global], Scope: [globalContext.VO], this: globalContext.VO }
2.初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]]
checkscope.[[scope]] = [ globalContext.VO ];
3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈
ECStack = [ checkscopeContext, globalContext ];
4.checkscope 函数执行上下文初始化:
- 复制函数 [[scope]] 属性创建作用域链,
- 用 arguments 创建活动对象,
- 初始化活动对象,即加入形参、函数声明、变量声明,
- 将活动对象压入 checkscope 作用域链顶端。
同时 f 函数被创建,保存作用域链到 f 函数的内部属性 [[scope]]
checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }
5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
ECStack = [ fContext, checkscopeContext, globalContext ];
6.f 函数执行上下文初始化, 以下跟第 4 步相同:
- 复制函数 [[scope]] 属性创建作用域链
- 用 arguments 创建活动对象
- 初始化活动对象,即加入形参、函数声明、变量声明
- 将活动对象压入 f 作用域链顶端
fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined }
7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值
8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
ECStack = [ checkscopeContext, globalContext ];
9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
ECStack = [ globalContext ];
第二段代码就留给大家去尝试模拟它的执行过程。
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()();
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: JavaScript 深入之闭包
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(42)
函数被创建时,保存作用域链到函数的内部属性[[scope]]这一步的一点个人理解,不知是否正确
最近才看到这个博客,好多概念有了全新的认识,感谢博主的分享。
有几个问题不知道自己的理解是否正确,以代码1举例说明
函数的创建和执行的关系。
全局中创建的函数是在全局上下文执行的时候创建的;函数a内部创建的函数b是在函数a执行的时候创建的。
在代码1中:
checkscope函数是在全局上下文执行的时候创建的,f函数是在checkscope函数执行时被创建的。
疑问1:如果没有最后一句checkscope();执行checkscope函数,f函数就不会被创建,那么f函数的[[scope]]以及fContext均不会生成?
函数创建时[[scope]]属性的生成。
疑问2:函数创建时[[scope]]属性是否是此时执行上下文栈中最上层的执行上下文对象的Scope的引用?
在代码1中:
checkscope.[[scope]] 就是ECStack中最上层上下文globalContext.Scope的引用
f.[[scope]] 就是此时ECStack中最上层上下文checkscopeContext.Scope的引用
在代码1中我的这个想法貌似能说通。
以上两个疑问还请聚聚们赐教
总感觉执行函数的作用域链长度 == 执行上下文栈的长度,不知道这样理解对不对
@mqyqingfeng博主大大你好:
我有以下几个问题没有弄清楚,如果博主能提供解答将不胜感激。
1.函数执行上下文中的作用域链[[scope]]表示是将链表转成数组的形式方便理解还是本身就是以数组的形式?
2.如果1中是链表的形式,那头节点是不是还会有一个类似next指针的属性。如果按我的理解应该函数创建时的词法作用域应该包含创建环境的VO和[scope]作用域。也就是每个节点有个[[scope]]属性可以向上追述查找。
3.可执行代码的作用域和作用域链的概念有些模糊。作用域是指的函数创建时的词法作用域(创建时执行环境的AO集合)?函数执行时的执行上下文(当前执行时的AO集合)?函数的作用域链能访问到的所有AO集合?
建议把调用函数的时候创建活动变量时,会存在变量提升,这个时候变量提升又分为let,var,我感觉把这部分加进去会更好,多谢楼主分享
我的理解:在表达是内部有一个隐式的var b的变量(跟表达式同名)。因此,在内部如果再次显式的var b =20,那显然会覆盖掉隐式的。但是,没有var的关键字声明,那么b=20只不过是修改这个隐式的b,而同时,这个b是只读的,不可修改的。因此输出的还是Function b.
其实你只要在开头加上'use strict',那么执行就会直接报错:b=20. 因为b是constant variable
第一个,函数b里面引用了函数a中的变量 aaa,所以形成了闭包, 第二个,函数b 没有引用函数a中的变量aaa,因为b函数内部用var声明了aaa,打印应该是undefined, 没有形成闭包, 所以 console.dir(b) 结果也不一样
和我的想法有点差异,欢迎交流。
你自己也说了呀 。如果一个函数定义表达式包含名称,函数的局部作用域将会包含一个绑定到函数对象的名称。实际上,函数的名称将成为函数内部的一个局部变量。 既然这个变量b(指向命名函数表达式的引用)存在于函数内部, 那么进行 b = 20 的赋值操作时,首先会找到 指向命名函数表达式引用的变量b,进行赋值,而这个变量又是不可改变的, 自然不会产生任何效果,输出都是命名函数表达式了
假如我们用JS数组这种数据结构来模拟栈的话,根据栈“先进先出“的特点,在进栈时可以理解为 要到 整个栈(数组)的头部去,也就是用unshift方法 ,当然也看你怎么来定义 头部和尾部对应数组的开头还是结尾了。。
function b () {
b = 20
console.log(b)
}
b() // 20
大大,还是这里,如果不用立即执行函数,打印的b就是20, 使用立即函数打印的就是函数b
function b(){} 等于window.b,当执行函数b后把window.b重新赋值为20,所以打印会是20。立即执行函数中函数名优先级高些,函数体中给函数同名变量赋值不会覆盖原有操作。
分析第二段代码
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈:
2.开始执行代码,全局上下文初始化:
3.初始化的同时,
checkscope
函数被创建,保存作用域链到内部属性[[scope]]
:4.开始执行
checkscope
函数,创建checkscope
函数执行上下文,并将checkscope
函数上下文压入执行上下文栈:5.
checkscope
函数上下文初始化:checkscope
函数作用域链顶端。初始化的同时,
f函数
被创建,保存作用域链到f函数
的内部属性[[scope]]
:6.
checkscope
函数执行,随着函数的执行,修改AO
的值,所以此时checkscopeContext
变更为:接着返回了
f函数
.7.
checkscope
函数执行完毕,checkscope
执行上下文从执行上下文栈中弹出:8.开始执行
f 函数
,创建f
函数执行上下文,并将f
函数上下文压入执行上下文栈:9.
f
函数上下文初始化:f 函数
作用域链顶端。10.
f
函数执行,沿着作用域链查找scope
的值,找到并返回了scope
.可是当
f 函数
执行的时候,checkscope 函数
上下文已经被销毁了(即从执行上下文栈中被弹出),怎么还会读取到checkscope
作用域下的scope
值呢?这是因为
checkscope
函数执行上下文初始化时,f 函数
同时被创建,保存作用域链到f 函数
的内部属性[[scope]]
,所以即使checkscope
函数执行完毕,被弹出执行上下文栈,但是checkscopeContext.AO
依然存在于f 函数
的内部属性[scope]]
中:所以在
f 函数
执行的时候仍然可以通过f 函数
的作用域链能找到scope
.所以这里就产生了闭包:11.
f
函数执行完毕,f
执行上下文从执行上下文栈中弹出:大佬,这个虽然你解读的很合理,但是还是有点疑惑,函数不是在调用时才执行嘛?你解读的怎么会先执行var a1=t()这样的函数呢?如果把例子改成把nAdd()放在var a1=t() 的上一行呢 @mqyqingfeng
关于 this 有个疑问: 箭头函数的执行上下文是没有自己 this 的,继承定义时执行上下文的 this 。 那问题来了,定义时的上下文执行完成就销毁了,那么当箭头函数执行时。是怎么确定this的指向的, 我目前的理解是把this看成一个普通属性,保存在AO中,函数访问this就像查找普通变量一样,通过原型链来访问。
关于 this 有个疑问: 箭头函数的执行上下文是没有自己 this 的,继承定义时执行上下文的 this 。 那问题来了,定义时的上下文执行完成就销毁了,那么当箭头函数执行时。是怎么确定this的指向的, 我目前的理解是把this看成一个普通属性,保存在AO中,函数访问this就像查找普通变量一样,通过原型链来访问。
对于第二个例子,请教一下,各位大佬看下理解的对吗?
@zuoyi615 哈哈,确实不好理解,因为涉及到很多规范上的内容,需要边查规范边读,但我也正是通过研究 this 第一次克服了对于全英文的规范的恐惧,希望你也去试一试~
第一个函数查找上级作用域中scope
第二个函数式闭包,保存了父级函数中scope的引用
所以两个值相等;
博主,请问那个nAdd(); 什么时候调用的? 我看不懂
@Flying-Eagle2 当然是执行这个函数的时候调用的啦~
同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]
checkscope预编译阶段,形参、函数f声明、变量scope声明。
f 函数被创建的活动是在checkscope函数预编译阶段进行还是在函数执行阶段进行的?
@suoz 我认为是在 checkscope 函数预编译阶段
@mqyqingfeng 大大
你看我这么理解对么~
执行函数checkscope时,分为预编译阶段和执行阶段,预编译阶段就是你所说的创建执行上下文、执行上下文初始化(复制函数[[scope]]属性创建作用域链、使用arguments创建活动对象、初始化活动对象{即形参、函数声明、变量声明}、将活动对象压入作用域链的顶端)。
当函数checkscope执行,处于预编译阶段中函数声明的时候,此时只是创建了f函数(只是创建了f函数的[[scope]]属性,这个属性只包含了checkscope函数的活动对象和全局变量对象,并不包含f函数的活动对象)
等到函数checkscope处于执行阶段时,就是
return f();
,此时调用f(),这时候才会创建f函数的上下文,以及上面所提到的相同四步骤。@suoz 是哒~ o( ̄▽ ̄)d
全局上下文初始化里面的VO里面的global是什么情况啊?
globalContext = {
VO: [global, scope, checkscope],
Scope: [globalContext.VO],
this: globalContext.VO
}
@yh284914425 这个 global 表示全局对象哈~
大神,能不能帮我分析下 下面执行上下文的具体处理过程 谢谢
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
@yh284914425 非常好的问题!但这个问题涉及到的知识点,其实整个系列文章都没有讲到过,日后我一定补上。
具体原因可以参考汤姆大叔的文章,简单的说一说,是因为当解释器在代码执行阶段遇到命名的函数表达式时,会创建辅助的特定对象,然后将函数表达式的名称即 b 添加到特定对象上作为唯一的属性,因此函数内部才可以读取到 b,但是这个值是 DontDelete 以及 ReadOnly 的,所以对它的操作并不生效,所以打印的结果自然还是这个函数,而外部的 b 值也没有发生更改。
@mqyqingfeng 好的,期待您的文章,您说的创建辅助的特定对象还是执行上下文不?
@yh284914425 具体我还没有研究过,我的猜想就是一个对象,储存了函数表达式的名称,然后将其添加到了 b 函数的作用域链中,大致类似于 Scope: [globalContext, {特殊对象}, AO]
博主:帮忙分析一下这个具体执行过程,我很难看懂啊!谢谢
@Flying-Eagle2
我们先看这段代码的结构,简化一下就是:
相当于先执行 _b 函数,然后将函数的执行结果作为参数传入 _a 函数
_b 函数为:
_b 函数执行
函数返回 1,然后将 1 作为参数传入 _a,相当于:
变量 p 的值就是一个函数为:
p(4) 的结果自然是 5
我就是这块没看懂
第一次返回的话函数a的值是1, this.a的值也应该是1吧;
你这个我更没看懂呢;4又是哪里传的,b 又是 谁传的?????啊啊啊啊啊,我真没看懂
好难理解呀~
分析第二段代码
1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈,并初始化全局上下文
初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性
[[scope]]
:2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈,并初始化函数上下文
同时 f 函数被创建,保存作用域链到 f 函数的内部属性
[[scope]]
:3.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
4.执行 f 函数,创建 f 函数执行上下文,并压入执行上下文栈,将其初始化
5.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值。正是因为 checkscope 函数执行上下文初始化时,f 函数同时被创建,保存作用域链到 f 函数的内部属性
[[scope]]
,所以即使checkscope函数执行完毕,被弹出执行上下文栈,但是checkscopeContext.AO
依然存在于 f 函数维护的[scope]]
:所以,闭包的概念产生了,定义:
6.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出
Over,hahahah
@flyerH 哈哈,不用这么客气,有问题就留言讨论哈~
@mqyqingfeng 非常感谢您的解答,谢谢!
@flyerH 这真的是个好问题!我们先看个简单的例子:
我们会发现,n 的值都是从 99 开始,执行 一次a1() 的时候,值会加一,再执行一次,值再加一,但是 n 在 a1() 和 a2() 并不是公用的。你可以理解为:同一个函数形成的多个闭包的值都是相互独立的。
接下来看这道题目,关键在于 nAdd 函数
当执行
var a1 = t()
的时候,变量 nAdd 被赋值为一个函数 ,这个函数是function (){n++}
,我们命名这个匿名函数为 fn1 吧。接着执行var a = t()
的时候,变量 nAdd 又被重写了,这个函数跟以前的函数长得一模一样,也是function (){n++}
,但是这已经是一个新的函数了,我们就命名为 fn2 吧。所以当执行 nAdd 函数,我们执行的是其实是 fn2,而不是 fn1,我们更改的是 a2 形成的闭包里的 n 的值,并没有更改 a1 形成的闭包里的 n 的值。所以 a1() 的结果为 99 ,a2()的结果为 100。
作者您好!之前有道题,通过看您的文章,大致有了一个猜想,但是还是不能很清晰的说出原因,烦请您看一下,谢谢!
不知是不是a2()的作用域置顶了,所以nAdd()修改的是a2()作用域里的变量,但闭包的话,同一个变量名难道不是指向同一个内存地址的值吗
从ECMAScript规范解读this,太不好理解了
@zuoyi615 this 是在函数执行的时候才确定下来的,checkscope 函数 和 f 函数的 this 的值跟作用域链没有关系,具体的取值规则还需要参照上一篇文章《JavaScript深入之从ECMAScript规范解读this》, 两者的 this 其实都是 undefined ,只是在非严格模式下,会转为全局对象。嗯,如果讲的不明白的话,就跟我说一下,我看怎么再表述下这个东西哈~
checkscope 函数 和 f 函数,在代码执行这一阶段,没有对各自的 this 做任何操作,所以沿着作用域链,最终找到全局 this 的引用,即 globalContext.VO 对象,是这样吧?