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深入之变量对象 #5

Open
mqyqingfeng opened this issue Apr 23, 2017 · 256 comments
Open

JavaScript深入之变量对象 #5

mqyqingfeng opened this issue Apr 23, 2017 · 256 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Apr 23, 2017

前言

在上篇《JavaScript深入之执行上下文栈》中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性:

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

今天重点讲讲创建变量对象的过程。

变量对象

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。

全局上下文

我们先了解一个概念,叫全局对象。在 W3School 中也有介绍:

全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。

在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。

例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

如果看的不是很懂的话,容我再来介绍下全局对象:

1.可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。

console.log(this);

2.全局对象是由 Object 构造函数实例化的一个对象。

console.log(this instanceof Object);

3.预定义了一堆,嗯,一大堆函数和属性。

// 都能生效
console.log(Math.random());
console.log(this.Math.random());

4.作为全局变量的宿主。

var a = 1;
console.log(this.a);

5.客户端 JavaScript 中,全局对象有 window 属性指向自身。

var a = 1;
console.log(window.a);

this.window.b = 2;
console.log(this.b);

花了一个大篇幅介绍全局对象,其实就想说:

全局上下文中的变量对象就是全局对象呐!

函数上下文

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

执行过程

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  1. 进入执行上下文
  2. 代码执行

进入执行上下文

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)

    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明

    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明

    • 由名称和对应值(undefined)组成一个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

举个例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);

在进入执行上下文后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  1. 全局上下文的变量对象初始化是全局对象

  2. 函数上下文的变量对象初始化只包括 Arguments 对象

  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  4. 在代码执行阶段,会再次修改变量对象的属性值

思考题

最后让我们看几个例子:

1.第一题

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

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???

第一段会报错:Uncaught ReferenceError: a is not defined

第二段会打印:1

这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。

第一段执行 console 的时候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}

没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。

当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。

2.第二题

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

会打印函数,而不是 undefined 。

这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

下一篇文章

《JavaScript深入之作用域链》

本文相关链接

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

深入系列

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

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

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

@izhangzw
Copy link

Arguments对象是什么 - -。

@mqyqingfeng
Copy link
Owner Author

引用《JavaScript权威指南》回答你哈:调用函数时,会为其创建一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。所有作为参数传入的值都会成为Arguments对象的数组元素。

@izhangzw
Copy link

VO 和 AO 到底是什么关系。

@jawil
Copy link

jawil commented May 12, 2017

未进入执行阶段之前,变量对象(VO)中的属性都不能访问!但是进入执行阶段之后,变量对象(VO)转变为了活动对象(AO),里面的属性都能被访问了,然后开始进行执行阶段的操作。

它们其实都是同一个对象,只是处于执行上下文的不同生命周期。@jDragonV

@mqyqingfeng
Copy link
Owner Author

@jawil 非常感谢回答,一语中的。

@alexzhao8326
Copy link

alexzhao8326 commented May 23, 2017

@mqyqingfeng 楼主,有幸拜读你的深入系列,收获颇多,但也存在一些疑问。比如变量对象留给我们的思考题的第二题,按照你的写法:

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1; // 打印函数

但个人觉得这句“这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。”解释得有点欠完整,如果我把代码改写成下面这样:

var foo = 1;
console.log(foo);
function foo(){
    console.log("foo");
};

这次打印结果就是“1”;

所以我觉得这么解释比较好:

进入执行上下文时,首先会处理函数声明,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

进入代码执行阶段,先执行console.log(foo),此时foo是函数的应用,再执行var foo = 1;将foo赋值为1,而在我改写的例子里中,先执行var foo = 1;再执行console.log(foo),所以打印1。我觉得加上代码执行阶段会更清晰,哈哈哈

@jawil
Copy link

jawil commented May 23, 2017

一个执行上下文的生命周期可以分为两个阶段。

  1. 创建阶段

在这个阶段中,执行上下文会分别创建变量对象,建立作用域链,以及确定this的指向。

  1. 代码执行阶段

创建完成之后,就会开始执行代码,这个时候,会完成变量赋值,函数引用,以及执行其他代码。

都没有错,博主讲的主要是针对变量对象,而变量对象的创建是在EC(执行上下文)的创建阶段,所以侧重点主要是EC的生命周期的第一个阶段,我觉得再执行var foo = 1这句话有点不妥,应该是给foo赋值,应该是执行foo=1这个操作,因为在EC创建阶段var已经被扫描了一遍。

@alexzhao8326

@alexzhao8326
Copy link

是的,显然你的说法更严谨,也符合分析的过程! 学习了@jawil

@mqyqingfeng
Copy link
Owner Author

@jawil 哈哈,十分感谢回答~~~ @alexzhao8326 这道题应该是因为没有分成两个阶段来讲,所以让你觉得分析得不是很完整吧。我在写的时候,觉得毕竟是思考题,讲清楚问题的关键点即可,所以也没有给出完整的分析。如果你看完前面的内容,相信你一定能明白结果为什么会是这样,对于你修改后的例子,相信你也能解释的了。当然了,学习时严谨的态度还是要有的,感谢指出,o( ̄▽ ̄)d

