摘要:代码执行阶段在这个阶段会生成三个重要的东西变量对象,作用域链变量对象在函数上下文中,我们用活动对象来表示变量对象。这时候执行上下文的作用域链,我们命名为至此,作用域链创建完毕。
好久没更新文章了,这一憋就是一个大的。
说起js中的概念,执行上下文和作用域应该是大家最容易混淆的,你说混淆就混淆吧,其实大多数人在开发的时候不是很关注这两个名词,但是这里面偏偏还夹杂好多其他的概念--变量提升啊,闭包啊,this啊!
因此,搞明白这两者的关系对深入javascript至关重要
JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。
上面提到的可执行代码,那么什么是可执行代码呢?
其实很简单,就三种,全局代码、函数代码、eval代码。
其中eval代码大家可以忽略,毕竟实际开发中处于性能考虑基本不会用到,所以接下来我们重点关注的就是全局代码、函数代码
在庞大的代码里必然不会只有一两个函数,那么如何管理每次执行函数时候创建的上下文呢
js引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
ECStack = [];
试想当js开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以 ECStack 最底部永远有个 globalContext:
ECStack = [ globalContext ];
举个?:
function out(){ function inner(){} inner() } out()
那么这个函数的执行上下文栈会经历以下过程:
ECStack.push(globalContext) ECStack.push(outContext) ECStack.push(innerContext) ECStack.pop(innerContext) ECStack.pop(outContext) ECStack.pop(globalContext)
再来看一个闭包的?:
function f1(){ var n=999; function f2(){ console.log(n) } return f2; } var result=f1(); result(); // 999
该函数的执行上下文栈会经历以下过程:
ECStack.push(globalContext) ECStack.push(f1Context) ECStack.pop(f1Context) ECStack.push(resultContext) ECStack.pop(resultContext) ECStack.pop(globalContext)
大家自行感受一下对比,一定要记住上下文是在函数调用的时候才会生产
既然调用一个函数时一个新的执行上下文会被创建。那执行上下文的生命周期同样可以分为两个阶段。
创建阶段
在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。
代码执行阶段
在这个阶段会生成三个重要的东西
a.变量对象(Variable object,VO)
b.作用域链(Scope chain)
c.this
变量对象
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
活动对象其实就是被激活的变量对象,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object,而只有活动对象上的各种属性才能被访问。
执行上下文的代码会分成两个阶段进行处理:分析(进入)和执行
进入执行上下文
当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:
函数的所有形参 (如果是函数上下文)
a.由名称和对应值组成的一个变量对象的属性被创建
b.没有实参,属性值设为 undefined
函数声明
a.由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
b.如果变量对象已经存在相同名称的属性,则完全替换这个属性
变量声明
a.由名称和对应值(undefined)组成一个变量对象的属性被创建;
b.如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
根据这个规则,理解变量提升就变得十分简单了
举个?分析下,看下面的代码:
function foo(a) { console.log(b) console.log(c) var b = 2; function c() {} var d = function() {}; b = 3; } foo(1);
在进入执行上下文后,这时候的 AO 是:
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined }
代码执行
在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值
还是上面的例子,当代码执行完后,这时候的 AO 是:
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" }
因此,这个例子代码执行顺序就是这样的
function foo(a) { var b function c() {} var d console.log(b) console.log(c) b = 2; function c() {} d = function() {}; b = 3; }作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。
而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。
经典的一道面试题
var a = 1 function out(){ var a = 2 inner() } function inner(){ console.log(a) } out() //====> 1作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。
上面讲到函数作用域是在创建的阶段确定
这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!
举个?
function out() { function inner() { ... } }
函数创建时,各自的[[scope]]为:
out.[[scope]] = [ globalContext.VO ]; inner.[[scope]] = [ outContext.AO, globalContext.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 的属性值
ECStack = [ globalContext ];
至此,关于作用域和执行上下文的介绍就到这里,希望大家多消化,有问题请在评论中及时指出
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/93598.html
摘要:之前一篇文章我们详细说明了变量对象,而这里,我们将详细说明作用域链。而的作用域链,则同时包含了这三个变量对象,所以的执行上下文可如下表示。下图展示了闭包的作用域链。其中为当前的函数调用栈,为当前正在被执行的函数的作用域链,为当前的局部变量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初学JavaScrip...
摘要:理解了这句话,我们就可以来看闭包了闭包前面说过,函数可以访问函数作用域链中的变量,但如果我们想在函数外访问函数内却不行了。 不管是闭包还是this关键字,都是困扰JS初学者的比较难懂的东西,如果你对它们的认识还不足够清晰,那么现在就一起把它们掌握掉。还是那句话,我们从最基本的开始,建立起一个非常清晰的知识结构,好了,开始吧 ? 闭包 当然我们今天说的是javascript里的闭包。要学...
摘要:全局和上下文中的作用域链这里不一定很有趣,但必须要提示一下。全局上下文的作用域链仅包含全局对象。代码的上下文与当前的调用上下文拥有同样的作用域链。代码执行时对作用域链的影响在中,在代码执行阶段有两个声明能修改作用域链。 1 定义 我们已经知道一个执行上下文中的数据(参数,变量,函数)作为属性存储在变量对象中。 也知道变量对象是在每次进入上下文是创建并填入初始值,值的更新出现在代码执行阶...
摘要:一看这二逼就是周杰伦的死忠粉看看控制台输出,确实没错就是对象。从根本上来说,作用域是基于函数的,而执行环境是基于对象的例如全局执行环境即全局对象。全局对象全局属性和函数可用于所有内建的对象。全局对象只是一个对象,而不是类。 觉得本人写的不算很烂的话,可以登录关注一下我的GitHub博客,博客会坚持写下去。 今天同学去面试,做了两道面试题,全部做错了,发过来给我看,我一眼就看出来了,因为...
摘要:回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。若是使用回调函数进行处理,代码就可以继续进行其他任务,而无需空等。参考理解回调函数理解与使用中的回调函数这篇相当不错回调函数 为什么写回调函数 对于javascript中回调函数 一直处于理解,但是应用不好的阶段,总是在别人家的代码中看到很巧妙的回调,那时候会有wow c...
阅读 2903·2021-10-27 14:19
阅读 539·2021-10-18 13:29
阅读 1131·2021-07-29 13:56
阅读 3548·2019-08-30 13:19
阅读 1929·2019-08-29 12:50
阅读 1039·2019-08-23 18:16
阅读 3525·2019-08-22 15:37
阅读 1900·2019-08-22 15:37