深入理解变量提升、函数提升和暂时性死区

788 阅读5分钟

变量提升和函数提升

JS中可以再变量和函数声明之前使用它们,这就称作变量提升/函数提升。


sayHi() // Hi there!

function sayHi() {
    console.log("Hi there!")
}

name = 'John Doe'
console.log(name)
var name

但实际上js并不会移动代码,作为单线程语言js肯定是按顺序执行。

但这里按顺序并不是逐行分析执行,而是一段一段的进行,并且会先进行编译阶段再到执行阶段。

在编译阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到Lexial Environment(词法环境)的JS数据结构内存中。所以这些变量才能在被真正声明之前使用。

变量提升

var变量提升

console.log(name) // 'undefined'
var name = 'John Doe'
console.log(name) // John Doe

这段代码实际上分为两个部分:

  • var name 表示声明变量 name
  • = 'John Doe' 表示为变量 name赋值为'Jogn Doe'

只有声明操作var name会被提升,而赋值这个操作并不会被提升,但是为什么变量name的值会是undefined呢? 原因是当JavaScript在编译阶段会找到var关键字声明的变量会添加到词法环境中,并初始化一个值undefined,在之后执行代码到赋值语句时,会把值赋值到这个变量。 过程如下:

// 编译阶段
lexicalEnvironment = {
  name: undefined
}

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

因此用var定义的函数表达式也不会被提升,如下helloworld是一个默认值为undefined的变量,而不是一个function对象:

helloWorld();  // TypeError: helloWorld is not a function

var helloWorld = function(){
  console.log('Hello World!');
}

let const 变量提升和暂时性死区

那么我们来看一下使用let和const定义的变量是否会发生变量提升呢?

console.log(a)  // ReferenceError: a is not defined
let a = 3

为什么会报一个ReferenceError错误,难道let和const声明的变量没有被提升吗?

实际上所有的声明(function, var, let, const, class)等都会被提升,但是只有用var关键字声明的变量才会被初始化undefined值,而let和const声明的变量则不会被初始化值。

只有在执行阶段js引擎遇到他们的赋值操作时,他们才会被初始化。也就是说使用let、const声明的变量在声明之前是无法访问的,这就是暂时性死区,即let和const声明的变量在声明之前无法访问。

如果JavaScript引擎在执行阶段let和const变量被声明的地方还找不到值的话,就会被赋值为undefined或者返回一个错误(const的情况下)。

let a
console.log(a)  // undefined
a = 5

在编译阶段,JavaScript引擎遇到变量a并将它存到词法环境中,但因为使用let关键字声明的,JavaScript引擎并不会为它初始化值,所以在编译阶段,此刻的词法环境像这样:

lexicalEnvironment = {
  a: <uninitialized>
}

如果我们要在变量声明之前使用变量,JavaScript引擎会从词法环境中获取变量的值,但是变量此时还是uninitialized状态,所以会返回一个错误ReferenceError。

在执行阶段,当JavaScript引擎执行到变量被声明的时候,如果声明了变量并赋值,会更新词法环境中的值,如果只是声明了变量没有被赋值,那么JavaScript引擎会给变量赋值为undefined。

tips: 我们可以在let和const声明之前使用他们,只要代码不是在变量声明之前执行:

function foo() {
    console.log(name)
}

let name = 'John Doe'

foo()   // 'John Doe'

函数提升

  • 函数声明和初始化都会被提升
console.log(square(5)); // 25
function square(n) {
  return n * n;
}
  • 函数表达式不会被提升
console.log(square); // undefined
console.log(square(5)); // square is not a function
var square = function (n) { 
  return n * n; 
}
function hoistFunction() {
  foo(); // 2
  var foo = function() {
    console.log(1);
  };
  foo(); // 1
  function foo() {
    console.log(2);
  }
  foo(); // 1
}

hoistFunction();

优先级

函数提升在变量提升之前

  1. 变量的问题,莫过于声明和赋值两个步骤,而这两个步骤是分开的。
  2. 函数声明被提升时,声明和赋值两个步骤都会被提升,而普通变量却只能提升声明步骤,而不能提升赋值步骤。
  3. 变量被提升过后,先对提升上来的所有对象统一执行一遍声明步骤,然后再对变量执行一次赋值步骤。而执行赋值步骤时,会优先执行函数变量的赋值步骤,再执行普通变量的赋值步骤。

例子1

typeof a; // function
function a () {}
var a;
// typeof a; // function  =》无论放在前面还是后面,解析后执行顺序都是一样

编译后

function a  // => 声明一个function a
var a       // =》 声明一个变量 a
a = () {}   // => function a 初始化
typeof a;   // function

例子2

function b(){};
var b = 11;
typeof b; // number

编译后

function b;  // => 声明一个function b
var b;       // =》 声明一个变量 b
b = (){};    // =》 function b 初始化
b = 11;      // =》 变量 b 初始化 =》变量初始化没有被提升,还在原位
typeof b;    // number

例子3

var foo = 'hello';
(function(foo){
  console.log(foo);
  var foo = foo || 'world';
  console.log(foo);
})(foo);
console.log(foo);
// 依次输出 hello hello hello

编译后

var foo = 'hello';
(function (foo) {
    var foo;  // undefined;
    foo= 'hello'; //传入的foo的值
    console.log(foo); // hello
    foo = foo || 'world';// 因为foo有值所以没有赋值world
    console.log(foo); //hello
})(foo);
console.log(foo);// hello,打印的是var foo = 'hello' 的值(变量作用域)