@mqyqingfeng
Copy link
Owner Author

@wedaren 进入执行上下文时,初始化的规则如下,从上到下就是一种顺序:

default

@zuoyi615
Copy link

zuoyi615 commented May 26, 2017

var foo = 1;
console.log(foo);
function foo(){
  console.log("foo");
};
这次打印结果就是“1”;

分解
var foo; // 如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
foo = 1;// 代码执行。PS: 如果没有这行,打印结果是 function foo(){console.log('foo')};
console.log(foo); // 1
function foo(){
  console.log("foo");
};

执行上下文的时候:

VO = {
    foo: reference to function foo(){}
}

然后再执行了 foo = 1 的操作,修改变量对象的 foo 属性值

AO = {
    foo:  1
}

执行代码 console.log(foo) 的结果: 1

@mqyqingfeng
Copy link
Owner Author

@zuoyi615 感谢写下自己的分析过程,如果这段代码是在全局环境下执行的,变量对象应该用 VO 表示,此时也没有 arguments 属性

@mqyqingfeng
Copy link
Owner Author

@zuoyi615 o( ̄▽ ̄)d

@oakland
Copy link

oakland commented May 29, 2017

@jawil ,你说的有一点误差,AO 实际上是包含了 VO 的。因为除了 VO 之外,AO 还包含函数的 parameters,以及 arguments 这个特殊对象。也就是说 AO 的确是在进入到执行阶段的时候被激活,但是激活的除了 VO 之外,还包括函数执行时传入的参数和 arguments 这个特殊对象。
AO = VO + function parameters + arguments
@jDragonV

@mqyqingfeng
Copy link
Owner Author

@oakland 非常感谢补充~~~ 这一点我也没有注意到~ o( ̄▽ ̄)d

@ckclark
Copy link

ckclark commented May 30, 2017

是w3school 不是W3C school

@mqyqingfeng
Copy link
Owner Author

@ckclark 哎呀呀,我犯了一个严重的错误,非常感谢指出~o( ̄▽ ̄)d

@MrGoodBye
Copy link

思考题第二题:

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

解:
JavaScript发现了一段可执行代码(executable code),准备创建对应的执行上下文(execution context):

在此之前

因为JavaScript的函数提升特性,将代码等量变换为:(1)

function foo(){// 函数提升
    console.log("foo");
}
console.log(foo);
var foo = 1;

又因为JavaScript的变量提升特性,将代码等量变换为:(2)

function foo(){// 函数提升
    console.log("foo");
}
var foo;// 变量提升
console.log(foo);
foo = 1;

开始创建对应的执行上下文(execution context):(3)

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

其中,此处探讨的VO只是被初始化(4)

当javaScript扫描到console.log(foo)时,执行代码之前,先进入执行上下文(execution context),(5)

因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。

VO = {
    foo: reference to function foo(){},
    ~foo:undefined// 此处疑问: 此处变量声明的foo是否保存在VO中;以何种形式保存
}

执行代码console.log(foo),查找到了VO中的foo,输出结果.(6)
接着执行foo = 1,执行之后,VO为:(7)

VO = {
    foo: 1
}

解答完毕.

第4处跟第5处都不很确定,其他地方也可能有理解不到位.请大家指出.

@MrGoodBye
Copy link

MrGoodBye commented May 31, 2017

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;
console.log(foo);

var foo = 1;

function foo(){
    console.log("foo");
}

另外,以上两处代码得出的结论一样,说明:

同一作用域下,函数提升比变量提升得更靠前.

大家知道的微微一笑就好了:)

@xumengzi
Copy link

根据你们的讨论,关于这一段代码的实现,

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

执行结果是函数和1,我可以这样理解么?

foo() 			  //函数提升
var foo			  //和函数重名了,被忽略
console.log(foo);	  //打印函数
foo = 1;		  //全局变量foo
console.log(foo);	  //打印1,事实上函数foo已经不存在了,变成了1

望不吝赐教!

@unknownzjc
Copy link

unknownzjc commented Jan 3, 2022

变量对象跟词法环境可不可以理解为是同一种东西?

1 similar comment
@yueyueyaofaguang
Copy link

变量对象跟词法环境可不可以理解为是同一种东西?

@eric-leo
Copy link

变量对象跟词法环境可不可以理解为是同一种东西?

这篇文章是es3标准的解释,现在es6标准来说,执行上下文包括:词法环境、变量环境、this。
而变量对象和词法环境不能简单对等,具体es6的解释可看这篇文章

@zreren
Copy link

zreren commented Feb 6, 2022

非常感谢

@freewalker8
Copy link

freewalker8 commented Feb 6, 2022 via email

@charmingYouYou
Copy link

变量对象跟词法环境可不可以理解为是同一种东西?

