变量提升和函数声明提升

本文最后更新于:1 年前

变量提升和函数声明提升

关于 Javascript 中的变量提升(variable hoisting)可能在前端界已经耳熟能详了,可能你会觉得设计初衷是什么呢,或者这里有出于什么考虑以及解决了什么问题呢?因为这块“特性”成为了前端人必须了解并绕过的坑,也可能成为了不少前端人的困扰。

其实根据 Brendan Eich 的说法:
Brendan Eich本人回复关于变量提升

大概意思就是,变量提升是函数提升的意外结果。(手动狗头)

题目

下面开始先给几道有趣的题:

1、

if (!(Ben in window)) {
  console.log(1);
  var Ben = 'handsome';
}
console.log(Ben in window);
console.log(Ben);

2、

var a;
(function () {
  a = 1;
  console.log(a);
  if (false) {
    var a = 2;
    console.log(a);
  }
  console.log(a);
})();
console.log(a);

3、

var a = 1;
{
  console.log(a);
  var a = 2;
  console.log(a);
}

4、

var b = 1;
(function () {
  console.log(b);
  var b = 2;
  console.log(b);
})();

5、

{
  a = 123;
  function a() {}
}
console.log(a);

6、

{
  function a() {}
  a = 123;
}
console.log(a);

7、

{
  a = 123;
  function a() {}
  a = 456;
  function a() {}
}
console.log(a);

变量提升

JavaScript 代码从开始经过分词(tokenize)、预解析(preparse)、解析(parse),而变量声明在 prepare 时期就完成了。

可以这么理解,一个变量有着声明初始化赋值的过程,而声明会被预解析时就完成并且进入到各自的执行环境中,而 var 在此时将初始化也完成了(undefined),函数声明则是连赋值也完成了(函数内存地址指向)。

  • let、const 提升了声明,并将变量放入词法环境中,这时候值为未初始化(uninitialized),所以不允许使用(暂时性死区)
  • var 提升了声明初始化,并将变量放入变量环境中,这时候值为 undefined
  • 函数定义提升了声明初始化赋值,将函数创建进入堆,并将值改为函数内存地址。

(可参考let、const 是否有变量提升?))

if (false) {
  var a;
}
console.log('a' in window); // true

这块代码内,window 对象内并没有 a 变量,而且条件为 false 也并不会执行内部代码,但是这段打印确实 true,这里就说明了在代码未解析执行时就已经完成了声明。

题目解析

  1. 题目 1 解析
    因为 var 声明会在 preparse 时期执行,先于执行语句解析声明变量,且 var 声明作用于函数执行的作用域,所以会穿透 for 和 if 语句。

  2. 题目 2 解析
    因为 var 声明会在 preparse 时期执行,先于执行语句解析声明变量,且 var 声明作用于函数执行的作用域,所以会穿透 for 和 if 语句。而 function 是有函数作用域的,所以 iife 内的 a 变量为私有,所以内部都会打印 1,函数外的 a 因未被赋值所以 undefined。

  3. 题目 3 解析
    因为 var 不进入块语句中的词法环境,并且会提升,而且对于 var 来说没块级作用域。

  4. 题目 4 解析
    这里就是因为函数作用域的问题,但是为什么第一个打印还是 undefined,因为 javascript 寻找变量顺序是当前作用域一直向上寻找,而这里因为变量提升所以在 iife 函数中声明了私有变量,但是未赋值,所以为 undefined。

函数提升

函数定义说的是 function 定义的函数(function func(){}),而不是匿名函数(var func = function(){})赋值。而 function 定义不在块语句中({})的话,它会在预解析时声明、初始化、赋值。

而 function 定义如果在块语句中的话,包括 if/for/try/with 等,在 ES3 标准中,预解析,都会将 function 的声明和初始化和赋值都会提升到块语句外层;而 ES6 标准后,块语句中 function 的提升只是声明和初始化,并不赋值。而为了兼容旧标准,块语句中的 function 会将执行时,将当前块语句中的变量环境映射一份到外层上下文中,使得外层拥有了 function 语句这一行以上的所有变量的赋值,而后面的所有变量都不会被提升,成为了当前块语句的私有变量

而上面的题目 5、6、7 就是典型的例子,其实如果只要劫持一下 window 的属性,就会知道是否赋值了(因为 var 提升会将变量定义到 window 内,而 let、const 并不会,即使是全局定义的 let、const 也不进入到 window 对象内的属性

Object.defineProperty(window, 'a', {
  set(e) {
    console.log('setA', e);
  },
});
{
  a = 123;
  function a() {}
  a = 456;
}
// setA 123

{
  a = 123;
  function a() {}
  a = 456;
  function a() {}
}
// setA 123
// setA 456

{
  function a() {}
  a = 123;
  function a() {}
  a = 456;
  function a() {}
  a = 789;
}
// setA f a(){}
// setA 123
// setA 456

大概解析如下:
块语句内函数定义

总结

这里能看出 var 和 function 因为历史原因或者新旧标准的原因导致了多个奇怪的问题,所以在平时开发工作中,也应该尽量使用 let 和 const。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!