你可能不知道的变量提高

相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)javascript

这部分原本打算放到简单介绍的执行上下文和执行栈里顺带说一句的,后来发现这里面内容也很多,包括暂时性死区、函数及变量提高逻辑、es6中的块级做用域等,就单开了一章。如下内容大概花费10分钟左右,欢迎评论补充知识和点赞~前端

从一道面试题提及

请说出 let,const,var 的区别java

大部分的回答是这样的,node

  1. let/const 是块级做用域
  2. let 不能重复定义,const不能重复赋值
  3. var 有变量提高

而实际上, let / const 也有变量提高git

先来看个栗子:es6

console.log(aVar); // undefined
console.log(aLet); // causes ReferenceError: aLet is not defined
var aVar = 1;
let aLet = 2;
复制代码

从结果上看,第二行没有找到aLet致使程序报错,代表let声明的变量并无提高github

没关系,再看两个栗子(请在浏览器环境运行,node环境结果不同)面试

let x = 'global';

function func(){
  console.log(x);
}

func(); // global
复制代码

func运行时,在函数内没有找到 x 的定义,沿着函数做用域链寻到外层关于 x 的定义。浏览器

let x = 'global';

function func1(){
  console.log(x);
  let x = 'func';
}

func1();
// Uncaught ReferenceError: Cannot access 'x' before initialization
// at func (<anonymous>:2:15)
// at <anonymous>:4:3
复制代码

咦,报错了?为何func1没有访问到全局环境下的x呢?不要着急,仔细看下错误提示:没法在初始化以前访问x。函数

好好想一想,这个错误意味着在func内第一行程序已经知道在本函数内有一个变量叫x了,只不过没有初始化(initialization)而已。

由此可得出结论**,因为 _let x = ‘func’_ 在函数做用域内存在变量提高,**阻断了函数做用域链的向上延伸。尽管 x 发生了变量提高,可是在初始化赋值前(before initialization)不容许读取。

这就引出了一个很重要的概念: 暂时性死区 (TDZ)

暂时性死区(Temporal Dead Zone, TDZ)

MDN 上关于暂时性死区的定义

let bindings are created at the top of the (block) scope containing the declaration, commonly referred to as “hoisting”. Unlike variables declared with var, which will start with the value undefined, let variables are not initialized until their definition is evaluated. Accessing the variable before the initialization results in a ReferenceError. The variable is in a “temporal dead zone” from the start of the block until the initialization is processed.

let绑定是在包含声明的(块)范围的顶部建立的,一般称为“提高”。不像用var声明的变量,let声明的变量不会被初始化(initialized)直到它们被定义位置的代码开始执行,在初始化以前访问变量会触发一个ReferenceError。从块的开始到变量初始化,变量都处于“暂时死区”。

简单来讲,let 仅仅发生了提高而没有被赋初值,在显式赋值以前,任何对变量的读写都会触发ReferenceError 错误。从代码块(block)起始到变量赋值之前的这块区域,称为该变量的暂时性死区

当程序控制流程运行到特定做用域(scope ≈ Lexical Environment) 时:即模块,函数,或块级做用域。在该做用域中代码真正执行以前,该做用域中定义的 let 和 const 变量会首先被建立出来,但由于在 let/const 变量被赋值(LexicalBinding)之前是不能够读写的,因此存在暂时性死区。

来看下下面代码验证下你的理解:

function test(){
   var foo = 33;
   if(foo) {
      let foo = (foo + 55); // ReferenceError
   }
}
test();
复制代码

因为词法做用域,表达式let foo = (foo + 55);中的foo被认为是if块中声明的foo,而不是函数第一行声明的var变量。在同一行中,if块的foo已经在词法环境中建立,因此程序不会沿着做用域链向上层寻找foo,但因为变量还未初始化,处于暂时性死区中,访问会触发ReferenceError。

来作道题吧:

function go(n) {
  // n here is defined!
  console.log(n); // Object {a: [1,2,3]}
  const a = n.a;
  for (let n of n.a) { // ReferenceError
    console.log(n);
  }
}

go({a: [1, 2, 3]});
复制代码

答案已经给了,欢迎在评论区留下你的看法

回到正题,从新看一下变量提高的逻辑.

全局做用域和函数做用域中的变量提高:

当进入执行上下文时,

  1. 函数的全部形参 (若是是函数上下文)
    • 由名称和对应值组成的一个变量对象的属性被建立
    • 若是没有实参,属性值设为 undefined
  2. 函数声明
    • 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被建立
    • 若是变量对象已经存在相同名称的属性,则彻底替换这个属性
    • 函数提高只会提高函数声明,而不会提高函数表达式。
  3. 变量声明
    • 由名称和对应值(undefined)组成一个变量对象的属性被建立;
    • 若是变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性

依据上述规则的逻辑,分析下列代码:

代码一:

var foo = function () {
    console.log('foo1');
}

foo();  // foo1

var foo = function () {
    console.log('foo2');
}

foo(); // foo2
// 依据规则二,函数表达式不会提高
// 依据规则三,相同变量名称不会干扰
复制代码

来看这段代码:

function foo() {
    console.log('foo1');
}

foo();  // foo2

function foo() {
    console.log('foo2');
}

foo(); // foo2
// 依据规则二,函数声明中若函数名相同,则后者彻底覆盖前者
复制代码

举一个小栗子巩固一下:

var a = 1;

function foo() {
    a = 10;
    console.log(a);     
    function a() {};
}

foo(); // 10

console.log(a); // 1
复制代码

在foo中,函数a存在变量提高,至关于

var a = 1; // 定义一个全局变量 a
function foo() {
    // 提高函数声明function a () {}到函数做用域顶端, 函数a也是变量
    var a = function () {}; // 定义局部变量 a 并赋值。
    a = 10; // 修改局部变量 a 的值
    console.log(a); // 打印局部变量 a 的值:10
    return;
}
foo();
console.log(a); // 打印全局变量 a 的值:1
复制代码

补充下块级做用域知识点:

  • ES5 只有全局做用域和函数做用域,没有块级做用域。

  • ES6 的块级做用域必须有大括号,若是没有大括号,JavaScript 引擎就认为不存在块级做用域。

  • ES6 引入了块级做用域,明确容许在块级做用域之中声明函数。ES6 规定,块级做用域之中,函数声明语句的行为相似于let,在块级做用域以外不可引用。

  • 容许在块级做用域内声明函数。

  • 函数声明相似于var,即会提高到全局做用域或函数做用域的头部。

  • 同时,函数声明还会提高到所在的块级做用域的头部。

友情提示,本篇文章建议与让人恍然大悟的词法做用域及做用域链讲解简单介绍的执行上下文和执行栈一块儿食用

ps: 我原本觉得暂时性死区是因为建立执行上下文的方式致使的,结果搜资料的时候发现块级做用域没有单独的执行上下文,只有词法环境,若你知道块级做用域与词法环境的相关知识,欢迎在评论区留言,我会及时补充进来

相关系列: 从零开始的前端筑基之旅(面试必备,持续更新~)

若是你收获了新知识,请给做者点个赞吧~

参考文档:

  1. let/const 的变量提高与暂时性死区
  2. MDN: let
  3. JavaScript深刻之执行上下文栈
相关文章
相关标签/搜索