变量提升

243 阅读10分钟

变量提升的现象

查看下面的代码:

console.log('num:', num);
var num = 1;
add();
function add() {
  console.log('num + 1:', num + 1);
  return num + 1;
}

猜一下上面执行的结果是什么?

如果你有学习其他语言的经历或者按照逻辑猜测,很可能认为上面的代码会报错,因为在变量num还没有声明,但是第一行就打印了num变量,同理当第四行add函数执行时,还没有进行声明。

但是上面代码可以执行,并且执行的结果是下面:

num: undefined
num + 12

为什么会出现这种结果哪?这就不得不提一下js的一种特殊现象”变量提升“。

变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量声明和函数声明提升到当前作用域最前面的行为。

下面的代码效果与上面的代码是相等的:

function add() {
  console.log('num + 1:', num + 1);
  return num + 1;
}
var num;
console.log('num:', num);
num = 1;
add();

注意:看起来变量和函数的声明会在物理层面移动到代码的最前面,但这只是帮助理解的例子。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。

这里有两个小问题

  1. 当变量声明和函数声明同时存在时,哪个先被提升?
  2. 函数表达式是否会被提升?

对于第1个问题,可以写一个例子进行验证:

console.log('add', add);
console.log('add', add());
var add = 1;
console.log('add', add);
function add() {
  console.log('add1');
}
function add() {
  console.log('add2');
}
var add = 2;
console.log('add', add);

这里先说一下”变量提升“相关的知识点,变量声明只会提升声明不会提升赋值操作,所以变量提升后,值默认是undefined;而函数声明会整个提升。另外如果变量声明或函数声明如果同名,则后面的声明会忽略。

上面的代码如果console打印的是函数,则证明优先提升函数声明,否则就是优先提升变量声明。最后执行的结果:

add [Function: add]
add2
add 1
add 2

证明”函数声明提升优先级高于变量声明,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。 “。

对于第2个问题,看一下下面代码,区分函数声明和函数表达式:

// 函数声明
function fn1() {}
// 函数表达式
var fn2 = function () {}

很明显函数表达式是变量声明的赋值操作,而变量提升只会提升变量声明而不会提升赋值操作,所以函数表达式不会被提升

下面总结变量提升的过程:

  1. 在当前作用域查找所有的变量声明和函数声明,把这些声明都提到最前面,函数声明在变量声明之前。
  2. 如果函数声明或者变量声明存在同名的,则同名的函数声明或者同名变量声明都是后者覆盖前者,但是变量声明和函数声明同名,变量声明不能覆盖函数声明。

变量提升的优点

看下面的代码:

function isEven(n) {
  if (n === 0) {
    return true;
  }
  
  return isOdd(n - 1);
}
alert(isEven(2));
function isOdd(n) {
  if (n === 0) {
    return true;
  }
  
  return isEven(n - 1);
}

假设没有变量提升,那么代码会怎么执行哪?

  1. 定义函数isEven
  2. 调用isEven函数。
  3. isEven函数执行的结果作为alert函数参数,执行alert函数。
  4. 定义isOdd函数。

但是在执行到第2步就报错了,因为在isEven函数中调用了isOdd函数,可以此时isOdd函数还没有定义,此时并不能使用。

如何解决这个问题哪?

其中的一个方案就是在执行前就在定义这些声明函数,这样代码执行时就不需要关心函数或者变量声明的顺序了,这就是变量提升

同时为了避免每次执行都需要进行这样的操作,JS引擎对此进行了优化,即在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。

再看一个demo:

var n = 100;
 
while (n-- > 0) {
  var foo = n;
}

如果没有变量提升,则会定义100此变量foo,其实并不需要定义这么多次。而因为变量提升的存在,代码等同于:

var n = 100;
var foo;
 
while (n-- > 0) {
  foo = n;
}

只定义了一次变量foo,对比没有变量提升来说,性能提升的很明显。

再看一个demo:

a = 1;
var a;
console.log(a); // 1

如果没有变量提升,上面的代码无疑会报错,但是有了变量提升,代码就可以正常执行了,虽然这是副作用,但是无疑提高了代码的容错性

总结变量提升的优点:

  1. 解决函数相互依赖问题。
  2. 提高编译性能。
  3. 提高代码容错性。

变量提升的缺点

查看下面的代码:

function showName(){
  var name = "JavaScript"
  // ...其他代码...
  
  // 新增代码开始
  if(true){
    var name = "CSS"
    console.log(name);
  }
  // 新增代码结束
  
  console.log(name);
  
  // 执行结果:
  // CSS
  // CSS
}
showName()

假设你在showName函数中新增一段代码,但是这段代码中你重新定义了name变量,新增的代码执行的结果符合预期,但是原来底部打印的结果却是变了。

再看一个例子:

function foo(){
  for (var i = 0; i < 5; i++) {
  }
  console.log(i); 
}
foo()

循环执行完毕后,依然可以打印变量i,变量没有被及时销毁。

总结变量提升的缺点:

  1. 变量提升导致使用变量或者函数时要特别注意变量是否重名和执行的时机,而且随着代码增加,风险越来越大,增加了开发成本和维护成本。
  2. 变量提升扩大了变量和函数的使用范围,由于ES6之前不存在块级作用域,所以在for循环中声明的变量或函数在执行完毕后依然可以使用。(不仅是for循环,while循环,try,finally等也存在这种情况)

解决变量提升

在上面的一节已经说明变量提升的缺点,那么如何解决这个问题哪?

ES6给出的方案是使用letconst声明变量,查看下面代码:

