mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript深入之作用域链

mqyqingfeng opened this issue · comments

前言

《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,对作者也是一种鼓励。

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

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

commented

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

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

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

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

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

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

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

听君一席话 胜读十本书

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

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

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

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

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

@xx19941215 想听听你的理解哈~

commented

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

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

@suoz知乎见过你

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

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

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

@keyiran 恩恩 谢谢 我也懂了

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

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

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

那上面这个怎么说呢

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

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

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

commented

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

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

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

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

@wanghaomayu 可以的,甚至你可以修改、转换或以此为基础进行创作 ,只要保持署名和非商业使用即可

您好,捋一捋里面的第4点,是不是应该把上一步添加倒执行上下文中的 Scope: checkscope.[[scope]], 写出来呢?这部分已经进入到执行checkscopeContext 了。

@Ak-lee 感谢指出~ 已经修改了~

这里有个疑问,VO 到底是在什么时候创建的,既然函数声明的时候[[scope]]内存储了所有父级VO,那是不是说明 VO 在函数声明的时候已经创建了?

大佬,有个问题。当一个函数执行完毕之后,函数被销毁AO被销毁 ,那么它就完全销毁了吗? 它的GO会不会销毁。

commented

@joy-yu 作用域规则是在代码编译阶段就决定的。个人认为,函数在定义的时候[scope]存储了父级的VO,但这个VO应该是个空的引用,当代码进入执行阶段的时候,父级的执行上下文才被创建,此时VO才有内容

@joy-yu 抱歉啦,这个问题之前没有看到,所以没能回复~ @leon-41 感谢回答,我赞同你的看法~

@ingertimeout 这个 GO 是指? 并没有完全销毁,否则的话,就不会产生闭包的效果了,可以接着往下看闭包那篇文章~

commented

作者大大你好,我有一个疑问。就是这里函数的生命周期。函数声明是在其所在当前执行上下文的准备阶段,函数执行应该是在执行上下文的执行阶段,函数赋值给一个变量是在执行上下文的执行阶段。那么函数创建是在执行上下文的准备阶段还是执行阶段呢?

@lovedd 在执行上下文的准备阶段

作用域链的本质是变量对象的指针列表。

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

输出123我可以理解,因为变量aaa不能提升,所以要顺着scope chain到b()的父对象上去找aaa;
但为什么第二处输出的是234呢?为何不是123?function a()内的AO包括了aaa = 123啊

@wlfjqcj 代码执行到aaa = 234;行时,函数b通过作用域链找到了函数a的AO里的aaa,闭包中引用到的aaa被重新赋值,执行到b();之后的一行时AO内的aaa值已经被覆盖为234了,所以输出234~

@Kylooe 感谢回答哈~ (*≧∪≦)

根据上文函数创建一部分的例子,函数foo()bar()[[scope]]属性应分别为

foo.[[scope]] = [
  globalContext.VO
];

bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
];

而实际上我在测试时发现foo()bar()函数的 [[scope]]属性均为

[globalContext.VO]

引用红宝书179页的说法:

在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[scope]]属性中

这一说法似乎与我测试的结果相符,而与您的说法矛盾。
不知道我哪里理解有误,望指教。

测试代码:

function foo(){
    function bar(){
        return 111;
    }
    bar();
    return 222;
}
foo();

测试截图:
2018-03-20_13-15-58

@lxwSiven 测试代码中的函数bar并没有尚未定义变量,导致[[scope]]属性并没有加上函数foo的作用域链,测试代码改为如下就可以看到函数bar的[[scope]]属性包含全局作用域和函数foo的作用域

function foo(){
    var a = 1;
    function bar(){
        console.log('a', a);
        return 111;
    }
    bar();
    return 222;
}
foo();

@Despair-lj 了解了,感谢回答

@lxwSiven 这个你是怎么用 chrome 打印出来,请教请教~

37637139-06150600-2c41-11e8-8a69-f7b463052b2c

@cobish console.dir(foo) 就可以了~

@Despair-lj 非常感谢回答哈~ ( ̄▽ ̄)~*

commented

@mqyqingfeng console.dir(foo)只能输出foo的,怎么输出foo里面bar呢?

@Rudy24 在 foo 函数里 console.dir(bar) ……

