深究hoisting在ES3和ES6之间的差异

103 阅读4分钟

我穿越了,文章写于*2020-04-11 21:06

知其然而知其所以然!

最近在群里看到一道题目:

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

这是一道关于JS变量提升(hoisting)的笔试题,可能有点人似曾相识,看似简单,但是错的人还是蛮多的。可以先自测一下~

最终答案:2,1(ES6中)  2,2(ES3中)

一、出乎意料的结果

关于JS变量提升(hoisting)的概念我在这里就不多说了,不懂的可以自行查阅MDN:developer.mozilla.org/zh-CN/docs/…

先从ES6角度来分析,即浏览器支持块级作用域的情况下(因为有大部分人不知道该代码在ES3中的执行情况)。

首先来看一下一些同学得出的答案:2,0

那么他是这么想的,执行逻辑相当于:

// EC(G) 全局执行环境
//变量提升
// var a;
var a = 0;
if (true) {    
    // EC(block) 块级作用域    
    // 函数声明提升(提升声明a并且初始化为一个函数)    
    // function a(){}    
    function a(){}    
    a = 1;    
    a = 2;    
    console.log(a);// 打印2
}
console.log(a);// 打印 0

没错,在JS执行过程中,{} 会形成一个块级作用域,并且对function生效。但是事实上代码真的是这样执行的吗?

我们直接在Chrome下测试一下。

图片

显然,代码并不是像上面所说的那样。为了更能清楚的解释这个原因,我们切换到ES3(IE10以下) 中去执行一下看看结果。

图片

显然,明显出现不同的结果了吧,我们一起来分析分析。
在ES3中,该代码执行等价于:

//EC(G) 全局执行环境
//变量声明
// var a;
//函数声明提升并初始化
//function a(){}
var a = 0;
//if (true) {    
    a = 1;   
    function a(){}    
    a = 2;    
    console.log(a);// 打印 2
//}
console.log(a);// 打印 2

然而,在ES迭代过程中,并不是把旧的执行机制完全丢弃,而是采用进行向后兼容的方式进行更新,所以在Chrome(ES6)中执行的时候还存在非常重要的一步兼容操作:


//EC(G) 全局执行环境
//变量声明
// var a;
//函数声明提升(仅提升声明a,并不会把a初始化为函数)-------tips1---------
// function a
var a = 0;
if (true) {    
    // EC(block) 块级作用域    
    // 函数声明提升    
    // function a(){}    
    a = 1;// 重新对a进行赋值    
    //兼容操作:执行完14行的时候,会将之前对a的所有修改操作映射到EC(G)中,    
    //用于兼容ES3的方式,那么此时全局的a = 1。    
    function a(){};//-------------tips2---------------    
    a = 2;//这里修改的是当前EC(block)的a    
    console.log(a);// 2
}
console.log(a);// 1

上面有两个标注为----tips----的地方容易会让你产生疑虑。

如果你对第一个tips1产生疑虑,那么请看这段代码:(你可以手动尝试)

//var a = 0; 
//我去掉这段代码console.log(a);
//打印 undefined,说明tips1成立。
if(true){  
    a = 1;  
    function a(){}  
    a = 2;  
    console.log(a)// 打印 2
}
console.log(a);// 打印 1

根据执行结果显示,tips1成立。

如果你对tips2产生疑虑,那么请看下面代码断点调试右边scope的变化:

第一步:Global中a=0,此时只有Global,即EC(G)。

图片

第二步:创建EC(block),此时函数a已经提升到最前面, Global中a=0。

图片

第三步:将EC(block)中的a赋值为1, Global中a=0,图略。

第四步:当执行完15行代码的时候,Global中的a已经修改为1。

图片

第五步:将EC(block)中的a赋值为2,之后修改的a都不会对全局a造成影响。Global中a=1。

图片

第六步:销毁EC(block),Global中a=1。

图片

所以在执行function a(){}后会对当前函数作用域或者全局作用域的a产生影响,进行向后兼容操作。如果存在多条该语句,那么则会同步多次修改。

二、差异点

上面详细分析了在ES3和ES6的环境下JS在hoisting方面的不同点。有人可能会有疑虑,为什么不拿ES5进行分析?

简单原因就是ES5较ES3没有实质性的变化。

需要注意的是,上面的比较都是针对块级作用域进行分析,在ES6中,在全局作用域和函数作用域的分类上新增块级作用域 ,而块级作用域仅对function、let、const生效,对var则不存在限制。而在ES3中,对var的操作只需要区分函数作用域全局作用域即可。

另外,可能有些同学还会见过类似这样的题目:(考察hoisting工作时机)

console.log(a);
if (false) {    
    a = 1;    
    function a(){}    
    a = 2;    
    console.log(a);
}

这里考察的是, JS hoisting是在编译(词法分析)的时候进行,而不是在代码执行的时候,如何验证?请看上面代码在ES3和ES6中的表现:

Chrome中:

图片

IE10以下:

图片

三、总结

以上就是个人对比不同es版本下hoisting表现不同差异的分析和总结,这类题目在面试中很常见,或许能够助你一臂之力~