js单线程为啥有变量提升

68 阅读6分钟

表现形式

JavaScript中奇怪的一点是你可以在变量和函数声明之前使用它们。就好像是变量声明和函数声明被提升了代码的顶部一样。

然而JavaScript并不会移动你的代码,所以JavaScript中“变量提升”并不是真正意义上的“提升”。

无论在函数的何处声明的变量,都会被提升至函数的开头部分,可以在变量声明前访问但不会报错。

JavaScript是单线程语言,所以执行肯定是按顺序执行。但是并不是逐行的分析和执行,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。

sayHi() // Hi there!

function sayHi() {
    console.log('Hi there!')
}
name = 'John Doe'
console.log(name)   // John Doe
var name

变量提升的定义

变量提升实际上很容易理解,就是说在任何位置所声明的变量或函数,都会自动“提”到最前面,就好像它们是在函数的开头声明的一样。

造成变量提升的原因?

首先我们要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。

造成变量提升的本质原因是因为JavaScript引擎在代码执行前会有一个解析的过程,创建执行的上下文,初始化一些代码在执行时所需要使用的对象。

在访问一个变量时,会在当前的执行上下文中的作用域中去查找,然而作用域的首端指向的是当前执行的上下文中的变量,这个变量是当前执行上下文的一个属性,这个变量包含了函数的形参,所有的函数和变量的声明,这个变量就会在代码解析的时候进行创建。

解析阶段/编译阶段

在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,this,arguments

在编译阶段阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到名为Lexical Environment的JavaScript数据结构内的内存中。所以这些变量和函数能在它们真正被声明之前使用。

JavaScript会检查语法,对函数进行预编译。创建全局执行上下文->将即将要执行的变量,函数声明都拿出来且赋值为undefined

在函数执行之前,仍会创建一个类似全局的执行上下文一样的函数执行上下文,在此时,函数将会多出thisarguments,函数的参数。

  • 函数提升
sayHi() // Hi there!
function sayHi() {
    console.log('Hi there!')
}

因为函数声明在编译阶段会被添加到词法环境(Lexical Environment)中,当JavaScript引擎遇到sayHi()函数时,它会从词法环境中找到这个函数并执行它。

lexicalEnvironment = {
  sayHi: < func >
}
  • 变量提升
console.log(name)   // 'undefined'
var name = 'John Doe'
console.log(name)   // John Doe

**只有声明操作var name会被提升,而赋值这个操作并不会被提升,**但是为什么变量name的值会是undefined呢?

原因是当JavaScript在编译阶段会找到var关键字声明的变量会添加到词法环境中,并初始化一个值undefined,在之后执行代码到赋值语句时,会把值赋值到这个变量。

// 编译阶段
lexicalEnvironment = {
  name: undefined
}
// 执行阶段
lexicalEnvironment = {
  name: 'John Doe'
}

所以函数表达式也不会被“提升”。helloWorld是一个默认值是undefined的变量,而不是一个function

helloWorld();  // TypeError: helloWorld is not a function
var helloWorld = function(){
  console.log('Hello World!');
}

注:变量提升只是提升变量声明的部分,并不会提升变量赋值的部分。

在执行阶段

按照代码的顺序进行依次执行。

那么为什么会有变量提升?

  • 提高性能

因为在代码执行会进行一次代码检查和预编译(只进行一次),这样就不需要在函数变量每次执行的时候都再进行一次代码检查和重新解析。

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

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

  • 容错了提升

例如下面代码,正是因为变量的提示在执行时就不会进行报错。

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

解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

变量提升导致的问题

变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。

例1:

在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。

var tmp = new Date();

function fn(){
  console.log(tmp);
  if(false){
    var tmp = 'hello world';
  }
}

fn();  // undefined

例2:

由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。

var tmp = 'hello world';

for (var i = 0; i < tmp.length; i++) {
  console.log(tmp[i]);
}

console.log(i); // 11