commented

@cobish 除去mqyqingfeng的方法外,还可以在Sources一栏的右侧中的Watch中添加foo和bar,设个断点,运行就可以看到了。

@mqyqingfeng 试了一下 chrome 的打断点,当代码是以下时:

function foo () {
  var a = 1;
  function bar () {
    console.log(a);
    debugger;
  }
  bar();
}

foo();

作用域链跟文章的一样:

a7as6 5 yrs p i7ehu_l

但是,当我 bar 函数不引用 foo 的变量时,Scope 发生了变化:

function foo () {
  var a = 1;
  function bar () {
    debugger;
  }
  bar();
}

foo();

chrome 下断点下是这样:

cx 8 _84 lfxg_b1y m 7u

bar 没有将 foo.AO 加入作用域链??

楼主是最好的中文解释,这是我看到最好的英文解释。

The scope chain property of each execution context is simply a collection of the current context's [VO] + all parent’s lexical [VO]s.
Scope = VO + All Parent VOs
Eg: scopeChain = [ [VO] + [VO1] + [VO2] + [VO n+1] ];

有一个问题,楼主是从哪里得知作用域的建立是初始化变量对象之后呢?

@cobish
我的观点是,chrome这个面板并没有能看到完整的作用域链,仅仅只能看到当下环境、闭包环境和全局环境,当然还是要看官方的解说,于是我找到了相关的文档

While paused on a line of code, use the Scope pane to view and edit the values of properties and variables in the local, closure, and global scopes.

虽说不能看到当前完整的作用域链,但是 Call Stack还是能看到的,而根据楼主的文章说明,inner的函数的作用域链就会包裹outer函数的执行上下文中的变量对象。
image
当然可以理解一下我上面说到的英文解释,虽然有时候我也说服不了自己为什么是这样

@baixiaoji

chrome 这个没显示完整的作用域链我就不清楚了。

不过 Call Stack 指的是执行上下文栈,并不是指 bar 的作用域链 Scope。

@cobish

chrome 这个没显示完整的作用域链我就不清楚了。

您说的这句话不是很理解,是理解为因为chrome没有展示完整的作用域链就不清楚到底有没有foo的这层作用域吗?
我上面给出的链接只是说明:chrome的这个面板只会展示哪几种作用域(local, closure, and global scopes)

不过 Call Stack 指的是执行上下文栈,并不是指 bar 的作用域链 Scope。

确实展示的执行上下文,可每一个执行上下文中可以抽象为一个拥有变量对象、原型链、this三个属性的对象。首先两次代码执行调用栈是一样的,如果我说调用栈都是一样了,那里面的原型链想必也会是一样的,您一定会反驳说不一定,可能第二种运行中就没有foo的作用域链呀。可这纠结的点变得是为什么这样设计作用域链?这样我也你能解释了,引用目前我看到最直白的作用域链解释:

The scope chain property of each execution context is simply a collection of the current context's [VO] + all parent’s lexical [VO]s.
Scope = VO + All Parent VOs
Eg: scopeChain = [ [VO] + [VO1] + [VO2] + [VO n+1] ];

译:每一个执行上下文的作用域链属性就是简单的收集当前上下文中的VO(变量对象)和其所有父级上下文中的词法VO。

var a = 1; var b = 2; function c(){ console.log(a); debugger; } c();
弱弱的问下,为什么dubugger的时候,控制台只能输出a,访问不了b,不是把父变量对象都压入栈了吗?

@cobish
这完全是 V8引擎做的优化,在创建bar函数的时候,并不是简单的将父函数的作用域链保存为bar的作用域链,而是作了处理,把没有引用的变量或整个VO从作用域链中去除了,楼主有一点在本文中没有提到,变量查找除了会顺着作用域链还会对作用域链中的每一个VO做原型查找,所以这个链条越短,内容越少,性能肯定最好。

function a() {
var aaa = 123;
function b(){
console.log(aaa); // undefined
var aaa = 234;
};
b();
console.dir(aaa);//123
};
a();
当子作用域(b的作用域)同样也含有一个和父作用域(a 的作用域)同名的变量aaa, 它不会去改变父作用域里的变量? 是这样的吗?