function showName(){
  let name = "JavaScript"
  
  // ...其他代码...
  
  // 新增代码开始
  if(true){
    let name = "CSS"
    console.log(name);
  }
  // 新增代码结束
  
  console.log(name);
}
showName()
​
// 执行结果:
// CSS
// JavaScript

可以看到执行结果是正确的,if语句内部执行的代码没有影响到外部的name变量。

letconst声明变量可以如何做到的哪?简单来说使用letconst声明变量会生成块级作用域。

可以先看一下ES5对于var变量的定义

Every execution context has an associated VariableEnvironment. Variables and functions declared in ECMAScript code evaluated in an execution context are added as bindings in that VariableEnvironment’s Environment Record. For function code, parameters are also added as bindings to that Environment Record.、

A variable statement declares variables that are created as defined in 10.5. Variables are initialised to undefined when created. A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.

翻译过来:

每个执行上下文都关联一个变量环境。ES6之前的代码执行时会把变量声明或者函数声明绑定到一个变量环境的环境记录。对于函数代码,参数也会被绑定到这个环境记录。

一个变量声明语句的变量在定义时就会在执行上下文的变量环境被创建。变量在创建时被初始化为undefined。变量的初始化器被赋值表达式分配值是在变量声明执行时而不是创建时。

然后看一下ES6对于let和const的定义

let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

翻译过来:

let和const声明定义的变量属于执行上下文的词法环境作用域。当这些变量包含的词法环境被实例化后才会被创建,但是在这些变量执行词法绑定之前不能被访问。当词法绑定执行时,带着初始化器的词法绑定定义的变量会被初始化赋值表达式赋值分配值,而不是变量被创建的时候。如果一个词法绑定的let表达式声明的变量时没有初始化,则词法绑定执行时默认分配一个undefined作为变量的值。

Table 23 — Additional State Components for ECMAScript Code Execution Contexts

ComponentPurpose
LexicalEnvironmentIdentifies the Lexical Environment used to resolve identifier references made by code within this execution context.
VariableEnvironmentIdentifies the Lexical Environment whose EnvironmentRecord holds bindings created by VariableStatements within this execution context.

The LexicalEnvironment and VariableEnvironment components of an execution context are always Lexical Environments. When an execution context is created its LexicalEnvironment and VariableEnvironment components initially have the same value.

在 ES6 中,LexicalEnvironment 组件和 VariableEnvironment 组件的区别在于前者用于存储函数声明和变量( letconst )绑定,而后者仅用于存储变量( var )绑定。

变量环境只有全局和函数作用域词法环境则是有全局、块、函数

上面两种声明方式的区别在于

  1. 变量提升中的变量声明都是在变量环境中创建,而let和const声明的变量在词法环境创建。
  2. 变量提升中的变量声明在定义时就被初始化了,而let和const声明只有执行时才会被初始化,如果let默认没有赋值,则初始化分类一个undefined的值,并且如果没有被词法绑定前是不能访问的。
  3. let和const支持块级作用域,而var不支持块级作用域。

那下面的demo举例:

function foo(){
    var a = 1
    let b = 2
    {
      let b = 3
      var c = 4
      let d = 5
      console.log(a)
      console.log(b)
    }
    console.log(b) 
    console.log(c)
    console.log(d)
}   
foo()

第一步:编译并创建执行上下文

  1. 编译并创建上下文。
  2. 通过 var 声明的变量,在编译阶段会被存放到变量环境中,并初始化为undefined。
  3. 通过 let 声明的变量,在编译阶段会被存放到词法环境中,不进行初始化。
  4. 此时函数内部的块级作用域的let声明的变量并没有在编译阶段放到词法环境中。

第二步:继续执行代码

  1. 当执行到代码块里面时,变量环境中a的值已经被设置成了1,词法环境中b的值已经被设置成了2。
  2. 当进入函数的块级作用域块时,作用域块中通过let声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量b,在该作用域块内部也声明了变量b,当执行到作用域内部时,它们都是独立的存在。
  3. 在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量(通过let或者const声明),进入一个作用域块后,就会把该作用域块内部的变量((通过let或者const声明))压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构

第三步:打印

  1. 当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量a的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给JavaScript引擎,如果没有查找到,那么继续在变量环境中查找。

第四步:执行完后

  1. 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出

    console.log(d):Uncaught ReferenceError

  2. 块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript引擎也就同时支持了变量提升和块级作用域了。

暂时性死区

查看下面的代码:

{
  console.log(bestFood);
  let bestFood = "Vegetable Fried Rice";
}

console.log执行结果是什么?undefined还是其他?

$ node es/TDZ.jsReferenceError: Cannot access 'bestFood' before initialization

结果是抛出错误。但是为什么抛出错误哪?答案是暂时性死区temporal dead zone (TDZ) )。

先看一下TDZ的定义:

A temporal dead zone (TDZ) is the area of a block where a variable is inaccessible until the moment the computer completely initializes it with a value.

  • A block is a pair of braces ({...}) used to group multiple statements.
  • Initialization occurs when you assign an initial value to a variable.

Suppose you attempt to access a variable before its complete initialization. In such a case, JavaScript will throw a ReferenceError.

TDZ是代码块的一个区域,这个区域包含从花括号开始到变量被初始化的一段空间,在这段空间中不允许访问变量,如果在这段空间尝试访问变量,则会抛出一个引用错误。

这里需要注意几点:

  1. var声明的变量没有暂时性死区。

    {
      // undefined
      console.log(bestFood);
      var bestFood = "Vegetable Fried Rice";
    }
    
  2. let初始化不一定需要赋值,不赋值默认是undefined。

    {
      let bestFood;
      // undefined
      console.log(bestFood);
      bestFood = "Vegetable Fried Rice";
      // Vegetable Fried Rice
      console.log(bestFood);
    }
    

参考资料