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

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

变量对象

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

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

全局上下文

我们先了解一个概念,叫全局对象。在 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,对作者也是一种鼓励。

Arguments对象是什么 - -。

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

VO 和 AO 到底是什么关系。

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

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

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

@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。我觉得加上代码执行阶段会更清晰,哈哈哈

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

  1. 创建阶段

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

  1. 代码执行阶段

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

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

@alexzhao8326

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

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

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

default

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

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

@zuoyi615 o( ̄▽ ̄)d

commented

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

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

是w3school 不是W3C school

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

思考题第二题:

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处都不很确定,其他地方也可能有理解不到位.请大家指出.

console.log(foo);

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

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

var foo = 1;

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

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

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

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

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

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

望不吝赐教!

@JarvenIV 是的,o( ̄▽ ̄)d

@MrGoodBye 在《JavaScript深入之执行上下文栈》中,我以前写错了一点,现在已经修正了,其实是在函数执行的时候,才创建执行上下文,这个可能将你误导了,我很抱歉。

因为第二个例子的代码写在了全局中,所以函数声明和变量声明都是在全局对象中,在代码执行阶段,执行 console.log 时,会创建 console.log 函数的执行上下文,然后读取全局变量中的 foo ,然后因为覆盖规则的原因,打印函数

给楼主点赞

非常感谢,感觉学会了很多

@LuDongWang 哈哈,你这是为我知错能改的精神点赞吗?

@huanqundong 看完这篇,还有十一篇,未来还有更多篇,希望你能保持学习的热情,不断成长~

foo();
if(true){
  function foo(){
    console.log('1111');
  }
}else{
    function foo(){
      console.log('22222');
  }
}

chrome59 和ie11都报“VM86:1 Uncaught TypeError: foo is not a function”;
如果用本文中的理论,有点问题,
代码在执行中分为,1进入执行上下文,2执行阶段
具体执行(按照本文的结论推理):
1进入执行上下文------AO={foo:reference to functionfoo(){console.log('22222')},}
2.那么执行阶段会 打印 “22222”;(但实际抛异常)

查阅了一下资料:说不同版本中,结果会不同

@youzaiyouzai666 我觉得这个更像是函数声明的问题。当函数声明出现在代码块中,javascript 引擎不会将其作为函数声明处理,而是处理成函数表达式。

所以,这段代码相当于:

var foo;
foo();
if(true){
  foo = function(){
    console.log('1111');
  }
}else{
    foo = function(){
      console.log('22222');
  }
}
commented

受益;
思考题中理解变量提升有帮助:

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

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

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

代码执行两个阶段:1.进入执行上下文,2. 执行阶段

在思考题1的第一段中,我在金丝雀中执行的结果与分析不同。
返回结果是:1,而非报错
function foo() {
console.log(a);
a = 1;
}

foo(); // ???
1
undefined

@alvis888 没有在金丝雀测验过哈~ 应该是解析不同吧

var foo = 1;
console.log(foo);
function foo(){
console.log("foo");
};
这次打印结果就是“1”;
这个打印结果为什么为1?
函数不是在初始阶段会提升到作用域顶部吗? @jawil

这段代码,我默认是全局环境下执行。
执行上下文的生命周期可以分为两个阶段(也就是这段代码从开始到结束经历的过程)。

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

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

这里我们先重点了解执行上下文中变量对象的创建。

变量对象的创建,依次经历了以下几个过程。

  1. 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值(全局环境下没有这步)。

  2. 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。

  3. 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。(上面的例子就属于这种情况,foo函数名与变量foo同名)

在上面的规则中我们看出,function声明会比var声明优先级更高一点。

我们直接从全局执行上下文开始理解。全局作用域中运行时,全局执行上下文开始创建。为了便于理解,我们用如下的形式来表示。

创建过程
global:EC = {
    // 变量对象
    VO: {},
    scopeChain: {},
    this: {}
}

// 因为暂时不详细解释作用域链和this,所以把变量对象专门提出来说明

// VO 为 Variable Object的缩写,即变量对象
VO = {
    foo: <foo reference>  // 表示foo的地址引用
   // foo: undefined
(如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。这个过程已被忽略)
}

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