@fengandzhy

  1. function b有自己的作用域,内部定义了aaa,所以在它的VO中aaa是undefined
  2. function a的VO中也定义了aaa,值是123;
  3. function b执行时会顺着它的作用域链做变量查找(reference resolution),先找到自己定义的aaa,输出是undefined,因为自己的VO在作用域链的第一个位置,最先被查找。
  4. 自然,运行时修改的也是b自己AO中的aaa,所以不会影响到a中的aaa。

@dreamerhammer
第四句话是本题重点,本题当中有一个地方写错了,不该是console.dir(aaa);,而是console.log(aaa);

我有个疑问,为什么文章中的第一步和第三步要分开,不能合为一步放在创建变量对象的后面吗?

image.png
这两个执行上下文是不是反了?

commented

@dreamerhammer 你好 如果在b()中的aaa没有var 那它只是单纯的修改了a中的aaa嘛? b中是不是并没有这个变量? 期待您的回答

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

@dreamerhammer 你好 如果在b()中的aaa没有var 那它只是单纯的修改了a中的aaa嘛? b中是不是并没有这个变量? 期待您的回答

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

@djknight1 分析下过程就知道了
1、a函数被创建,保存作用域链到内部属性[[scope]]

a.[[scope]] = [
   globalContext.VO
]

2、要执行函数a了,创建函数a的执行上下文,并且将执行上下文压入执行上下文栈

ECStack=[
   aContext,
   globalContext.VO
]

3、a函数并不立刻执行,做准备工作,复制函数a的[[scope]]属性创建作用域链

aContext = {
   Scope: ascope.[[scope]]
}

4、b函数创建

b.[[scope]] = [
   globalConext.VO
]

5、要执行b函数了,创建函数b的执行上下文,并且将执行上下文压入执行上下文栈

ECStack = [
   bContext,
   aContext,
   globalContext
]

6、创建函数a活动对象,随后初始化函数a活动对象(加入形参、函数声明、变量声明)

aContext = {
   AO: {
       arguments: {
           length: 0
       }
       aaa: undefined
   },
   Scope: ascope.[[scope]]
}

7、将函数a活动对象压入ascope作用域链顶端

aContext = {
   AO: {
       arguments: {
           length: 0
       },
       aaa: undefined
   },
   Scope: [AO, [[Scope]]]
}

8、准备工作做完,开始执行函数a,随着函数a的执行,修改AO的属性值

aContext = {
   AO: {
       arguments: {
           length: 0
       }    
       aaa: 123
   },
   Scope: [AO, [Scope]]
}

9、随着函数a的执行,函数b创建活动对象,随后初始化函数b的活动对象(加入形参、函数声明、变量声明)

bContext = {
   AO: {
       arguments: {
           length: 0
       }
       // aaa: undefined // 如果有var的话,会有此处
   },
   Scope: bscope.[[scope]]
}

10、将函数b活动对象压入bscope作用域链顶端

bContext = {
   AO: {
       arguments: {
           length: 0
       }
       // aaa: undefined // 如果有var的话,会有此处
   },
   Scope: [AO, bscope.[[scope]]]
}

11、准备工作完成,开始执行函数b,随着函数b的执行修改AO属性值

aContext = {
   AO: {
       arguments: {
           length: 0
       }    
       // aaa: 234; // 如果有var的话,会有此处
   },
   Scope: [AO, [Scope]] // 没有var的话,会修改[[Scope]]中父作用域链的值,所以第2个console才是234,否则,只能修改本函数上下AO中的值,改变不吝父作用域链的aaa,父作用域连只能是123了。
}

12、函数b执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
   aContext,
   globalContext
]

13、函数a执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
   globalContext
]

@mqyqingfeng 不知道顺序上队不对,还请不吝赐教~

@wangweiwei 我感觉你有4处有点小问题。第一个是步骤2里 globalContext.VO改为 globalContext,第二个是步骤4应该在步骤6后面并且b.[[scope]] = [ aContext.AO , globalConext.VO ],第三个是步骤5应该在步骤8后面,第四个是你的aContext.AO里少了b函数
还有一点是步骤11里aaa应该是增加了globalConext.VO对象的属性aaa。还望指正交流