这篇文章是es3标准的解释,现在es6标准来说,执行上下文包括:词法环境、变量环境、this。 而变量对象和词法环境不能简单对等,具体es6的解释可看这篇文章

中文译文可看: 【译】理解 Javascript 执行上下文和执行栈

@wangxiaotian
Copy link

这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。

所以未通过 var 关键字声明的变量去了哪里?

引用权威指南里的:如果没有使用严格模式并给一个未声明的变量赋值的话,JS会自动创建一个全局变量。
所以a去了全局变量对象里了(在执行到赋值代码时)

@nmfmn
Copy link

nmfmn commented Mar 7, 2022

内容很好, 评价里的内容也很好! 受益匪浅!

@Rebornjiang
Copy link

image
大佬们帮忙看看AO与VO关系,哪种理解应该是正确的?

@freewalker8
Copy link

freewalker8 commented May 7, 2022 via email

@kouteisang
Copy link

感谢博主,我们是否可以理解AO是先进行变量以及函数的提升,然后再执行代码。

@Vsnoy
Copy link

Vsnoy commented Jul 21, 2022

我认为讲到“全局上下文”部分,最后一句结论不完全正确:变量对象就是全局对象呐!

我们知道,全局下变量声明(var)和函数声明将会挂载到顶层对象(window 或 global)中。但是使用 let 或 const 所声明的变量并不会挂载到顶层对象下。但是当“查找”变量并最终到达全局上下文时,仍会正确找到对应变量。这里的查找应该是在 VO 对象上查找。但如果是这样,上面的结论就不对了。所以纠结是怎样的呢?

麻烦大佬解惑,谢谢!

是个好问题,我也觉得这里有点不妥,目前也没找到合理的解释。
有答案了艾特我一下,我也很想知道。

@allworldg
Copy link

@Vsnoy let不挂载到global,但是会挂载跟global同样层级的Script。仍然可以通过AO找到。

let A1 = 'Script'
let A2 = 'Global'
debugger

image

@Beixiaomi
Copy link

思考题2:
关于函数提升和变量提升的优先级,其实完整的表述应该是:函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖。
可以参考这篇文章 https://blog.csdn.net/ksjdbdh/article/details/120815118 楼主说的也没错,只是一部分。

@SUNDONG123
Copy link

SUNDONG123 commented Jan 12, 2023

AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
在进入到执行阶段前,这个地方应该是VO吧

@Hongmiaomiao
Copy link

这里补充《你不知道的JS上卷》中的一段话,感觉可以作为更好地理解执行上下文的补充。

关于JS引擎和编译器如何理解下面这行变量赋值。

 var a = 2 

下面我们将var a = 2;分解

编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。但是当编译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内存,将其命名为a,然后将值2保存进这个变量。”然而,这并不完全正确。事实上编译器会进行如下处理。

1.遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。

2.接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(查看1.3节)。如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

从上面这段话来看,个人认为可以把执行过程这样解释:

  1. 进入执行上下文 (编译器做词法、语法分析、代码生成)
  2. 代码执行 (引擎做编译、执行(变量的赋值就是这个执行的过程))

@freewalker8
Copy link

freewalker8 commented Feb 28, 2023 via email

@Hongmiaomiao
Copy link

Hongmiaomiao commented Feb 28, 2023

在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

关于这段说法和前文的代码有个疑问请教

文中片段「在进入执行上下文的时候」
AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to `function` c(){}, d: undefined }
a:1 ,应该算是对实参的赋值?这个为什么是在进入执行上下文时就赋值了?

@sanhaoys
Copy link

sanhaoys commented Mar 22, 2023

在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

关于这段说法和前文的代码有个疑问请教

文中片段「在进入执行上下文的时候」 AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to `function` c(){}, d: undefined } a:1 ,应该算是对实参的赋值?这个为什么是在进入执行上下文时就赋值了?

参照

  1. 函数的所有形参(如果是函数执行上下文)
  • 由名称和实际值组成的一个属性被创建
  • 没有实参,赋值为 undefined

函数传入实参时,变量对象的a就已经被赋予实际值了

@freewalker8
Copy link

freewalker8 commented Mar 22, 2023 via email

@1693146833
Copy link

1693146833 commented Nov 26, 2023

简而言之
同名的函数和变量之间只能有一个
函数在创建阶段就已经赋好值了,所有这阶段函数优先,而执行阶段变量才会赋好值,在执行阶段变量优先。

@freewalker8
Copy link

freewalker8 commented Nov 26, 2023 via email

@thiskiller
Copy link

“当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。”
这句话有问题吧?我在外层打印a是会报错的,它是在方法内定义的

@freewalker8
Copy link

freewalker8 commented Feb 21, 2024 via email

@thiskiller
Copy link

function foo(a) {
console.log(a);
function a() {

}
}

foo("name");

有个疑问,在执行阶段,形参被赋值时应该会覆盖解析阶段的值啊,此时打印出来的应该是name,而不应该是Function a啊

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