Skip to content

JavaScript深入之变量对象 #5

Open
@mqyqingfeng

Description

@mqyqingfeng
Owner

前言

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

Activity

izhangzw

izhangzw commented on May 10, 2017

@izhangzw

Arguments对象是什么 - -。

mqyqingfeng

mqyqingfeng commented on May 10, 2017

@mqyqingfeng
OwnerAuthor

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

izhangzw

izhangzw commented on May 12, 2017

@izhangzw

VO 和 AO 到底是什么关系。

jawil

jawil commented on May 12, 2017

@jawil

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

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

mqyqingfeng

mqyqingfeng commented on May 12, 2017

@mqyqingfeng
OwnerAuthor

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

alexzhao8326

alexzhao8326 commented on May 23, 2017

@alexzhao8326

@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

jawil commented on May 23, 2017

@jawil

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

  1. 创建阶段

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

  1. 代码执行阶段

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

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

@alexzhao8326

alexzhao8326

alexzhao8326 commented on May 23, 2017

@alexzhao8326

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

mqyqingfeng

mqyqingfeng commented on May 23, 2017

@mqyqingfeng
OwnerAuthor

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

mqyqingfeng

mqyqingfeng commented on May 26, 2017

@mqyqingfeng
OwnerAuthor

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

default

282 remaining items

sanhaoys

sanhaoys commented on Mar 22, 2023

@sanhaoys

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

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

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

参照

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

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

freewalker8

freewalker8 commented on Mar 22, 2023

@freewalker8
1693146833

1693146833 commented on Nov 26, 2023

@1693146833

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

freewalker8

freewalker8 commented on Nov 26, 2023

@freewalker8
thiskiller

thiskiller commented on Feb 21, 2024

@thiskiller

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

freewalker8

freewalker8 commented on Feb 21, 2024

@freewalker8
thiskiller

thiskiller commented on Feb 21, 2024

@thiskiller

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

}
}

foo("name");

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

xingqq

xingqq commented on Feb 11, 2025

@xingqq

评论区学习看到这样一句话:在上面的规则中我们看出,function声明会比var声明优先级更高一点。
有一个疑问,变量对象中的变量声明,指的是var声明的变量吗?是否包括let和const

freewalker8

freewalker8 commented on Feb 11, 2025

@freewalker8
JunlinZhu-Tommy

JunlinZhu-Tommy commented on May 13, 2025

@JunlinZhu-Tommy
function foo() {
    console.log(a);
    a = 1;
}

foo(); // ???

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

这个感觉不太对,为什么不是第一段报错后就中断了呢?

freewalker8

freewalker8 commented on May 13, 2025

@freewalker8
freewalker8

freewalker8 commented on Jun 18, 2025

@freewalker8
loveheavenlina

loveheavenlina commented on Jun 18, 2025

@loveheavenlina
rainbowyy

rainbowyy commented on Jun 18, 2025

@rainbowyy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @wangweiwei@ckclark@think2011@freeser@wweggplant

        Issue actions

          JavaScript深入之变量对象 · Issue #5 · mqyqingfeng/Blog