// 执行阶段
VO ->  AO   // Active Object
AO = {
    foo: 1(此时函数已被重新赋值)
}

因此,上面你提供的代码,执行顺序就变成了这样:

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

所以最后打印的是1.

总结:你自己总结吧 @YanLIU0822

@jawil 谢谢,讲的听清楚的,但是我还有个问题,想问问,关于变量提升,是不是可以把变量函数提升也放在这里解释呀?

在进入执行上下文的时候,会有两个阶段。
第一阶段就是创建阶段,创建变量对象(暂时只考虑变量对象,除此之外还有作用域链,this):
变量对象包括三部分:
arguments(如果是函数的话);

函数声明,key : value,key为有function声明的函数名(所以对于函数表达式并不是这个范围,而是作为变量处理),value为指向函数的引用;( 这是不是跟函数提升有关,这里会提升整个函数体? ),如果已存在变量名,也会覆盖,(这就是对应着同一个上下文中,同名函数会被后者覆盖?);

变量声明,key: value, key 为有var声明的变量名,value也undefined,如果变量名存在的话,比如说跟上一步的函数名同名,则会跳过,保持key: value不变;(这个是不是跟变量提升只是提升变量声明,而不提升初始化值有关呀?)

在创建阶段已经完成了变量和函数提升,变量只会提升声明不包括初始化值,函数会提升整个函数体,而且如果出现同名,会被最近的函数体覆盖。除此以外,其他按照原始顺序不变?

然后到执行阶段,就是按照创建阶段的顺序执行吧。

是这样理解的么😄?

你所说的变量函数应该是函数表达式,就是类似var a=function(){},这种你就看做一个跟var a=1来看待。这当然也存在变量提升。

同名函数会被后者覆盖,如果是函数跟变量同名则会跳过,等到变量赋值时候,变量也会覆盖函数原来的值。其他粗略看了一眼,好像没什么毛病,这种概念性的东西多结合实践体会一下,博主已经写得很清楚了。@YanLIU0822

function foo(a) { var a; return a; } function bar(a) { var a = 'bye'; return a; } [foo('hello'), bar('hello')] // ['hello', 'bye']
那这块代码是不是可以用下面这句话解释了:

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

