Blog icon indicating copy to clipboard operation
Blog copied to clipboard

JavaScript深入之作用域链

Open mqyqingfeng opened this issue 8 years ago • 156 comments
trafficstars

前言

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

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

《JavaScript深入之变量对象》

深入系列

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

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

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

mqyqingfeng avatar Apr 24 '17 02:04 mqyqingfeng

大神你好,问你一个问题,checkscope函数被创建时,保存到[[scope]]的作用域链和checkscope执行前的准备工作中,复制函数[[scope]]属性创建的作用域链有什么不同么?为什么会有两个作用域链?

menglingfei avatar May 05 '17 08:05 menglingfei

checkscope函数创建的时候,保存的是根据词法所生成的作用域链,checkscope执行的时候,会复制这个作用域链,作为自己作用域链的初始化,然后根据环境生成变量对象,然后将这个变量对象,添加到这个复制的作用域链,这才完整的构建了自己的作用域链。至于为什么会有两个作用域链,是因为在函数创建的时候并不能确定最终的作用域的样子,为什么会采用复制的方式而不是直接修改呢?应该是因为函数会被调用很多次吧。

mqyqingfeng avatar May 05 '17 09:05 mqyqingfeng

@menglingfei 在js中复制有分两种,比如说基本类型的复制,就是直接的赋值,两个变量以后互不影响。而引用类型的复制,是指两个变量同时指向一个对象。我觉得这里应该说的是后者吧。

suoz avatar Jun 08 '17 07:06 suoz

函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,想问一下变量对象是创建上下文的时候才有的吧 function foo() { function bar() { ... } } 要是foo没有创建上下文,那bar怎么保存foo的变量对象啊

yh284914425 avatar Jun 23 '17 01:06 yh284914425

@yh284914425 以你举的例子为例的话,当 foo 函数的执行上下文初始化的时候,才会创建 bar 函数。

mqyqingfeng avatar Jun 23 '17 10:06 mqyqingfeng

@mqyqingfeng 好吧,当函数创建的时候,就会保存所有父变量对象到其中这个过程感觉不是很明白,能说的清楚一些吗,以我举的例子为例的话,当 foo 函数的执行上下文初始化的时候,才会创建 bar 函数,bar函数保存foo的变量对象,那更外层的变量对象呢

yh284914425 avatar Jun 24 '17 02:06 yh284914425

@yh284914425 更外层就是全局对象呐~ 所以bar 的 [[scope]] 属性值就是 [ fooContext.AO, globalContext.VO];

mqyqingfeng avatar Jun 24 '17 03:06 mqyqingfeng

@mqyqingfeng 我知道呀,就是想问一下,[[scope]] 属性值是怎么把globalContext.VO保存进去的,有点转牛角尖,嘿嘿

yh284914425 avatar Jun 24 '17 03:06 yh284914425

@yh284914425 根据词法作用域的规则找出最外层的就是 globalContext ,然后……然后就保存呐……具体是怎么保存进去的,这个应该是实现层面上的吧

mqyqingfeng avatar Jun 24 '17 03:06 mqyqingfeng

听君一席话 胜读十本书

sandGuard avatar Jul 18 '17 08:07 sandGuard

@sandGuard 感谢,这真是对我莫大的肯定~

mqyqingfeng avatar Jul 19 '17 03:07 mqyqingfeng

函数生命周期 你好,请问大神我画的这幅图对吗?

xx19941215 avatar Jul 19 '17 11:07 xx19941215

《JS高程》讲到,每一个函数都有自己的执行环境。好像这里没有讲到额

xx19941215 avatar Jul 20 '17 01:07 xx19941215

@xx19941215 函数都有自己的执行环境,其实就是讲函数执行的时候,会创建函数执行上下文,这个在《JavaScript深入之执行上下文栈》和 《JavaScript深入之变量对象》都有讲到

mqyqingfeng avatar Jul 20 '17 03:07 mqyqingfeng

@xx19941215 关于这张图,有几个疑问的地方?一个是默认存在了一个 window 的引用是指什么意思?一个是先创建的执行环境还是先创建的活动对象?一个是如果有闭包的话,AO是否会被释放?一个是执行环境的作用域链对象的 AO 引用出栈,为什么需要出栈呢?

mqyqingfeng avatar Jul 20 '17 05:07 mqyqingfeng

@xx19941215 想听听你的理解哈~

mqyqingfeng avatar Jul 20 '17 05:07 mqyqingfeng

受益! 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。

deot avatar Jul 25 '17 09:07 deot

@mqyqingfeng 1.函数作用域链在初始化的时候顶端是window。2.先创建活动对象,然后创建执行环境。关于后两个问题,我之前写了一篇文章介绍了我的理解:图解JS闭包 这是我的理解,还请大神看看对不对啊