@mqyqingfeng 大佬你好, 之前我在看原型那一块的时候,说的是如果对象本身没有这个属性方法便会到原型链上找。例如:

function Animal() {}
let animal = new Animal()
animal.toString()

在调用这个toString这个方法时,最终肯定是在Object上的,到底是根据原型链查找,还是根据作用域链查找的

commented

我想问一下,查找变量,到底根据的是作用域链,还是作用域吖?

大神们帮我看一下,我根据示例写的这个作用域链是否正确

// 1. 执行全局代码,并压入栈
ECStack = [
globalContext
]
//全局上下文初始化,
globalContext = {
VO: {
arguments: {
length: 0
},
scope: undefined
},
Scope: globalContext.VO,
this: globalContext.VO
}
// checkscope函数创建
checkscope.[[scope]] = [
globalContext.VO
];

//执行checkscope函数,并压入栈
ECStack = [
checkscopeContext,
globalContext
]

//checkscope函数上下文初始化
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, globalContext.VO],
this: undefined
}

commented

大神,请教一个问题,您开篇提到的:

对于每个执行上下文,都有三个重要属性:
变量对象(Variable object,VO)
作用域链(Scope chain)
this

但是在
3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
Scope: checkscope.[[scope]],
}

这里用的是 Scope。所以说 执行上下文中的 Scope 就是作用域链吗?

function f() { console.log('I am outside!'); }
(function () {
f();
if (true) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
}());

执行这个报错,很奇怪,根据博主的作用域链的理论,应该执行出来I am inside才对啊。

@fengandzhy 根据博主之前所说,在块中的函数声明其实相当于函数表达式,所以在匿名函数中经过预处理之后,f的值还未定义,即为undefined。此时调用则报错TypeError: f is not a function。你可以在调用f()之前输出f看看。

讲的可以说很棒了

在创建函数的时候,根据词法作用域,会初始化函数的[[scope]],存放函数父级的执行上下文,在执行函数的时候,会创建函数的执行上下文,依次初始化函数的arguments对象,形参,函数内部的变量和函数,再加上之前初始化的[[scope]]构成了完整的函数作用域链。

这样理解是不是比较全面了?

So perfect tutor : )

Maybe you should publish a book, thus, more people will benefits from it. I cannot appriciate it more.

大神,你好!执行上下文栈引入你先前不是说是使用push方法
image

@mqyqingfeng 大佬你好, 之前我在看原型那一块的时候,说的是如果对象本身没有这个属性方法便会到原型链上找。例如:

function Animal() {}
let animal = new Animal()
animal.toString()

在调用这个toString这个方法时,最终肯定是在Object上的,到底是根据原型链查找,还是根据作用域链查找的

作用域链的作用主要用于查找标识符,当作用域需要查询变量的时候会沿着作用域链依次查找,如果找到标识符就会停止搜索,否则将会沿着作用域链依次向后查找,直到作用域链的结尾。而原型链是用于查找引用类型的属性,查找属性会沿着原型链依次进行,如果找到该属性会停止搜索并做相应的操作,否则将会沿着原型链依次查找直到结尾。https://segmentfault.com/a/1190000009965278 拾人牙慧,希望对你有帮助

@mqyqingfeng 大佬你好 5.第三步 应该是这样吧 您看看
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO,checkscope. [[Scope]]]
}

大神,例子的第二步是不是写错了,应该是

ECStack = [
    globalContext,
    checkscopeContext
];

这文章看似都那么小,信息量还挺大的。谢谢作者

作用域链是只有进入函数上下文才存在吗?如果函数激活后,作用域链数据有了,这时候进入一个子函数作用域,但没有从执行上下文栈剔除,这个时候作用域链还存在吗

作用域链是只有进入函数上下文才存在吗?如果函数激活后,作用域链数据有了,这时候进入一个子函数作用域,但没有从执行上下文栈剔除,这个时候作用域链还存在吗

’由多个执行上下文的变量对象构成的链表就叫做作用域链‘所以没有进入函数上下文是不存在作用域链的。第二个问题,进入子作用域的时候,执行上下文栈没剔除,作用域链是存在的,因为子作用域的作用域链需要用到刚刚的作用域链拼接构成。

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

这个地方你可以理解成,函数内部存在一个属性,保存着对于外部作用域的引用,这样就会直接通过这个属性直接拿到外部作用域的一些变量

看了三遍才真的理解,还倒回去看了前面的文章。开心😄

image
为什么说是用arguments创建的活动对象? Object.create(arguments)这样操作的吗?我以为只是创建一个对象,然后初始化arguments属性呢? 怎么证明是arguments创建的活动对象?

commented

大神您好,想和您请教个问题,根据“箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this”(箭头函数 | MDN),而在您文章中看到,this是保存在执行上下文中的,箭头函数的this如何继承?

commented

大神您好,想和您请教个问题,根据“箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this”(箭头函数 | MDN),而在您文章中看到,this是保存在执行上下文中的,箭头函数的this如何继承?
理解成箭头函数的this会跟随他的父级作用域的this就对了,比如箭头函数定义在一个函数A里面,那么箭头函数里面访问this就和这个函数A的this相同,放在全局下面,this的指向就和全局的相同(window或者undefined)

commented

大神您好,想和您请教个问题,根据“箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this”(箭头函数 | MDN),而在您文章中看到,this是保存在执行上下文中的,箭头函数的this如何继承?

首先要了解一点的是这篇文章所讨论的内容是在 ES3 规范下的, 在 ES6 规范中很多内容都发生了改变了, 因为原有的规范已经无法描述这些新的内容了.

函数(不是胖箭头)在执行的时候会确定当前 this 的指向, 也就是在执行上下文初始化的时候, 这在 ES3 中是没有例外的, 但是胖箭头函数, 是没有的:

An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment.

简单来讲新规范中规定了 this 需要向它绑定的词法作用域中获取.

大神,例子的第二步是不是写错了,应该是

ECStack = [
    globalContext,
    checkscopeContext
];

我也感觉这块是不是写错了

let 的语法会引入 块级作用域

if (true) {
    let a = 1
    function b () {
        let c = 2
        function d () {
            let e = 3
            console.log(a, c, e)
        }
        console.dir(d)
    }
}

这段代码输出的内容 d.[[Scopes]] 如下,块级作用域确实存在d函数的作用域链中

[[Scopes]]: Scopes[3]
0: Closure (b) {c: 2}
1: Block {a: 1}
2: Global {...}

块级作用域应该是不创建执行上下文的,但是块级作用域应该如何理解呢,求大佬解释下 万分感谢

块级作用域有创建执行上下文的能力,你把 let a = 1换成var a=1 就只会有两个执行上下文了 果你的if等{}代码块(不包含函数)中出现let、const就会创建块级作用域进行let和const的词法声明。 if(true){    let a=1 } 还有一点单语句 if(true)   var a=1 if(true)   let a=1 let这里会报错,就是应为单语句没有创造作用域的能力,导致无法词法声明

------------------ 原始邮件 ------------------ 发件人: "Xu Kang"<notifications@github.com>; 发送时间: 2020年3月28日(星期六) 中午11:29 收件人: "mqyqingfeng/Blog"<Blog@noreply.github.com>; 抄送: "1269803709"<1269803709@qq.com>; "Comment"<comment@noreply.github.com>; 主题: Re: [mqyqingfeng/Blog] JavaScript深入之作用域链 (#6) let 的语法会引入 块级作用域 if (true) { let a = 1 function b () { let c = 2 function d () { let e = 3 console.log(a, c, e) } console.dir(d) } } 这段代码输出的内容 d.[[Scopes]] 如下,块级作用域确实存在d函数的作用域链中 [[Scopes]]: Scopes[3] 0: Closure (b) {c: 2} 1: Block {a: 1} 2: Global {...} 块级作用域应该是不创建执行上下文的,但是块级作用域应该如何理解呢,求大佬解释下 万分感谢 — You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.

有具体的文档对块级作用域会创建上下文的介绍嘛?这边用 var 在 if 语句中声明变量,d.[[Scopes]]中只记录两个作用域,是因为,会把 a 声明在全局变量中,就不存在块级作用域了,并不能说明块级作用域会创建上下文呀。
第二点说的那个单语句,简单点理解就是,ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域,而 let 只能出现在当前作用域的顶层,所以会报错。严格模式下,函数声明也会报错

'use strict'
if (true) function fn() {}