AO = { arguments: { 0: 'hello', length: 1 }, a: 'hello', // a: undefined }
AO = { arguments: { 0: 'hello', length: 1 }, //a: 'hello', a: 'bye' }

不知道可不可以这样理解@mqyqingfeng

@freeser 嗯嗯,正是如此~

@alexzhao8326 我觉得你举的例子也是符合作者所说的情况的。
即:var a =1; console.log(a);function foo(){console.log("foo");}; 在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
然而此时,只是将一个function类型的a,赋值为了1,这js这种也不是什么不允许的。就像你声明了一个对象,你把它赋值为了一个数组一样。所以打印出来,a的值还是1.

commented

@alvis888 我的canary 是报错,没有输出1

commented

@oakland 我在其他的文章里有看到一种说法,认为在函数作用域内,AO===VO的,认为函数初始化之后,将arguments放置在AO中即是VO https://stackoverflow.com/questions/6337344/activation-and-variable-object-in-javascript

commented

我突然发现看完正文,再把评论看一遍。再回头看一遍正文啥都明白了

@mqyqingfeng 你好,有幸拜读了你的文章,很是受益。
文中有个地方想请教一下

1. 函数的所有形参 (如果是函数上下文)
- 由名称和对应值组成的一个变量对象的属性被创建
- 没有实参,属性值设为 undefined

  • 这个“所有形参”是不是就是你所说的arguments对象?我看到你后面的例子中调用foo(1)的时候,在进入执行上下文后,AO的a属性是1,这个a是形参吧,它在函数执行的第一阶段(就是所谓的进入执行上下文阶段)的时候就已经赋值成1了么(我原来理解的是第一阶段是赋值成undefined的)?还是说在代码执行阶段再赋值成1的?
  • 后面那句“没有实参,属性值为undefined”,没看明白什么意思,你是说,如果没有实参,arguments属性就赋值为undefined么?

我又想到一个问题,既然评论里有提到VO和AO是一个东西,只是在执行上下文的生命周期中会转换角色而已,那就是说在第一步进入执行上下文的时候那时还不是AO,而是VO咯?这算挑刺吗?哈哈

@bluefantasy728
我理解也是的,VO和AO是同一个,未进入执行阶段VO的属性不能访问,进入执行之后,VO变成AO,就能访问了。

楼主,关于词法解析阶段的初始化顺序,你的总结和我之前的了解有很大出入啊,第一阶段是分析形参没疑问,但是是第二阶段你总结的是分析函数声明,我的笔记第二阶段是分析var声明的变量,最后一个阶段才是函数声明。笔记如下:

词法分析:

  1. 分析形参:接收形参到挂载到AO,接收实参赋值,如果没有传实参,值为undefined
  2. 分析变量声明(var):如果AO上已存在,不做修改(var 声明的变量和参数本质上都是私有变量),如果AO上不存在,挂载到AO,值undefined。
  3. 分析函数声明:挂载函数名到AO,并赋值函数体, 如果AO上已存在,覆盖。

注:

函数声明,在词法分析阶段,函数名挂载到AO上时,就在这个阶段立马赋值。
var 声明的变量,词法解析阶段,只负责挂载该变量到AO上,值为undefined,赋值在执行阶段进行。
也就是说在执行阶段赋值的,只剩下var声明的变量。
即使是使用var声明的函数表达式这种情况,在AO上值也为undefined。

用这个顺序解释任何场景的题目感觉都挺合理的,特别是对于变量重名的时候,例1:

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

词法分析顺序:形参->var声明->函数声明(包括赋值)

词法分析阶段结束时(未进入执行阶段),因为函数声明是最后分析的,所以AO:{a:function(){}}。
然后进入执行阶段,遇到console.log(a),此时AO上a为function(){},所以打印函数,继续向下执行,遇到var a = 1; 因为是执行阶段,只执行 a = 1,所以AO{a:1}。继续向下执行,遇到console.log(a);,打印此时AO上a,也就是1。

例2(上面大家的例子):

console.log(foo);//function(){}
function foo(){
     console.log("foo");//没执行
}
var foo = 1;

词法分析顺序:形参->var声明->函数声明(包括赋值)

词法阶段结束后(未进入执行阶段),AO{foo:function(){}}
然后是执行阶段,遇到console.log(foo),此时AO上的foo就是函数体,所以打印函数,然后继续执行,遇到var foo = 1;执行foo = 1;此时AO{foo:1},执行完毕,AO上最终的foo为1。但是你在代码最上面打印foo的时候,var foo = 1;这一句还没执行,AO上的foo还是函数,未被1覆盖。如果在代码的最后再打印一次console.log(foo),那就是1了。

至于你们上面总结的:
1,同一作用域下,函数提升比变量提升得更靠前。
2,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
【这些都是主观的结论,优先级之分只是表象,根本原因还是词法分析顺序和执行顺序一起决定了谁覆盖了谁】

@keyiran 这两种分析规则都能解释通这两个例子, 我也觉得这些都是主观的结论。这些结论可以帮助大家分析具体的例子,采用哪一种都可以,毕竟 Hoisting 只是一种思考执行上下文(特别是创建和执行阶段)在JavaScript中如何工作的一种方式,真到具体执行的时候,就涉及到 JavaScript 引擎的扫描规则,而这个规则究竟是什么样的呢?我并没有查过具体的资料,不过我的猜想是:顺序扫描,如果先有变量,后遇到同名的函数,就覆盖,如果现有函数,后遇到同名的变量,就不覆盖,接着执行~

看了通篇 反复看了评论看懂了 谢谢楼上各位大大
我的理解是 楼主的思考题和评论里的就是foo = 1 在console.log(foo);之前还是之后
楼主的思考题是之后的 那么 foo在ec的时候是function foo(){};和var foo;那么由于函数声明高于变量声明,AO里的foo不是undefined 而是 ,所以打印本身函数。
评论里的是在这个之前,先执行foo=1,AO里的foo已经是1了 ,故执行到console.log(foo);打印1;
请不吝赐教

commented

在函数上下文中,arguments与函数形参是什么关系?ES6与之前的实现似乎是不同的

@liruilong119 在 ES5 的非严格模式下,arguments 与形参有绑定关系,具体可以参考 #14,ES6 下一些情况比如有默认值的时候,会默认采用严格模式,取消这种绑定关系

为什么实参的值可以取到,而b就是undefinde?

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

@mengLLLL应该是由于活动对象AO在进入上下文时由Arguments对象初始化,能够获得函数的实参了,所以此时有值,而b是函数内部声明的参数,由于变量声明提升,被AO获得,但此时上下文还没执行,没有完成赋值操作所以b的值为undefined

@liruilong119 感谢回答,正是如此,@mengLLLL 这个时候的 AO 指的是进入执行上下文时的 AO ,只是完成了变量提升和函数提升,但是并没有进入执行阶段~

commented

看来我好好看看博主的 git了 基础太差了我

@mqyqingfeng

function foo() {
    console.log(a); 
    a = 1;
}
foo(); // a is not defined
// 这里的a不是全局变量吗?为什么在全局找不到?
function foo() {
    console.log(a);
}
a = 1;
foo(); // 1
// js是词法作用域,为什么打印出来是1?
function foo() {
    console.log(a);
}
foo(); // a is not defined
a = 1;
// 还是一样,我觉得应该是undefined

求解答,谢谢。

@veedrin

  1. 第一个a = 1是一个未声明的变量进行赋值的操作,它不会被提升,所以在执行console.log(a)的时候在搜索的时候搜索不到的,然后在执行a = 1的时候才会在全局的VO中进行注册,这个时候a才能被搜索到
  2. 第二个首先在全局的VO中注册了foo函数,然后执行a = 1的时候再在全局中注册了a这个变量,接着再执行foo(),所以打印出来的就是1
  3. 第三个跟第一个是同样的解释

@DarkYeahs 感谢回答哈~ o( ̄▽ ̄)d @veedrin 我也来说一下,第一个例子是因为,只有通过 var 声明的变量才会有提升, a 没有提升,执行 console.log(a) 的时候也没有挂载到全局变量上,所以会报错。

第二个和第三个例子本质是一样,值得注意的是,词法作用域规定的是查找变量的区域有哪些,并不规定具体的内容有哪些,以第二个例子为例的话,词法作用域规定了 foo 函数查找变量的时候,先从 foo 函数中查找,查找不到,就去到全局变量中查找,但是全局变量中到底有哪些变量,这是要根据函数什么时候执行才能确定的

let a = 1;
console.log(window.a);//undefined
博主能解释一下let和var底层的区别吗?不胜感激~

@mqyqingfeng @DarkYeahs

原来未声明的变量不会提升,多谢!

issues留言不会提醒么?说好的同性交友社区呢!

@Zefeng666 let 的这种特性,即 let 会在全局作用域下创建一个新的绑定,但该绑定不会添加为全局对象的属性,应该是规范中就规定成这样的,跟它的不提升特性并没有什么关系。

至于 let 和 var 底层的区别,我能想到的内容在 ECMAScript 6 入门 都有……

@mqyqingfeng

想请教一下博主关于变量提升的问题:

console.log(a);  // undefined
var a = 1;

因为变量提升,打印 a 不会报错,但是只会打印出 undefined,只声明没有赋值。

按照执行上下文的理解,代码会分成两个阶段进行处理:分析和执行,关于分析中变量声明的部分,

由名称和对应值(undefined)组成一个变量对象的属性被创建

这里是不是改为 只创建名称,不赋值 更合适呢?

@RayJune 这就要看怎么理解 undefined 这个值了,我也不能保证我说的是正确的,在我看来,undefined 是一个原始值,是一个占用着固定内存大小的内容,当只声明变量的时候,其实相当于将这个原始值赋值给变量,所以才会表述为 由名称和对应值(undefined)组成一个变量对象的属性被创建

commented

个人觉得变量提升就是个伪概念,完全可以摒弃这一说法。用执行上下文的变量对象来理解这些要容易的多。

@joker09 确实,变量提升是一个在规范中找不到的术语。变量提升被认为是思考执行上下文(特别是创建和执行阶段)在 JavaScript 中如何工作的一种方式。不过如果直接用变量对象去解释,估计给新手会增加不少的理解成本~

commented

确实,一边看楼上各位的精彩讨论,一边再看分析,在没看评论之前,我也就立马想到了,比如调整变量a和函数a的顺序

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

到底会产生什么样的结果?
实际上,@keyiran ,这位大大,我觉得给出了最好的解释,他反驳了一个问题就是,前面很多人强调的,函数声明会是最高的优先级,同名变量不会覆盖?我也觉得这个地方,确实有点说不通,
答案,就是,他说的,执行顺序和词法分析决定了这个谁覆盖谁的问题。
当然,很多看不懂的人还继续问为啥的话,我觉得,应该是没看懂,楼主写的很关键的地方就是

执行环境准备+执行阶段,这2个概念,

其实,我之前看过很多,有这个概念,一直没理解,今天看了这么多同学的讨论,对这个地方,确实有点理解啦

@Ghohankawk 哈哈,感谢分享自己的想法,正是大家的交流才让一个知识点越聊越明了~

function bar(a){
	console.log(a);
	function a() {};
}

执行 bar(2)
结果是 function a() {}
是不是说明 如果形参名称跟已经声明函数相同,则形参也不会干扰声明函数这类属性

@houzhiying977 可以呀,其实在规则里也有讲到:

default

如果变量对象已经存在相同名称的属性,则完全替换这个属性,这个所谓的“相同名称的属性”就包括形参和变量声明~

所以所有的变量都是存在变量对象上的对吗?而原始类型存在栈上的说法是错误的?变量对象又是存在哪的?- -

commented

需要注意的是,在严格模式下,this默认是指向undefined,并非window对象。

@ShumRain 这个不冲突吧……变量对象你可以理解为一个对象,对象以及对象中的值存在哪里就还是存在哪里呀~

@jiangjiu 确实如此,感谢补充哈~

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

foo(); // Uncaught ReferenceError: a is not defined

在进入执行上下文和执行到console语句时,AO都是:

AO = {
    arguments: {
        length: 0
    }
}

因此会报错,因为AO中不包含a的定义消息。

如果把代码改一改,结果就会发生变化

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

foo(); // undefined

原因在于在编译阶段,变量声明会被提升,代码其实会被理解成以下的样子:

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

foo(); // undefined

因此,在进入执行上下文时和执行到console语句时:

AO = {
    arguments: {
        length: 0
    },
    a: undefined
}

@wnbupt 感谢补充哈~ 确实是这样的~

博主请教一下:变量对象和作用域有什么区别。
我看你不知道的JS中对作用域的解释为:定义如何在某些位置存储变量,以及如何在稍后找到这些变量。我们称这组规则为作用域。
我看汤姆大叔博客对变量对象的解释:如果变量与执行上下文相关,那变量自己应该知道它的数据存储在哪里,并且知道如何访问。这种机制称为变量对象。
感觉都是规定数据存储和访问的机制,不同点是什么。我的理解二作用域是一个抽象的概念,变量对象是对抽象概念实现的一个实实在在的对象?不知道理解是否正确。

@mqyqingfeng , @Ghohankawk ,两位大大你们好。如果真如@keyiran 所说,词法分析的顺序是 1形参、2变量声明、3函数声明,且”如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性“这句话只是主观的论断。那下面的代码怎么解释呢?

function fun(a){
   var a;
   console.log(a);
   a = 2;
}

fun(1);  //打印1,而不是undefined

按照@keyiran的说法,第一步分析形参后VO的属性a为1,接下来第二步是分析变量声明,a作为声明的变量,值为undefined,应该会覆盖掉VO已存在的a。但是结果却为1(Chrome浏览器下)。为什么呢?
难道正确的词法分析顺序应该是 1变量声明、2形参、3函数声明?

@lawpachi 嗯嗯,我的想法跟你一致呀~

@Kevin-Loeng 我们先整理下变量对象的分析顺序:

文章中的:

  1. 先分析形参
  2. 分析函数,如果存在,则替换
  3. 分析变量,如果存在,则不修改

@keyiran 的方式:

  1. 分析形参
  2. 分析变量,如果存在,不做修改
  3. 分析函数,如果存在,则替换

其实想一想,两者实现的最终结果是一样的……

以你举得这个例子为例:

function fun(a){
   var a;
   console.log(a);
   a = 2;
}

fun(1);  // 打印1,而不是undefined

按照我的方式:

  1. 分析形参,值为1
  2. 分析函数,不存在,则不替换
  3. 分析变量,已经存在,不修改

所以最终打印的值为 1。

按照@keyiran 的方式:

  1. 分析形参,值为 1
  2. 分析变量,存在,不做修改
  3. 分析函数,不存在

最终分析的值也是 1。

如果真要论个优先级的话,函数声明最高,因为它能覆盖变量声明和形参,其次是形参,因为变量声明不能覆盖形参,最低的就是变量声明了。

我个人觉得无论是文章中的还是 @keyiran 的,都是一种对规律的总结,真正的实现方式到底是怎样的,估计只有实现语言的人知道了。

而且你想想,有没有可能在底层实现的时候,是顺序扫描代码的,如果先有变量,后遇到同名的函数,就覆盖,如果现有函数,后遇到同名的变量,就不覆盖呢?

还有一个问题,执行代码会产生执行上下文,执行上下文分为1.进入执行上下文2. 代码执行。按照分析博主的分析,变量提升处于1.进入执行上下文阶段。
但是JS代码执行会先进行编译(词法分析,生成AST,生成计算机识别代码),然后进行代码执行。编译过程的词法分析就会变量提升。所以又怎么会在执行代码时候才产生的变量提升呢?

@lawpachi 没有说到执行代码的时候产生变量提升呀,不过执行代码的时候,会根据代码修改变量对象~

麻烦博主指教:进入执行上下文会产生AO变量对象,按照@jawil 变量对象创建的几个过程的说法 ,

变量对象的创建,依次经历了以下几个过程。

建立arguments对象。

检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。

检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。(上面的例子就属于这种情况,foo函数名与变量foo同名)

所以按照如上所述变量对象的创建会产生变量提升。

但是根据你不知道的JS中写到,变量提升存在代码的编译过程。还没到产生执行上下文阶段。

@lawpachi 文章中其实没有区分编译阶段与进入执行上下文阶段的,文章中认为两者是一样的:

default

分析其实就是指编译的过程,也是产生执行执行上下文的过程

commented

箭头函数中已经没有了arguments属性... 博主能分析下为什么把这个干掉了呢?

@xxyj 可能是因为 arguments 设计的不够好吧……正常情况下,我们使用 arguments 还需要转成数组形式,ES6 可能认为这是个没有必要的操作,所以就干掉了 arguments, 然后提供了一个更好用的方案,就是利用拓展运算符:

(...arguments) => console.log(arguments);

你还可以写成这样:

(first, ...arguments) => console.log(first, arguments);

使用这种方式,完全可以替代掉 arguments,所以就去掉了吧……

不过这是我猜的啦,在规范中我并没有找到去掉的原因~

@mqyqingfeng 你说全局对象是由Object构造函数实例化的一个对象,但是window.proto === Object.prototype为false啊

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

  b = 3;

}

foo(1);

这里我有一点不懂,大佬。 就是b=3这里 b不是全局变量吗? 为什么执行之后,b还是会覆盖var b = 2 ? 它这个b = 3 不是应该放到全局VO里面去吗?

@mqyqingfeng

function a(b){
	console.log(b); // ƒ b(){}
	function b(){}
	console.log(b); // ƒ b(){}
}
a(function c(){})

对于这个现象,我更倾向于js对于函数的执行采取的流程为进入函数的执行上下文,创建函数的AO -> 形参/变量的声明 -> 实参赋值给AO中对应的属性 -> 函数的声明 -> 执行函数体内其余,而且我有个看法,是否是这样的:js的编译阶段,只会对全局环境下的变量和函数进行声明及提前,而不会去访问被声明的函数体内部?否则似乎无法解释函数体内部的函数声明会覆盖实参传给形参的值这一现象。

function foo3(a) {
    var a = 10
    function a() {}
    console.log(a)
}
foo3(20) // 10

请问博主为什么会输出10呢?不是function声明会被提升,覆盖arguments中的a和变量声明,应该打印出[Function: a]吧?多谢解答~~
@mqyqingfeng

@wnbupt 要分清楚函数的分析过程和执行过程。

分析阶段:
1、形参并赋值: a=20;(这里不确定形参是不是在分析阶段就赋值)
2、函数声明并赋值: a=function a(){};(函数声明的同时会进行赋值操作)
3、变量声明: var a(已经存在变量a,所以不会对AO有所影响)

执行阶段:

1、a=10;(将a的值修改为10,不管你以前是函数还是其他什么值,现在就是10)
2、console.log(a); // 10

@MagicHacker 嗯,这样说确实不严谨,准确的说是继承了 Object
default

@ciomedyu 这个……在函数内部声明了 var b = 2; 然后修改 b = 3,打印的结果肯定是 3 呀……

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

  b = 3;
  console.log(b) // 3
}

