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 · 230 comments
Open

JavaScript深入之变量对象 #5

mqyqingfeng opened this issue Apr 23, 2017 · 230 comments

Comments

@mqyqingfeng
Copy link
Owner

@mqyqingfeng 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

@izhangzw izhangzw commented May 10, 2017

Arguments对象是什么 - -。

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 10, 2017

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

Loading

@izhangzw
Copy link

@izhangzw izhangzw commented May 12, 2017

VO 和 AO 到底是什么关系。

Loading

@jawil
Copy link

@jawil jawil commented May 12, 2017

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

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

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 12, 2017

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

Loading

@alexzhao8326
Copy link

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

Loading

@jawil
Copy link

@jawil jawil commented May 23, 2017

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

  1. 创建阶段

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

  1. 代码执行阶段

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

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

@alexzhao8326

Loading

@alexzhao8326
Copy link

@alexzhao8326 alexzhao8326 commented May 23, 2017

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

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 23, 2017

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

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 26, 2017

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

default

Loading

@zuoyi615
Copy link

@zuoyi615 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

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 26, 2017

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

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 27, 2017

@zuoyi615 o( ̄▽ ̄)d

Loading

@oakland
Copy link

@oakland oakland commented May 29, 2017

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

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 29, 2017

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

Loading

@ckclark
Copy link

@ckclark ckclark commented May 30, 2017

是w3school 不是W3C school

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 31, 2017

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

Loading

@MrGoodBye
Copy link

@MrGoodBye MrGoodBye commented May 31, 2017

思考题第二题:

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

Loading

@MrGoodBye
Copy link

@MrGoodBye 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");
}

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

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

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

Loading

@xumengzi
Copy link

@xumengzi xumengzi commented May 31, 2017

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

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

望不吝赐教!

Loading

@anjina
Copy link

@anjina anjina commented Oct 23, 2020

有幸读到博主的文章,这里有点疑问,《你不知道的JavaScript(上卷)》中提到,变量的声明和查找应该发生在代码的编译阶段,在执行阶段才有了上下文环境,进而创建变量对象;但是如你文中提到的在创建上下文环境的时候,在变量对象中进行了函数声明和变量声明;JavaScript代码执行应该是一个怎样的流程呢?还望解答

我理解 这里的文章中 代码编译阶段 和 执行上下文的准备阶段 应该是一个意思

Loading

@anjina
Copy link

@anjina anjina commented Oct 23, 2020

看过之后还有一点疑惑。我知道JS里的垃圾回收规则是引用计数。
如果按作者的说法,一个函数闭包持有的scopeChain里包括上层的VO,而且VO里保存的是函数内所有的变量以及形参等。
那是不是意味着只要函数闭包一直存在,那它所持有的作用域链上的所有资源(不管它用到还是没用到都在VO里被引用)都不会销毁。

个人理解 上层VO中没有被引用的变量在上层函数执行完出栈后会 销毁

Loading

@shekang
Copy link

@shekang shekang commented Oct 26, 2020

对于第二题的理解

console.log(foo);

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

var foo = 1;

创建变量对象

第一步:函数声明 foo

vo = {
  foo: reference function foo(){},
}

第二步: 变量声明 foo, 由于变量名称跟已经声明的函数名称相同, 所以变量对象的 foo属性依旧是reference function foo(){}

执行阶段

根据上面分析的vo对象, foo应该输出foo函数

变形

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

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

根据vo对象,打印的依旧是foo函数

再变

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

由于将foo赋值为1, 所以打印 1

Loading

@Shwvi
Copy link

@Shwvi Shwvi commented Oct 29, 2020

Arguments对象是什么 - -。

就是传入函数的参数呀

Loading

@silence1226
Copy link

@silence1226 silence1226 commented Dec 17, 2020

AO = {
arguments: {
0: 1,
length: 1
},
a: 1,
b: undefined,
c: reference to function c(){},
d: undefined
}
请问各位为什么a的值是1,d的值是undefined?
因为资历比较浅,看不太懂

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

Loading

@cw84973570
Copy link

@cw84973570 cw84973570 commented Jan 2, 2021

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

变量对象会包括:

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

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

    • 没有实参,属性值设为 undefined

  2. 函数声明

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

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

我想问下这里是按顺序初始化变量对象的吗?

还有说形参的那部分,前面说名称和对应值,后面说实参,请问这里的对应值和实参是否指的是同一个东西?


找到原文了:http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/#entering-the-execution-context

Loading

@yuanwenq
Copy link

@yuanwenq yuanwenq commented Jan 22, 2021

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

foo(); // ???

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

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

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

AO = {
arguments: {
length: 0
}
}
没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。

对于这道题的这个解释我不是很明白这个时候函数内部没有我理解,但是全局上下文中为什么就没有呢?在执行之前准备阶段函数内部的a=1;怎么处理?他跟函数外边var a; 然后函数内部a=1;不一样吗?没有使用var声明会变成一个全局对象的属性,那默认应该是undefiend啊为啥此处报错了呢? @mqyqingfeng

因为 a 作为全局变量的属性,没有 var 声明,没有声明就没有提升。而且 console.log(a) 优于 a = 1 执行,此时的window 还没有 a 这个属性,所以在 console 打印的时候找不到 window.a 这个属性,自然就会报错

Loading

@wpolairs
Copy link

@wpolairs wpolairs commented Mar 27, 2021