xx19941215 avatar Jul 26 '17 00:07 xx19941215

@suoz知乎见过你

LongYue9608 avatar Aug 07 '17 13:08 LongYue9608

@yh284914425 我也有过你的疑问,不过看了作者的答复明白了,在源代码中当你定义(书写)一个函数的时候(并未调用),js引擎也能根据你函数书写的位置,函数嵌套的位置,给你生成一个[[scope]],作为该函数的属性存在(这个属性属于函数的)。即使函数不调用,所以说基于词法作用域(静态作用域)。

然后进入函数执行阶段,生成执行上下文,执行上下文你可以宏观的看成一个对象,(包含vo,scope,this),此时,执行上下文里的scope和之前属于函数的那个[[scope]]不是同一个,执行上下文里的scope,是在之前函数的[[scope]]的基础上,又新增一个当前的AO对象构成的。

函数定义时候的[[scope]]和函数执行时候的scope,前者作为函数的属性,后者作为函数执行上下文的属性。

keyiran avatar Aug 20 '17 07:08 keyiran

@keyiran 恩恩 谢谢 我也懂了

yh284914425 avatar Aug 21 '17 03:08 yh284914425

@mqyqingfeng 大神,红宝书的180页有这样的描述,先上代码:

function createComparisonFunction(propertyName){
  return function(object1, object2){
    var value1 = object1[propertyName]
    var value2 = object2[propertyName]
  }

  if(value1 < value2){
    return -1
  }else if(value1 > value2){
    return 1
  }else{
    return 0
  }
}

var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });

在匿名函数从 createComparisonFunction()中被返回后,它的作用域链被初始化为包含 createComparisonFunction()函数的活动对象和全局变量对象。

issue对作用域链的描述:

多个执行上下文的变量对象构成的链表就叫做作用域链

不能理解,“作用域链被初始化为活动对象”,这句话该怎么理解,作用域链被初始为对象?

变成全局变量对象还能理解,因为赋值的参数compare属于全局变量对象。还是说书中的作用域链,还指当前作用域的变量对象?

LiuYashion avatar Sep 29 '17 10:09 LiuYashion

@LiuYashion 首先对我这么晚的回复深表歉意,其次关于这个问题,我觉得可能是翻译的问题,这句话的意意应该是说在执行 createComparisonFunction("name") 的时候,匿名函数被返回,然后初始化该匿名函数的作用域链,该作用域链包含了 createComparisonFunction() 函数的活动对象和全局变量对象。

mqyqingfeng avatar Oct 11 '17 03:10 mqyqingfeng

@mqyqingfeng 楼主,如果说里面的函数scope是复制外层函数的scope的话,那这两个scope里面的变量应该是没有关联才对,

function a(){
  var aaa = 123
  function b(){
    console.log(aaa)
    aaa = 234
  }
  b()
  console.log(aaa)
}
a()

那上面这个怎么说呢

watsonnnnn avatar Oct 24 '17 00:10 watsonnnnn

@nicewahson 复制的是 scope,而不是具体的变量,举个不严谨的例子:假如 scope 为 [checkscopeVO, globalVO],复制就相当于 [checkscopeVO, globalVO].concat(), 是一种浅拷贝,然而 checkscopeVO 和 globalVO 才储存着能访问到的变量名称,所以对能访问到的变量是没有影响的

mqyqingfeng avatar Oct 25 '17 03:10 mqyqingfeng

提一个小小意见,建议作者可以把执行过程结合chrome的调试图来进行scope的初始状态和变化过程

7kr avatar Nov 30 '17 07:11 7kr

@7kr 感谢建议,日后修订的时候会加上~

mqyqingfeng avatar Dec 01 '17 04:12 mqyqingfeng

貌似有一点是关于用Function构建出的函数,它的内部属性[[scope]]貌似只有全局的VO对象

hazxy avatar Dec 01 '17 15:12 hazxy

@hazxy 感谢补充,我从 MDN 找到了这一段:

使用Function构造器生成的函数,并不会在创建它们的上下文中创建闭包;它们一般在全局作用域中被创建。当运行这些函数的时候,它们只能访问自己的本地变量和全局变量,不能访问Function构造器被调用生成的上下文的作用域。这和使用带有函数表达式代码的 eval 不同。

mqyqingfeng avatar Dec 05 '17 02:12 mqyqingfeng

兄弟,我是一名学习前端的在校大学生,感觉深入系列很值得研究学习,能转载在自己的个人博客上么,一定标明出处

wanghaomayu avatar Dec 08 '17 13:12 wanghaomayu