重新认识变量提升
在聊变量提升之前,我们可以先看一个大家或许熟悉,或许不熟悉的现象
console.log(a); // 输出undefinde var a = 10;
上述例子中我们可以看到,运行结果是undefined,而这似乎和我们理解的JavaScript代码执行是按顺序执行的有所出入,按理说JavaScript是解释执行的,在进行console.log(a)之前并没有对a进行声明,为什么并没有报错而是输出了undefined而不是报错为声明呢?这里就涉及到了变量的提升。
1. 理解声明和赋值
在解释变量提升之前,还有一些事情非常值得去解释一下,首先便是变量声明和赋值。想想我们平时写代码的时候,创建一个值为10的变量是不是像这样呢
var a = 10;
然而上述代码实际上执行了两个过程,首先我们对变量a进行声明var a,然后才对变量a进行了赋值操作a = 10,我们在举一个例子,下面例子中对函数的声明赋值实际上也是两个过程。
// 函数声明,但不涉及赋值 function show() { console.log('show 执行了'); } // 函数声明赋值 var show = function () { console.log('show 执行了'); }; /* 上述过程相当于相当于 var show; show = function () { console.log('show 执行了'); }; */
现在我们知道了,原来我们之前写的创建一个变量的代码,实际上是把变量声明和变量赋值整合在一起了,现在我们可以来解释变量提升了。
2. 变量提升
首先我们需要知道一点,虽然JavaScript是解释执行,但实际上在执行之前,还会有一个编译过程,这个过程会收集代码中的变量和函数的声明并放在代码的开头,且会对其设定默认值,也就是undefined,因此我们可以解释最开始的例子,为什么会输出undefined了,实际上运行结果像下面这样
var a = undefined; console.log(a); // 输出undefined a = 10;
我们接下来看看加上函数声明的例子
console.log(a); // undefined show(); // show function show() { console.log('show'); } var a = 10;
上述代码执行结果如代码注释,其中函数声明部分也是被提升到了代码最开始,因而可以把代码分为两个部分
// 声明 var a = undefined; function show() { console.log('show'); } // 执行 console.log(a); show(); a = 10;
这样就很好解释,为什么输出会是undefined和show了,因为输出a的时候还没有对其赋值,而执行函数show的时候,已经完整声明了,自然也能够正常执行输出了。
我们再来实验一下,假设我们对函数不是采用函数声明,而是用变量声明赋值的方式会是怎样呢?
console.log(a); show(); var show = function () { console.log('show'); }; var a = 10;
聪明的读者会发现,上述代码执行到show()时就会报错,原因其实就是因为在执行show()时,变量show = undefined而会报错提示show is not a function。
至此,我们已经理解了变量提升以及为什么会有变量提升,主要原因就是因为在执行代码之前,引擎会对代码中的变量进行收集并放到执行代码之前,并对其赋值为undefined,实际上,变量的声明会被放入一个叫执行上下文的地方,下面我们来看看执行上下文是什么。
3. 执行上下文
一段代码经过编译,会生成执行上下文和可执行代码,而可执行上下文可以看成一个环境,这个环境里保存了执行代码所需的变量声明、函数声明、this(所以说this其实是执行上下文中的一个属性)等信息。例如我们在之前提到的var声明的变量就会被放入执行上下文中的变量环境里,而对于我们还没有提及到的例如const, let声明的变量则会被放入词法环境中以支持块级作用域,大概就像下面这个图
![](https://uploadfiles.nowcoder.com/files/20220716/790545723_1657958871595/1657954925751-4d44b5c0-ee73-40ea-88c2-53f7725cc1c0.png)
注意:实际上let, const声明的变量依然会进行提升,不过它们并不会被赋予初始值,因而如果在赋值之前进行访问,会抛出ReferenceError,此部分说明可以参考mdn的描述
Variables declared with let and const are also hoisted but, unlike var, are not initialized with a default value. An exception will be thrown if a variable declared with let or const is read before it is initialized.let和const变量声明依然会被提升,然而与var不同的是,它们不会被赋予一个默认值进行初始化。如果一个let或者const变量在初始化之前被访问,则会抛出一个异常(ReferenceError)
例如
console.log(a); // ReferenceError: Cannot access 'a' before initialization let a = 10;
4. 实践面试题
学以致用,我们以一个面试题为例,来对变量提升进行一次过程分析,再次巩固一下今天的内容。
console.log('1', fn()); function fn() { console.log(1); } console.log('2', fn()); function fn() { console.log(2); } console.log('3', fn()); var fn = 'hello'; console.log('4', fn()); function fn() { console.log(3); }
我们来一步一步分析下
- 编译阶段:函数声明和变量声明会依次执行,并被放入执行上下文的变量环境中,首先拿到console.log(1);的函数声明fn;接着拿到console.log(2);的函数声明fn,但是此时变量环境中有了,于是第二个fn函数覆盖掉原有的声明;接着发现var fn声明,于是用初始值undefined初始化后放入环境变量,并覆盖原有的fn声明;最后一个fn同理,覆盖掉var fn = undefined。
- 执行阶段:执行阶段代码实际只有这么几句了
console.log('1', fn()); console.log('2', fn()); console.log('3', fn()); fn = 'hello'; console.log('4', fn());
- 执行第一行,首先执行fn()输出3,函数返回undefined,然后执行console.log('1', fn());,输出1 undefined
- 执行第二行,同上,输出3,接着输出2 undefined
- 执行第三行,同上,输出3,接着输出3 undefined
- 执行第四行,将fn赋值为hello
- 执行第五行,首先执行fn(),由于fn此时为字符串的变量,因此执行报错fn is not a function,执行结束
- 执行结果如下
3 1 undefined 3 2 undefined 3 3 undefined TypeError: fn is not a function
5. 总结
- 对于JavaScript的代码执行前,引擎会执行编译,此阶段将收集变量声明和函数声明,并将它们放入执行上下文中,var声明的变量和函数声明会放入变量环境中,而let,const声明变量会放入词法环境中。
- 变量声明和赋值是两个不同的过程,声明阶段var变量会被初始化为undefined,而let,const声明变量不会初始化,在赋值前访问会报错。
- 变量提升指的是在编译阶段声明的变量和函数,会被提升到执行代码之前。