var a = 0;
if(true){
console.log(a);
a = 1;
function a(){};
a = 21;
console.log(a);
}
console.log(a);
大家可以帮忙看下这个题吗?第三个输出没搞懂为什么

Loading

@gaopeng0108
Copy link

@gaopeng0108 gaopeng0108 commented Apr 4, 2021

@wpolairs 一般来说,不建议在 if 语句中使用函数声明(可以使用函数表达式),因为不同浏览器的解释机制不同。但是针对你给出的代码进行一定的改写就可以看出在相应的浏览器下是如何运作的

var a = 0;
if (true ){
  console.log(window.a, a);
  a = 1;
  function a(){ };
  a = 21;
  console.log(window.a, a);
}
console.log(window.a, a);

image

可以看到,全局环境中的 a 和 条件语句中的 a 被引擎解释成不同的两个变量

Loading

@ablikim915
Copy link

@ablikim915 ablikim915 commented Apr 7, 2021

console.log(window.$a,$a)  // undefined undefined
var $a = 1;
console.log(window.$a,$a)  // 1 1
{
    console.log(window.$a,$a)  // 1 ƒ $a(){} 
    $a = 2;
    console.log(window.$a,$a)  // 1 2
    function $a(){};
    console.log(window.$a,$a)  // 2 2
    $a = 3;
    console.log(window.$a,$a)  // 2 3
}
console.log(window.$a,$a)  // 2 2

这段代码中,为什么 $a 的第四次赋值没有覆盖全局 $a 的值?我试了一下,如果函数声明去掉的话,$a = 3 会覆盖全局 $a 的值。
@mqyqingfeng 求大佬解个答。

Loading

@pen-duo
Copy link

@pen-duo pen-duo commented May 26, 2021

Arguments对象是什么 - -。
Arguments对象时所有非箭头函数中都可用的局部变量。此对象包含传递给函数的每个参数
,第一个参数在索引0处。Arguments对象不是一个数组,它类似于数组,但除了length属性和索引元素之外没有由任何数组的属性和方法。

Loading

@toFrankie
Copy link

@toFrankie toFrankie commented Aug 23, 2021

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

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

麻烦大佬解惑,谢谢!

Loading

@zhou-zzz
Copy link

@zhou-zzz zhou-zzz commented Sep 10, 2021

image
文中说第一个foo()会报错 ? 很困惑

Loading

@Chorer
Copy link

@Chorer Chorer commented Sep 10, 2021

image
文中说第一个foo()会报错 ? 很困惑

我这边试了一下确实是会报错的,你看看是不是控制台变量没有清除

Loading

@zhou-zzz
Copy link

@zhou-zzz zhou-zzz commented Sep 10, 2021

Loading

@banxian721
Copy link

@banxian721 banxian721 commented Oct 18, 2021

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

Loading

@wz-china
Copy link

@wz-china wz-china commented Nov 22, 2021

老师您好,我想问一下

function test() {
  console.log("out");
}
(function () {
  if (false) {
    function test() {
      console.log("in");
    }
  }
  test();
})();
// 为什么这里输出的结果不是out,而是直接报错呢?

Loading

@toFrankie
Copy link

@toFrankie toFrankie commented Nov 23, 2021

老师您好,我想问一下

function test() {
  console.log("out");
}
(function () {
  if (false) {
    function test() {
      console.log("in");
    }
  }
  test();
})();
// 为什么这里输出的结果不是out,而是直接报错呢?

在 Chrome 中,块级作用域内的函数声明,会发生提升行为,但请注意这个提升至 IIFE 的 testundefined(这点与起初的认知是稍微有点不同,对吧),这是浏览器 JS 引擎的行为,那使用圆括号去调用 undefined 肯定会报错。还记得在 Safari 14 的时候,这段代码并不会报错,因为提升至 IIFE 的 test 就是一个函数,因此不会报错。当我今天再去试的时候(Safari 15),已经会报错了,表现与当前最新的 Chrome 一致。

另外,在 ES5 规则中,函数只能在全局作用域和函数作用域内声明,而不能在块内声明。注意,这是规则本身就不允许的,但是浏览器厂商在实现的时候,并没有严格遵循这一规定,不同 JS 引擎实现可能有所不同。因此,才会有上述的差异。

总的来说,应遵循这一原则:函数声明请在全局或函数作用域内声明,若在块内,请使用函数表达式。在 ESLint 中专门有一个规则去检查这种情况:no-inner-declarations

与你疑问相关的,此前我写过一篇文章有提到,有兴趣可以看下:点击这里

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Nov 23, 2021

@toFrankie 十分感谢帮忙回答,@wz-china 这个问题其实在阮一峰老师的 《ECMAScript 6 入门》也有提到,链接是 https://es6.ruanyifeng.com/#docs/let ,在块级作用域与函数声明这个章节,跟你是同样的例子

Loading

@Bkyaro
Copy link

@Bkyaro Bkyaro commented Dec 1, 2021

久闻冯哥大名,请教有关最后一个例子的问题

question

感谢

Loading

@SiriusZHT
Copy link

@SiriusZHT SiriusZHT commented Dec 1, 2021

Loading

@Bkyaro
Copy link

@Bkyaro Bkyaro commented Dec 2, 2021

@SiriusZHT 感谢解答😘

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Dec 2, 2021

@Bkyaro 是冴羽!冴羽!!冴羽!!!

Loading

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Dec 2, 2021

@SiriusZHT 感谢解答~

Loading

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

Successfully merging a pull request may close this issue.

None yet