JavaScript深入之作用域链

发布于 2022-07-14 21:31:19 字数 2199 浏览 242 评论 41

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

今天重点讲讲作用域链。

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。

函数创建

函数的作用域在函数定义的时候就决定了。这是因为函数有一个内部属性 [[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
];

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(41

你怎么这么可爱啊 2022-05-04 13:51:30

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

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

sum()

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

╰沐子 2022-05-04 13:51:30

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

半﹌身腐败 2022-05-04 13:51:30

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

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

遇到 2022-05-04 13:51:30

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

○愚か者の日 2022-05-04 13:51:30

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)

淑女气质i 2022-05-04 13:51:30

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

仙气飘飘 2022-05-04 13:51:30

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

月下伊人醉 2022-05-04 13:51:30

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

甜嗑 2022-05-04 13:51:30

你好,请教一下~
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找

可爱暴击 2022-05-04 13:51:30

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

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

意犹 2022-05-04 13:51:30

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

窝囊感情。 2022-05-04 13:51:30

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

零度° 2022-05-04 13:51:29

@keyiran 恩恩 谢谢 我也懂了

紫瑟鸿黎 2022-05-04 13:51:29

一群天狗,这个写的正常人都看不懂好吧

别低头,皇冠会掉 2022-05-04 13:51:29

这文章是不是有点过时了,AO, VO是es3时代的说法?
现在是lexical environment:词法环境,variable environment:变量环境吧?

苦笑流年记忆 2022-05-04 13:51:29

第一次提问还提到 issue 里了,尴尬。
如果说在 foo 函数进入上下文栈创建变量对象,此时 bar 函数开始创建,bar.[[scope]] 保存了存所有父变量对象到其中。那么如下代码怎么解释?
function foo() {
var a = 10
function bar() {
var b = 20
}
console.dir(bar)
}
foo()
function foo() {
var a = 10
function bar() {
var b = 20
console.dir(arguments.callee)
}
bar()
}
foo()
此时 bar 输出的 [[scopes]] 属性里只有 Global 对象。没有包含 VO(foo)

两个foo()都是调用的下面一个foo()函数,我在里面加了一个console.dir(bar),打印出来的bar函数的[[Scopes]]属性是包含foo函数的AO的,arguments.callee已经不推荐使用了。看文章说会影响闭包,不过具体原理不是很清楚

倥絔 2022-05-04 13:51:29

看了很多原形、构造函数、实例三者解释及关系的文章都没看懂,这篇竟然终于看懂了。 原形就是模具, 构造函数就是生产工厂,通过模具跟工厂就能生产产品(实例)出来啦。

叹倦 2022-05-04 13:51:29

看了很多原形、构造函数、实例三者解释及关系的文章都没看懂,这篇竟然终于看懂了。 原形就是模具, 构造函数就是生产工厂,通过模具跟工厂就能生产产品(实例)出来啦。

我觉得你对原型可能有误解。我认为原型并不是模具,每个实例其实访问的是同一个原型,而并没有按原型作为模具来给每个实例都复制出原型中对应的属性

爱*していゐ 2022-05-04 13:51:29

当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[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]]属性已经写入,并存储在函数对象中。 ”
请问我这么理解对吗

弱骨蛰伏 2022-05-04 13:51:29
checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

应该理解成

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

把?

梦境゜ 2022-05-04 13:51:28

@xx19941215 想听听你的理解哈~

不爱素颜 2022-05-04 13:51:28

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

我是男神闪亮亮i 2022-05-04 13:51:28

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

寒尘 2022-05-04 13:51:28

@suoz知乎见过你

花开半夏魅人心 2022-05-04 13:51:28

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

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

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

-残月青衣踏尘吟 2022-05-04 13:51:26

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

不美如何 2022-05-04 13:51:09

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

笑脸一如从前 2022-05-04 13:48:31

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

傾旎 2022-05-04 13:48:11

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

走过海棠暮 2022-05-04 13:46:48

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

厌倦 2022-05-04 13:43:59

听君一席话 胜读十本书

掀纱窥君容 2022-05-04 13:43:46

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

甜心小果奶 2022-05-04 13:37:01

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

兔姬 2022-05-04 13:23:03

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

无力看清 2022-05-04 11:52:55

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

乖乖哒 2022-05-04 10:57:20

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

¨。骷髅 2022-05-04 02:19:45

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

挽你眉间 2022-05-03 19:39:02

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

作妖 2022-05-02 20:52:12

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

爱情眠于流年 2022-04-29 17:23:01

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

~没有更多了~

关于作者

陪你到最终

暂无简介

文章
评论
24 人气
更多

推荐作者

七七

文章 0 评论 0

囍笑

文章 0 评论 0

盛夏尉蓝

文章 0 评论 0

ゞ花落谁相伴

文章 0 评论 0

Sherlocked

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文