JavaScript变量提升运行机制

2,089 阅读4分钟

JavaScript的工作原理是,先解析代码,获取所有声明的变量或者函数,然后运行。这造成的结果就是所有声明的变量或者函数都被提升到代码的头部,这叫做声明提示提升。

先看一个换汤不换药的经典例子:这个例子大家其实都知道发生了变量提升导致\color{red}{console.log(a)};结果是undefined。问题来了,这个提升的过程在哪里发送?怎么发生?

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

一、JavaScript的执行过程

1. JavaScript在执行之前会经历一个编的过程(其他术语:创建、预解释过程)。

1.1 宏观的角度

  • 创建\color{red}{Scope chain}(包含变量对象,父级作用域上下文的变量对象)。
  • 创建\color{red}{variableObject}(参数、内部变量、函数声明)。
  • 设置\color{red}{this}
PS:变量对象的创建过程
  • 根据函数的参数,创建并初始化argument object。
  • 扫描函数内部的代码,查找函数声明。对于所有找到的函数名和函数引用,都会放到变量对象中。如果之前已经存在同名的函数,会进行覆盖。
  • 扫描函数内部的代码,找到变量声明,对于所有找到的变量申明,都放到变量对象中,并初始化为undefined,变量名称跟已经声明的参数或者是函数相同,就会被忽略,不会干扰到之前的声明。

1.2 微观的角度

  • \color{red}{分词/词法分析},这个过程就是将字符串分解为有意义的代码块,这些代码块我们称为词法单元。
  • \color{red}{解析/语法分析},这个过程是将词法单位抽象为抽象语法树AST。
  • \color{red}{代码生成}。将AST生成可以执行的代码。

2. 编译完之后会进行一个执行阶段。

2.1 执行:变量的值、函数的引用、执行代码。

1、\color{red}{变量的提升发生在编译的代码生成阶段析},例如在代码中var a,编译器会询问作用域中是否有一个该名称的变量存在。如果存在,就继续往下编译,如果不存在就会在当前作用域申明一个新的变量。

2、运行阶段,引擎发现代码a = 1的时候,首先会查询当前作用域是否是否有一个名为a的变量,如果存在就进行 赋值,\color{red}{如果没有就向上级执行,这个查询过程是在作用域链中完成}。大多数情况下,编译发生在代码执行之前的几微秒(甚至更短)。

二、ES6 let、const

ES新增的变量声明语法let与const。我们用let作为例子。

function f() {
  console.log(a);
  let a = 2;
}
f(); // ReferenceError: a is not defined

这段代码直接报错a is not defined,let和const拥有类似的特征,阻止了变量提升,当执行console.log(a)的时候变量没有定义,所以报错了。 在阮一峰老师的网站也写到let和const都是不存在变量提升的。如下

\color{red}{但是在最近的阅读中发现,有人对let真的不存在变量提升提出了疑问。},对此我也产生的疑问,发现在不同的权威机构有着不一样的解释。

  • MDN中写到:In ECMAScript 2015, let do not support Variable Hoisting, which means the declarations made using "let", do not move to the top of the execution block.

在MDN中认为let不存在变量提升

  • ECMA-262-13.3.1 Let and Const Declarations写到: 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.

这说明即使是 block 最后一行的 let 声明,也会影响 block 的第一行。这就是提升(hoisting)

  • ECMA-262: 8.2.1.2 Runtime Semantics: EvalDeclarationInstantiation( body, varEnv, lexEnv, strict)写到: The environment of with statements cannot contain any lexical declaration so it doesn't need to be checked for var/let hoisting conflicts.

这句话也间接的证明 let hoisting 的存在。\color{red}{在 ECMAScript 2015中, let 也会提升到语句块的顶部。但是,在这个语句块中,在变量声明之前引用这个变量会导致一个 ReferenceError的结果。}

那其实大家会有疑问,为什么上面的代码会报错。其实这并不是由于变量不提示导致的,而是由于TDZ(临时性死区)导致的。

在举个例子:
{
    a = 2;
    let a;
}
这段代码可以解释为:
{
    let a;// 变量提升
    "start TDZ"
    a = 2; // 这里在TDZ中间,所以会导致a = 2 报错
    a;
    "end TDZ"
}

所以破案了:let是存在变量提升。它“变量提升的行为”,是由于TDZ导致的。

so...总结一下

  • let 声明会提升到块顶部
  • 从块顶部到该变量的初始化语句,这块区域叫做 TDZ(临时死区)
  • 如果你在 TDZ 内使用该变量,JS 就会报错,注意TDZ 跟 hoisting不等价。