18

JavaScript深入系列第五篇,讲述作用链的创建过程,最后结合着变量对象,执行上下文栈,让我们一起捋一捋函数创建和执行的过程中到底发生了什么?

前言

《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
    }
}

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深入之词法作用域和动态作用域》

《JavaScript深入之执行上下文栈》

《JavaScript深入之变量对象》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。


冴羽
9.4k 声望6.3k 粉丝

13

引用和评论

13 条评论
头像
一禅

变量对象什么情况下不会销毁,被scope引用就不销毁?还是被作用域链引用才不销毁?

如果是scope那么函数执行完销毁作用域链,不会销毁scope,那变量对象就一直存在???

如果是作用域链,函数执行时才创建作用域链,执行之前就没有作用域链,那变量对象不是早被销毁了吗???

2019-08-23
头像
kiwi

因此完整的作用域链=当前AO+[[Scope]]属性,对吗?

2017-04-15
2017-04-15
kiwi

@kiwi @冴羽 thx~

2017-04-15
头像
kiwi

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从上层执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。

这里“从上层执行上下文”的表述是否不太严谨?我一开始看到以为是从执行上下文栈中去找,但应该是在[[Scope]]属性里找吧?

2017-04-15
冴羽(作者)

@kiwi 确实是在[[scope]]属性中查找,只是讲变量对象的时候,还没有讲到函数的scope内部属性,这时候的上层其实是想表达语法层面上的上层,让你差点误解了,很抱歉,让我想想怎样表达更好

2017-04-15
kiwi

@kiwi @冴羽 对 我也觉得会引起一个循环定义的问题...总之看了你的这一系列觉得挺有收获,终于捋顺了许多,谢谢~

2017-04-15
冴羽(作者)

@kiwi 感谢指正哈,这个系列才写了一半,欢迎关注和接着指正o((^▽^))o

2017-04-15
头像
Linhh

大大你好,我想问一下,查找变量是根据作用域链的,那么作用域的用处在哪里,它和 VO/AO 有什么关系吗?

2019-01-03
头像
brook

楼主看下我这个说法有问题吗。作用域和上下文是两回事,作用域是静态的,上下文是是函数调用过程中确定的,作用域链的查找是沿着作用域去查找,而不是沿着上下文查找。

2020-06-11