Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript深入之作用域链 #6

Open
mqyqingfeng opened this issue Apr 24, 2017 · 153 comments
Open

JavaScript深入之作用域链 #6

mqyqingfeng opened this issue Apr 24, 2017 · 153 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Apr 24, 2017

前言

《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 mqyqingfeng changed the title JavaScript深入之从ECMAScript规范解读this JavaScript深入之作用域链 Apr 24, 2017
@menglingfei
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@suoz
Copy link

suoz commented Jun 8, 2017

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

@yh284914425
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@yh284914425
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@yh284914425
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@sandGuard
Copy link

听君一席话 胜读十本书

@mqyqingfeng
Copy link
Owner Author

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

@xx19941215
Copy link

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

@xx19941215
Copy link

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

@mqyqingfeng
Copy link
Owner Author

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

@mqyqingfeng
Copy link
Owner Author

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

@mqyqingfeng
Copy link
Owner Author

@xx19941215 想听听你的理解哈~

@deot
Copy link

deot commented Jul 25, 2017

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

@xx19941215
Copy link

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

@LongYue9608
Copy link

@suoz知乎见过你

@keyiran
Copy link

keyiran commented Aug 20, 2017

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

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

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

@yh284914425
Copy link

@keyiran 恩恩 谢谢 我也懂了

@coreOldMan
Copy link

当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,这里的父变量对象就是指活动对象吗,如果父活动对象的属性值发生改变,那么 [[scope]] 属性的值是否会改变?

function foo(a) { var y = 20 function bar() { console.log(y) } y++ } foo(1);
bar被创建后```
bar.[[scope]]={
foo.AO:{
arguments: {
0: 1,
length: 1
},
a: 1,
y: undefined,
bar: reference to function bar(){},
},
globalContext.VO
}
执行到y++后
bar.[[scope]].foo.AO.y 也会从undefined变为21,函数的 [[scope]] 属性是所有父函数的活动对象的引用组成的数组,会随着父函数的活动对象的改变而改变,所以这个说法是错的 “ [[scope]]在函数创建时被存储--静态(不变的),永远永远,直至函数销毁。即:函数可以永不调用,但[[scope]]属性已经写入,并存储在函数对象中。 ”
请问我这么理解对吗

@shunjizhan
Copy link

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

应该理解成

checkscopeContext = {
    ...
    Scope: [AO, ...checkscope.[[scope]]]
}

把?

@zk0816
Copy link

zk0816 commented Sep 22, 2021

var a = 10
function foo(){
console.log(a)
}

function sum() {
var a = 20
foo()
}

sum()

这个作用域链 如何执行的了

@ChengLin-Zhou
Copy link

捋一捋中第4步中 这会是不是还是VO 当进行到函数执行修改值时候才变成AO?

@xsfxtsxxr
Copy link

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

这时候应该不叫AO吧,应该是VO,当函数执行的时候才是AO。

@huangjinyes
Copy link

image

@huangjinyes
Copy link

各位大佬我想问下为什么 [[Scopes]] l里面的Closure (outerFunction) {hidden: 4}。会为4 的按照我的 理解 inc 函数会 查找当前活动对象 在查找outerFunction 变脸对象在查找 Global 变量对象 自身活动对象没 有hidden 变量然后查找outerFunction 但是 打印的话他的值是4 为什么 myClosure.inc() 的返回结果是正确的呢。对不上了呀

@huangjinyes
Copy link

var myClosure = (function outerFunction() {

var hidden = 1;

return {
    inc: function innerFunction() {
        return hidden++;
    },
    hidden
};

}());
myClosure.inc(); // 返回 1
console.dir(myClosure.inc)
myClosure.inc(); // 返回 2
myClosure.inc(); // 返回 3
console.dir(myClosure.inc)

@windf1sh
Copy link

AO:activation object,活动对象;VO:variable object,变量对象。

@yyqxjwxy
Copy link

@keyiran 我想问下作者说的啥叫AO,啥叫VO,对这些抽象概念一脸懵逼,活动对象是干嘛的,变量对象又是干嘛的

@mqyqingfeng
Copy link
Owner Author

@yyqxjwxy 这些概念可以看本篇的上一篇文章 《JavaScript深入之变量对象》, 属于 JavaScript ES5 规范里的概念

@chesscai
Copy link

你好,请教一下~
1.函数创建时确认了父级词法作用域链scope;
2.函数执行时创建了变量对象AO,组成作用域链[AO,scope];

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    function inner(){
        return scope2;
    }
    inner();
    return scope2;
}
checkscope();

开始执行时,
inner作用域链[innerAO,innerScope];
checkscope作用域链[checkscopeAO,checkscopeScope];
inner查找scope2时,innerAO找不到,到innerScope链显然找到了!
按照描述1,innerScope是在创建时确定,此时并没有checkscopeAO,即没有scope2;

请问innerScope具体是什么?按照理解,scope2查找到checkscope这层时,实际上是到checkscopeAO找

@ihepta
Copy link

ihepta commented Mar 4, 2022

各位大佬我想问下为什么 [[Scopes]] l里面的Closure (outerFunction) {hidden: 4}。会为4 的按照我的 理解 inc 函数会 查找当前活动对象 在查找outerFunction 变脸对象在查找 Global 变量对象 自身活动对象没 有hidden 变量然后查找outerFunction 但是 打印的话他的值是4 为什么 myClosure.inc() 的返回结果是正确的呢。对不上了呀

因为你 return 3 之后执行了 hidden++

@zengmanying
Copy link

zengmanying commented Mar 4, 2022 via email

@ihepta
Copy link

ihepta commented Mar 4, 2022

@mqyqingfeng 捋一捋里面的第一步是不是应该是 globalContext.AO?
checkscope.[[scope]] = [
globalContext.VO // 这里是不是应该是 AO ?
];

@jayguojianhai
Copy link

先return 后执行++, 改为return ++hidden就对上了

@zengmanying
Copy link

zengmanying commented Apr 2, 2022 via email

@jayguojianhai
Copy link

jayguojianhai commented Apr 2, 2022 via email

@adjfks
Copy link

adjfks commented May 9, 2022

在《javascript忍者秘籍》里说到的词法环境[[Environment]]与这里的[[Scope]]和作用域链有什么关系?感觉书里讲的[[Environment]]很像作用域链

@zengmanying
Copy link

zengmanying commented May 9, 2022 via email

@caozhongran
Copy link

大佬 可以举例说明下 什么情况下会遇到函数预解析时的:《 第二次复制函数[[scope]]属性创建作用域链》的逻辑吗 这块还是没有理解透彻

@zengmanying
Copy link

zengmanying commented Aug 24, 2022 via email

@returnMaize
Copy link

函数执行上下文中的对象是不是就等于这个函数的作用域
也就是说,每个可执行代码块的执行上下文 = 代码块作用域 + 作用域链 + this

@zengmanying
Copy link

zengmanying commented Jun 20, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests