变量提升与函数提升

36 阅读6分钟

变量提升

我们先看一道比较经典的题:

// 分别输出什么?
var a = 1;
var b;
function fun() {
    console.log(a);
    console.log(b);
    var a = b = 3;
    console.log(a);
    console.log(b);
}
fun();
console.log(a);
console.log(b);

如果看到这道题你已经知道答案是什么并且知道为什么会是这个答案,那可以不必看这篇文章了,因为你也知道我会说些什么,但是如果不知道结果是什么或者说你输出的结果和自己设想的不一样,那就需要再看下去了。

这篇文章是基于上一篇所写的实践内容,如果还没看上一篇的那些概念,我强烈建议先去看一遍再回来这边看分析,那就会很容易理解这篇文章。

我们知道,作用域分为全局作用域和局部作用域,而一个函数查找变量的方式是先从自己的作用域去查找,如果没有它才去上一级的作用域去查找。

上式中他们的变量是这样的:

  1. 声明了ab两个变量,给a赋值为1,因为没有给b赋值,所以b的值为undefined
  2. 在函数fun()中,只声明了一个变量ab是全局变量,不要搞错了),并且给ab分别赋值为3

ok,分析完他们的变量之后咱们继续看函数里面的代码,他们首先分别输出ab,接着赋完值之后再次输入ab

错误的解

按照我们一般的思维,它的运行步骤应该是这样的:

  1. 一开始因为函数中ab还没有声明,所以会先去全局变量找,找到的结果是a = 1,b = undefined,所以函数里面前两行输出为1undefined
  2. 因为已经声明了a,并且a和全局的b一起被赋值为3,所以后面的输出结果是33
  3. 函数fun()执行完成之后开始执行最后两行,因为全局变量的a没有被重新赋值所以值还是1,而全局变量的b赋值为3了,所以最后的结果应该为13
  4. 综上所述,最终的输出结果应该为1,undefined,3,3,1,3

然而,实际的结果却是undefined,undefined,3,3,1,3

那为什么会导致这种结果呢?我们接着看下去。

正确的解

从我们上篇的执行上下文的生命周期中我们知道,函数在调用的时候会先创建变量对象,而创建变量对象的操作分别为

  1. 创建并初始化arguments对象
  2. 建立属性名为函数名的属性,属性值为函数的内存地址
  3. 建立属性名为变量名的属性,属性值为undefined

执行上下文的创建阶段完成之后开始进入执行阶段,然后就开始运行函数里面的代码。这时候就会发现,原来在一开始执行console.log(a);之前,所声明的a已经声明好,并且赋值为undefined了,而后面var a = b = 3;其实只是给ab赋值而已,所以上述的代码可以写为:

var a = 1;
var b = undefined;;
function fun() {
    var a = undefined;
    console.log(a);
    console.log(b);
    b = 3;
    a = b;
    console.log(a);
    console.log(b);
}
fun();
console.log(a);
console.log(b);

这样是不是很容易理解了呢?

像这样在运行之前先把声明的变量放到作用域顶部并且将它们赋值为undefined的方式我们称之为变量提升

函数提升

接下来我们再看一个例子:

// 分别输出什么?
console.log(fun);
fun();
function fun() {
    return 'fun函数提升测试'
}

从上面的学习中我们知道对于声明的函数,它会建立属性名为函数名的属性,属性值为函数的内存地址。 也就是说函数和变量总会先被提升到作用域的顶端然后才开始执行函数,所以fun()函数会被提升到全局作用域的顶部,我们就可以将上面的代码改成这样:

function fun() {
    return 'fun函数提升测试'
}
console.log(fun);
fun();

这样的代码我们就很容易知道它们运行的结果是什么了。

注意事项

到这里我们已经讲完了变量提升和函数提升的原理,总结一下就是创建执行上下文的时候会创建变量对象,而变量对象创建的时候会先把要声明的变量赋予默认值(undefined),要声明的函数的值则为它的引用。

如果遇到同名的函数或者变量JS的处理方式是这样的:

  1. 遇到同名函数声明,将变量环境中函数引用指向新的函数地址
  2. 遇到同名变量声明,则直接跳过该声明。

但是有一点我们需要注意:

console.log(fun);
fun();
var fun = function() {
    return 'fun函数提升测试'
}

取函数提升的例子,我们把函数的声明方式改成上面这样,它会先输出undefined然后报错。这是为什么呢?

其实问题就出现在函数的声明方式上,在函数提升的例子中我们是直接用function关键字声明一个具名函数,在进行函数提升的时候赋给它的值是这个函数的引用,而我们现在的例子是通过表达式声明的匿名函数,说白了就是将这个函数赋值给了一个变量,既然是变量,那在进行提升的时候就是进行的变量提升,因此会先赋予它默认值undefined。至于报错是因为undefined它不是一个函数,你以函数的方式去运行的它自然就会报错。

var,let,const

ES5以后我们用来声明变量的关键字就不止是var了,而且var现在越来越少被使用。但是这不是我们本篇文章要说的内容,我们要说的是这三个值在作用域内的变量提升。

首先我们先看一下这几个例子:

// 由于抛异常会停止代码运行,所以b和c选择性输出
console.log(a); // undefined
console.log(b); // Cannot access 'b' before initialization
console.log(c); // Cannot access 'c' before initialization
var a = 5;
let b = 5;
const c = 5;

从上面我们可以看到,当bc在声明前使用的话会报错,那这就说明使用letconst声明变量的话必须先声明然后再使用,不然会报错,用var则不用,因为变量提升的原因。但是这就说明letconst没有变量提升吗?我个人并不是这么认为的。

我们再看一下这个代码:

console.log(d); // d is not defined

因为d没有在任何地方被声明,因此使用的话会给出d未定义的错误,但是bc的提示却不是这样的。这就说明他们已经被提升了,但是没有赋值为undefined。而在运行到声明之前都无法使用。

var 不同,用 letconst 声明的变量不会被提升到作用域的顶部。它们在"暂时性死区"(Temporal Dead Zone,TDZ)中存在,从块的开始直到实际的声明语句。如果在声明之前访问 letconst 变量,将会导致 ReferenceError 错误。

在暂时性死区的时候使用那些变量就会报错,所以要求我们必须先声明变量然后再使用变量。