重新认识变量提升


在聊变量提升之前,我们可以先看一个大家或许熟悉,或许不熟悉的现象

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;

这样就很好解释,为什么输出会是undefinedshow了,因为输出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声明的变量则会被放入词法环境中以支持块级作用域,大概就像下面这个图

图1 执行上下文(图片来源《浏览器工作原理与实践》)

注意:实际上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);
}

我们来一步一步分析下

  1. 编译阶段:函数声明和变量声明会依次执行,并被放入执行上下文的变量环境中,首先拿到console.log(1);的函数声明fn;接着拿到console.log(2);的函数声明fn,但是此时变量环境中有了,于是第二个fn函数覆盖掉原有的声明;接着发现var fn声明,于是用初始值undefined初始化后放入环境变量,并覆盖原有的fn声明;最后一个fn同理,覆盖掉var fn = undefined
  2. 执行阶段:执行阶段代码实际只有这么几句了
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. 总结

  1. 对于JavaScript的代码执行前,引擎会执行编译,此阶段将收集变量声明和函数声明,并将它们放入执行上下文中,var声明的变量和函数声明会放入变量环境中,而let,const声明变量会放入词法环境中。
  2. 变量声明和赋值是两个不同的过程,声明阶段var变量会被初始化为undefined,而let,const声明变量不会初始化,在赋值前访问会报错。
  3. 变量提升指的是在编译阶段声明的变量和函数,会被提升到执行代码之前。
#前端#
全部评论
讲得很细,易懂,学费了。有个问题是如果提升的函数声明与变量同名,那么哪个优先级高呢?
点赞 回复 分享
发布于 2022-07-16 16:36
JavaScript有编译过程为啥不跟java一样叫编译型语言
点赞 回复 分享
发布于 2022-07-16 16:41

相关推荐

起名字真难233:人家只有找猴子的预算,来个齐天大圣他们驾驭不住呀😂😂
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
10
6
分享

创作者周榜

更多
牛客网
牛客企业服务