JavaScript变量与函数提升

307 阅读4分钟

作者:小斯基@毛豆前端

本文要点:

  • 函数提升;
  • var,let,const三种方式变量提升的区别;

首先看一些大家熟悉的代码:
code1:

// foo调用在定义之前
foo();
function foo() {
  console.log('hello world');
}
// hello world

code2:

// 报错
foo();
var foo = function() {
  console.log('hello world');
}
// TypeError: foo is not a function

code3:

// 不报错
console.log(`a is ${a}`);
var a = 'hello world'
// a is undefined

code4:

// 报错
console.log(`a is ${a}`);
let a = 'hello world'
// ReferenceError: a is not defined

code5:

// 报错
console.log(`a is ${a}`);
const a = 'hello world'
// ReferenceError: a is not defined

下面进入正题: JS引擎会在正式执行之前先进行一些预处理,在这个过程中,首先将变量定义和函数定义的各个阶段操作(创建、初始化和赋值)提升至当前作用域的顶端,然后进行接下来的处理。(注:由于引擎的不同,预处理也会有所差异)。

code1和code2比较好理解。code1中函数定义会被提升到当前作用域顶部,然后再做后续执行,即foo函数的定义被js引擎自动提升到foo()前面,所以可以正常调用。而code2中,foo实际上是一个变量定义,然后将foo变量指向一个匿名函数, 那既然变量定义也会有提升,为何会报错呢?

code3的变量提升理解起来也比较简单,但是code4,code5中分别使用let、const抛出了错误,看来js对var、let、const三种定义方式的变量提升有所区别。下面将详细讲解:

js变量和函数定义有三个阶段: 创建create、初始化initialize 和赋值assign,针对不同的定义对不同的阶段做提升:

  1. 函数定义的创建、初始化和赋值三个阶段都被提升了;
  2. var定义的创建、初始化被提升了,赋值未被提升;
  3. let的创建被提升了,但初始化和赋值都未被提升;
  4. const的创建被提升了,初始化未被提升,特殊的一点是const没有赋值阶段,所以const定义的变量值不能改变;

现在我们详细梳理下上述代码:

  1. code1中函数定义的三个阶段都被提升,所以是先创建变量foo(此时还无法使用foo),然后将foo初始化为undefined,再将foo赋值为函数体,最后执行foo();
  2. code2中是var变量定义,先创建foo,然后将其赋值为undefined,之后调用foo(),所以这里会报错;
  3. code3和code2类似,foo在初始化后执行console.log,所以打印出undefined;
  4. code4中是let变量定义,先创建foo,由于初始化未被提升,所以创建之后立即执行console.log,于是就报错了;
  5. code5与code4报错过程类似。

现在我们理解了js对各种变量和函数定义做提升时的区别。那么变量和函数定义的提升有没有优先级顺序呢?答案是有的。请看如下代码:

console.log(foo);
function foo() {
  console.log('hello world');
}

var foo = 1;
console.log(foo);

// [Function: foo]
// 1

这段代码可解释为: 首先找到js引擎先找到var定义语句,创建变量foo,然后将其初始化为undefined,之后找到function定义语句,重新创建foo,之后将其赋值为函数体(因为function的赋值过程也被提升了),然后执行第一个console.log,接下来 才执行foo变量的赋值过程(因为var定义的赋值过程未被提升),最后执行第二个console.log。若将代码中var换成let,将会抛出SyntaxError: Identifier 'foo' has already been declared。因为let变量定义不允许有第二次 创建过程, function定义的创建过程就抛错了。

关于let还有一个很有意思的现象,假如我们执行如下代码:

let x = x; // x之前未定义过
// Uncaught ReferenceError: Cannot access 'x' before initialization

x = 1;
// Uncaught ReferenceError: x is not defined

在抛出一段错误之后会发现,在当前上下文中不能再使用名为x的变量了,也不能对x赋值。导致这个现象的原因大概是: let定义首先创建x,之后执行代码,发现let x = x等号右边的x对x产生了引用,由于变量初始化之前就被引用了,所以抛出Cannot access 'x' before initialization,并且这个错误会导致let x语句在创建x后初始化失败(前面的 错误导致不能进入初始化过程),由于js变量定义的初始化过程只有一次,一旦失败就不能再次初始化了,此时的x处于一个已创建但又不能初始化的状态,即所谓的暂时性死区, 此后对x的引用或赋值都会抛出错误。

另外,ES6中的class声明也存在提升,但是它和let、const一样,有一些限制,如果在声明位置之前引用,会抛出一个异常。

最后提醒大家在写js代码过程中,尽量遵循一点: 先声明,后使用