乘风破浪

js声明提升(hoisting)和 TDZ(Temporal Dead Zone)暂时性死区

先谈谈作用域

  • 什么是作用域:
    就是某个变量有起作用的范围
  • 词法作用域和动态作用域:
    词法作用域:在变量声明的时候,它的作用域就已经确定了;
    动态作用域:在程序运行的时候,由程序的当前上下文(执行环境)决定的;
  • js属于词法作用域
    词法作用域的访问规则:
        先在当前作用域中查找,如果找到就直接使用,如果没有找到,那么就到上一级作用域中查找,如果还没有找到那么就重复这个查找过程,直到全局作用域。

JS中的作用域

  1. script标签构成的全局作用域;
  2. 在js中,函数是唯一一个可以创建作用域的对象;
1
2
3
4
5
6
7
var a1 = "a1";
var b1 = "b1";
function func() {
var a1 = "n1"
console.log(a1); //n1 a1先在func函数内部作用域中查询
console.log(b1); //b1
}

变量和函数的提升

js的执行过程
预解析阶段,变量和函数的提升(声明)

具体的执行阶段:
变量和函数的提升:
js代码是一个从上至下逐步解析的过程,在这个过程中,之前会把所有的变量和函数提前声明。

1
2
3
4
5
6
console.log(a); //undefined 而不是报错
var a = 10;
f1(); //f1 而不是报错
function f1() {
console.log("f1")
}

上面一段js代码,会先把var a与fun函数提前声明,所以代码实际可模拟成先这段代码

1
2
3
4
5
6
7
var a; //变量提前声明,但未定义
function f1() { //函数提前声明
console.log("f1")
}
console.log(a); //声明为定义结果为undefined
a = 10
f1(); //结果为f1

经过上面的解析,结果便可以理解了。
具体会出现的一些问题和几种情况:
1.变量和变量同名的情况,后面的变量会把前面的变量覆盖。

1
2
3
4
5
6
7
8
9
var n1 = "n1";
console.log(n1); //n1
function test() {
console.log(n1)
}
test(); //n1
var n1 = "new n1" //覆盖之前n1的值
console.log(n1); //new n1
test() //new n1

2.函数和函数同名的情况,后面的函数会覆盖前面的函数。

1
2
3
4
5
6
7
8
9
f1(); //20
function f1() {
console.log(10);
}
f1(); //20
function f1() {
console.log(20);
}
f1(); //20

3.函数和变量同名,可以理解为:函数声明提升,而变量的声明不提升(实际上变量也提升了,但是会被函数覆盖)。

1
2
3
4
5
6
console.log(demo); //function demo() {console.log(我是函数)}
var demo = "我是字符串";
function demo() {
console.log("我是函数")
}
console.log(demo); //我是字符串

4.变量提升是分作用域的

1
2
3
4
5
6
var num = 5;
function test1() {
console.log(num); //undefined
var num = 10; //此处函数test1作用域中有声明变量num, 会提升声明, 但不会赋值
}
test1()

5.如果函数是函数表达式定义,那么在做函数声明提升的时候,仅仅只会把var变量的名字提升到当前作用域中

1
2
3
4
5
6
console.log(func) //undefined
var func = function() { // 此处只是提升声明var func, 所以为undefined
console.log("func")
}
var func = "我是MT"
console.log(func); //'我是MT'

深入理解 Hoisting

上面描述的是关于ES5中的声明提升,即使用var声明的变量的声明提升。 有人说在ES6中用
let和const声明的变量不会发生声明提升,那究竟是不是这样呢?
我们先来看一段代码。

1
2
console.log(a); //ReferenceError
let a = 1

运行这段代码会报错,把上面代码中的let换成var就不会报错,会输出undefined, 那这是不是
就说明用let声明的变量不会发生声明提升呢?

我们先假设用let声明的变量不会发生声明提升,看看会发生什么?

1
2
3
4
5
var a = 1;
(function() {
console.log(a)
let a = 2
})();

按照我们预先的假设,如果用let声明变量不会发生声明提升,这里console语句输出的结果应该是1。
但是这里会报ReferenceError的错误。这说明了用let声明的变量不会发生声明提升,这一结论是错误的!
那么用let声明的变量究竟会不会发生声明提升的现象呢?

答案是会发生声明提升,用let/const/class声明的变量均会发生声明提升,既然用let声明的变量会出现声明提升的现象,
那之前的报错(ReferenceError)又怎么解释呢?

区别就在于var和let声明的变量在发生声明提升时,初始化的行为不同导致的,用var声明的变量会初始化为undefined,
而用let声明的变量会保持为未初始化的状态。也就是这样:

1
2
3
4
5
6
(function() {
console.log(a); //undefined, a 会初始化为 undefined
console.log(b); //ReferenceError, b 会保持为为初始化的状态
var a = 1
var b = 2
})();

这也就解释了为什么会报错的那个问题,到这里我们就对变量的声明提升有了深入的理解。那么声明是TDZ呢?

什么是TDZ?

ECMScript标准里并没有给出TDZ(全称Temporal Dead Zone, 暂时性死区)定义,这JS社区里提出的一种说法。
TDZ指被声明和被初始化之间的这段时间。我们来看一个例子:

1
2
3
4
5
6
7
8
let a = "outer";
(function() {
//内部的 a 变量在这里被声明, TDZ开始的地方
console.log(a) // ReferenceError
let a = "inner" //a 变量被初始化,TDZ结束
})()

所以在内部的变量初始化之前访问a变量是会报错的,就因为有了TDZ我们更容易发现bug。为了更好的理解TDZ我们再看一个例子:

1
2
var a = a //没问题
let b = b //ReferenceError, 在这里b并没有被初始化,还处在TDZ当中

再看下面一个例子

1
2
3
4
5
var x = 1;
function foo(x = x) { //x = x 代表如果不传指,则参数x 会取 x 为默认值
//...
}
foo() //ReferenceError: x is not defined

上面的代码中,x = x 形成了一个单独作用域(context),等到初始化结束,这个作用域就会消失,实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错。

引用