变量提升
在“当前上下文”中,代码执行之前,浏览器首先会将所有带 var/function 的关键字进行提前声明和定义,var 进行提前声明,function 不仅会提前声明还是提前定义(赋值)。
例如:
console.log(a);
var a = 1;
console.log(fn);
function fn() {}
a 的声明和定义在 console.log(a) 之后,但是提前使用 a 不会报错而是输出了 undefined,说明 a 被提前声明了。
fn 的声明和定义在 console.log(fn) 之后,但是提前使用 fn 不会报错,并且正确的输出了 fn,说明 fn 不仅被提前声明了,还被提前定义(赋值)了。
变量提升,无论条件是否成立都会进行变量提升。但是在判断体中,function 新老版本浏览器中表现不太一样(var 没有影响),在老版本浏览器中,function 变量提升之后是声明+定义,但是在新版本浏览器中 function 变量提升只会声明不会定义。
新版本浏览器:
老版本浏览器:
let、const 的类变量提升
为什么说 let、const 是类变量提升了,因为let、const 本身是没有变量提升的,但是他们存在某种机制来对 let、const 进行处理。
以 let 为例:
console.log(a);
let a = 1;
由于 let 是没有变量提升机制的,所以在还没有声明之前就使用了肯定会报错,但是会报什么错误了?「Uncaught ReferenceError: a is not defined」?
其实不然,报错 「Uncaught ReferenceError: Cannot access ‘a’ before initialization」。
注意:如果你要在浏览器控制台中直接运行,请加上一个块级作用域。 { 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 看看。
debugger;
console.log(a);
var a = 1;
console.log(b);
let b = 2;
发现 b 是定义在脚本中,这里的脚本就是 VG(G),而 a 是在 全局 GO 中,说明二者并不是一样的处理,let 词法解析阶段就已经明确在此上下文中未来一定会存在这些 let、const 声明的变量。
接下来我们看一个面试练习。
面试练习
console.log(fn);
function fn() {
console.log(1);
}
console.log(fn);
var fn = 12;
console.log(fn);
function fn(){
console.log(2);
}
console.log(fn);
第一步变量提升
- function fn() {console.log(1);} fn 函数声明加定义,一个函数的创建会在 Heap 堆内存中开辟一块空间(0x001)来存储函数(详情看上一篇文章:函数的底层执行机制),所以 fn -> 0x001。
- var fn = 12; var 只会声明不会定义,但是发现上下文中已经声明过 fn 了,所以不会重复声明。
- function fn(){console.log(2);},fn 现在是一个新的函数,但是发现在此上下文中已经存在 fn ,所以不会重复声明,但是会重新定义。所以开辟一块新的内存空间(0x002)来存储新的 fn 函数。对比第一步的 fn 内存地址是不一样的,开辟新的内存就是一个定义的操作,所以 fn -> 0x002。
第二步代码执行
- console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 0x002。所以输出 ƒ fn(){console.log(2);}
- function fn() {console.log(1);} 跳过,因为这一步在变量提升阶段已经处理过了。
- console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 0x002。所以输出 ƒ fn(){console.log(2);}
- var fn = 12; var 声明已经在变量提升阶段处理过,现在就是进行重新赋值。fn -> 12
- console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 12。所以输出 12。
- function fn(){console.log(2);} 跳过,因为这一步在变量提升阶段已经处理过了。
- console.log(fn); 在此上下文中查找 fn,发现存在,fn -> 12。所以输出 12。
总结
这类变量提升的题目,在面试中会经常出现,但是很多同学会认为比较简单,往往出问题,所以遇到这类题目,需要熟练的在大脑中构想一副脑图,明白内存中变量的存储指向。这样才能清楚无误的输出结果。