foo(1);

这里的问题应该是外层的 b 并没有被修改:

var b = 1;

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

  b = 3;
  console.log(b) // 3
}

foo(1);

console.log(b) // 1

@mygaochunming 感谢回答哈~ 确实如此 ヾ(^▽^ヾ)

@mygaochunming 多谢多谢~我明白了,其实就是因为函数声明会优先变量声明,已经被提前了,之后的a = 10就会覆盖函数,所以打印出来就是10。

@mqyqingfeng

function foo(b){
	console.log('b', b);
        /* 
            b ƒ b(d){
		console.log('d1:', d);
		function d(){};
		console.log('d2', d);
	    } 
        */
	function b(d){
		console.log('d1:', d); // d1: ƒ d(){}
		function d(){};
		console.log('d2', d); // d2 ƒ d(){}
	}
	console.log('b():', b(function c(){})); // b(): undefined
}
foo(function a(){})

上述例子的补充,发现函数体内声明的函数依然如此。JS引擎在预编译阶段究竟做了什么?为什么“函数体内部的函数声明提前”能够将尚未执行的函数的实参值给顶替掉?大佬是否对此有所了解

针对第二个问题,我有个不同的见解。

console.log(foo);

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

var foo = 1;

这里是因为变量提升(hoisting)
进入执行上下文时,伪代码如下:

