JavaScript 变量提升

193 阅读4分钟

变量提升

从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。

 首先我们要知道的是javaScript引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行的运行。
 就是说有两个阶段,编译阶段和执行阶段
 

解析代码是一段一段地编译,编译完了之后才会到执行阶段。我理解的是,一个代码块{}中包含的代码就是一段,里面的变量都是先编译后执行。在编译阶段阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到名为[Lexical Environment]词汇环境的JavaScript数据结构内的内存中。所以这些变量和函数能在它们真正被声明之前使用。

let关键字

  /**
   变量没有声明
  */
  console.log(a);//Uncaught ReferenceError: x is not defined
-----------------------------------------------------------------------------------------------
  /**
   如果JavaScript引擎在`let`和`const`变量被声明的地方还找不到值的话,就会被赋值为`undefined`或者返回一个错误(`const`的情况下)
  */
  let a;
  console.log(a);//undefined
-----------------------------------------------------------------------------------------------
  /**
   在编译阶段,JavaScript引擎遇到变量`a`并将它存到词法环境中,但因为使用`let`关键字声明的,JavaScript引擎并不会为它初始化值,如果我们要在变量声明之前使用变量,JavaScript引擎会从词法环境中获取变量的值,但是变量此时还是`uninitialized`状态,所以会返回一个错误`ReferenceError`。
  */
  console.log(a);
  let a;//Uncaught ReferenceError: Cannot access 'x' before initialization
-----------------------------------------------------------------------------------------------

var关键字

  /**
   变量没有声明
  */
  console.log(a);//Uncaught ReferenceError: x is not defined
-----------------------------------------------------------------------------------------------
  /**
   当JavaScript在编译阶段会找到`var`关键字声明的变量会添加到词法环境中,并初始化一个值`undefined`,在之后执行代码到赋值语句时,会把值赋值到这个变量。
  */
  let a;
  console.log(a);//undefined
-----------------------------------------------------------------------------------------------
  /**
  当JavaScript在编译阶段会找到`var`关键字声明的变量会添加到词法环境中,并初始化一个值`undefined`,在之后执行代码到赋值语句时,会把值赋值到这个变量。 
  */
  console.log(a);
  var a;//undefined

理解ES6的块级作用域

块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至一个单独的{}都可以被看作是一个块级作用域(注意,对象声明中的{}不是块级作用域)。简单来说,如果一种语言支持块级作用域,那么其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁

变量提升的好处:提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。

最后总结

  1. let有变量提升,暂时性死区就是变量提升的证明,因为变量提升(编译阶段将该变量放在了内存中)为let的变量放在词法环境的数据结构中,并给了一个uninitialized的状态标记,就是未初始化的标记,如果提前访问该变量,就会返回Uncaught ReferenceError: Cannot access 'a' before initialization的错误
LexicalEnvironment: { 
    EnvironmentRecord:{ 
    Type: "Object", // 在这里绑定标识符 
    a: < uninitialized >, 
    outer: <null> 
},

2.let的暂时性死区Temporal Dead Zone(TDZ)中不能访问未初始化的变量,只有在赋值之后,才会初始化变量,而且执行阶段执行到变量被声明的时候,如果还没有被赋值,就会自动赋值undefined给该变量.简单地说,就是let声明的变量只有在赋值(主动赋值或者自动赋值)之后才会被访问.

3.事实上所有的声明(function, var, let, const, class)都会被"提升"