变量提升

在“当前上下文”中,代码执行之前,浏览器首先会将所有带 var/function 的关键字进行提前声明和定义,var 进行提前声明,function 不仅会提前声明还是提前定义(赋值)。
例如:

  1. console.log(a);
  2. var a = 1;
  3. console.log(fn);
  4. function fn() {}

image.png
a 的声明和定义在 console.log(a) 之后,但是提前使用 a 不会报错而是输出了 undefined,说明 a 被提前声明了
fn 的声明和定义在 console.log(fn) 之后,但是提前使用 fn 不会报错,并且正确的输出了 fn,说明 fn 不仅被提前声明了,还被提前定义(赋值)了

变量提升,无论条件是否成立都会进行变量提升。但是在判断体中,function 新老版本浏览器中表现不太一样(var 没有影响),在老版本浏览器中,function 变量提升之后是声明+定义,但是在新版本浏览器中 function 变量提升只会声明不会定义。
新版本浏览器:
image.png
老版本浏览器:
image.png

let、const 的类变量提升

为什么说 let、const 是类变量提升了,因为let、const 本身是没有变量提升的,但是他们存在某种机制来对 let、const 进行处理。
以 let 为例:

  1. console.log(a);
  2. let a = 1;

由于 let 是没有变量提升机制的,所以在还没有声明之前就使用了肯定会报错,但是会报什么错误了?「Uncaught ReferenceError: a is not defined」
image.png
其实不然,报错 「Uncaught ReferenceError: Cannot access ‘a’ before initialization」
image.png

注意:如果你要在浏览器控制台中直接运行,请加上一个块级作用域。 { console.log(a); let a = 1; }

这是为什么了?其实最开始浏览器从服务器获取的js都是文本(字符串),声明的文件格式「content-type: application/javascript」。浏览器首先按照这个格式去解析代码,也就是进行「词法解析」生成 AST 语法树。
基于 let、const 等声明的变量,在词法解析阶段就已经明确在此上下文中未来一定会存在这些 let、const 声明的变量,在代码执行时,如果在具体声明的变量之前使用了这些变量,浏览器就会报错「Uncaught ReferenceError: Cannot access ‘a’ before initialization」。所以说 let 、const 类似会有一种变量提升,但是本质上并不是变量提升。

看如下代码,用 debugger 看看。

  1. debugger;
  2. console.log(a);
  3. var a = 1;
  4. console.log(b);
  5. let b = 2;

image.png
发现 b 是定义在脚本中,这里的脚本就是 VG(G),而 a 是在 全局 GO 中,说明二者并不是一样的处理,let 词法解析阶段就已经明确在此上下文中未来一定会存在这些 let、const 声明的变量。

接下来我们看一个面试练习。

面试练习

  1. console.log(fn);
  2. function fn() {
  3. console.log(1);
  4. }
  5. console.log(fn);
  6. var fn = 12;
  7. console.log(fn);
  8. function fn(){
  9. console.log(2);
  10. }
  11. console.log(fn);

第一步变量提升

  1. function fn() {console.log(1);} fn 函数声明加定义,一个函数的创建会在 Heap 堆内存中开辟一块空间(0x001)来存储函数(详情看上一篇文章:函数的底层执行机制),所以 fn -> 0x001。

前端基石:预处理机制,变量提升 - 图7

  1. var fn = 12; var 只会声明不会定义,但是发现上下文中已经声明过 fn 了,所以不会重复声明。
  2. function fn(){console.log(2);},fn 现在是一个新的函数,但是发现在此上下文中已经存在 fn ,所以不会重复声明,但是会重新定义。所以开辟一块新的内存空间(0x002)来存储新的 fn 函数。对比第一步的 fn 内存地址是不一样的,开辟新的内存就是一个定义的操作,所以 fn -> 0x002。

前端基石:预处理机制,变量提升 - 图8

第二步代码执行

  1. console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 0x002。所以输出 ƒ fn(){console.log(2);}
  2. function fn() {console.log(1);} 跳过,因为这一步在变量提升阶段已经处理过了。
  3. console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 0x002。所以输出 ƒ fn(){console.log(2);}
  4. var fn = 12; var 声明已经在变量提升阶段处理过,现在就是进行重新赋值。fn -> 12

前端基石:预处理机制,变量提升 - 图9

  1. console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 12。所以输出 12。
  2. function fn(){console.log(2);} 跳过,因为这一步在变量提升阶段已经处理过了。
  3. console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 12。所以输出 12。

image.png

总结

这类变量提升的题目,在面试中会经常出现,但是很多同学会认为比较简单,往往出问题,所以遇到这类题目,需要熟练的在大脑中构想一副脑图,明白内存中变量的存储指向。这样才能清楚无误的输出结果。