var foo;// 把所有变量提升
foo = function() {console.log('foo')} ;// 函数声明会被提升(函数表达式并不能被提升)
console.log(foo);
foo = 1;

所以执行代码的时候,会打印出函数

谢谢博主,看了之后收益颇多。
想请教一个问题,关于es6 let const没有变量提升,是不是就不能按照“进入执行环境”那三步来分析VO了?再结合此前讲的静态作用域,怎么感觉像是变成了动态作用域了?关于let应该怎么理解呢?

@lucky3mvp 是的哈~ let 和 const 没有变量提升,所以用的是其他的规则,比如说暂时性死区,要说怎么解释 let 的话,嗯……我个人认为是:过程依然分为进入执行上下文和代码执行两个阶段,在进入执行上下文阶段,对于 let 声明的变量进行"检查",比如是否重复声明,暂时性死区等等,然后代码执行阶段赋值,那么问题来了,是在进入上下文阶段创建的呢还是代码执行阶段创建的呢?一时间我也有些疑惑啦……

至于变成了动态作用域,嗯……这个意思不是很理解哈~

@Tan90Qian

为什么“函数体内部的函数声明提前”能够将尚未执行的函数的实参值给顶替掉?

嗯……因为规则就是这么设计的……那为什么这么设计呢?我就不清楚了……

@coconilu hoisting 毕竟只是一个帮助大家理解的概念,我也不能百分百确定就是这样,只要能用它解释更多的场景就没有什么问题~

你好博主,关于第一题昨天想了许久没有想通。foo,bar方法中,变量a没有用 var 进行修饰,会被解析为全局变量。在执行阶段,在foo,bar的AO中查找a,没有找到,然后到全局中查找,同是在全局中,为什么执行foo会报错。
今早又看了一遍,有了新的理解:在进入执行上下文阶段,只会将有 `var,function修饰的变量或方法添加到变量对象中。在进行执行阶段前一刻,foo和bar方法的它们的VO中均没有a属性。在执行阶段,执行到 a= 1时,才将a变量添加到全局的变量对象中而不是在进入执行上下文阶段。所以foo方法中会报错,bar方法会打印 1。
请问博主可以这么理解吗?

function foo() {
    console.log(a);
    a = 1;
}
foo(); // Uncaught ReferenceError: a is not defined

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

@LbHongYu 是的~