JavaScript 查漏补缺(一)

134 阅读3分钟

1. for循环作用域

我们都知道,一个for循环由循环头循环体两个部分组成。 在JavaScript中,这两个组成部分的作用域是分开的,循环头部分是一个父作用域,循环体部分是一个单独的子作用域
因此,我们在循环头中用let关键字声明的变量,在循环体中是可以再次声明,不会报错的。

for (let i = 0; i < 3; i++) {
  let i = 'test';
  console.log(i);
}
// test
// test
// test

2. for循环头中的变量声明

在学习js的过程中,经常能看到类似这样的代码:

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

因为循环头中的i是用var声明的,是一个全局变量,每个a[i]console.log(i)都指向全局变量i,因此输出为10
如果将var关键字改为let

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

从这个例子中,我们可以看出来,每一轮循环中的i都是重新声明的,因此每个a[i]中才可以输出相对应的数字。

因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

循环头中给i赋值为0的操作只会进行一次。

3. let、const的暂时性死区TDZ(Template Dead Zone)和变量提升

3.1 TDZ

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

3.2 变量提升

在搜索关于变量提升问题答案的过程中,本人发现了以下两个链接:

看到关于变量提升的高赞答案:

x = y = "global";
// function scope:
(function() {
    x; // Uncaught ReferenceError: Cannot access 'x' before initialization
    y; // global
    let/… x;
    var y 
}());
// block scope (not for `var`s):
{
    x; // Uncaught ReferenceError: Cannot access 'x' before initialization
    let/const/… x;
}

以上代码中,我们先声明了一个全局变量x,这时不论我们在哪种作用域中,都能正常输出global。 但是如果我们使用letconst再次声明变量,就会出现报错。这说明我们使用let/const声明变量时仍存在某种提升(当然这个情况可以用暂时性死区的概念来解释)


3.2.1 变量生命周期

以上关于变量提升的阐述都不够清晰,变量生命周期的概念有助于更清晰的理解变量提升。 变量生命周期包括以下三个阶段:

  • 声明阶段(Declaration phase) 在作用域内注册一个变量
  • 初始化阶段(Initialization phase) 分配内存并将之与作用域中的对象进行绑定。在此步骤中,变量将使用进行自动初始化undefined。
  • 分配阶段(Assignment phase) 为完成初始化的变量分配一个值。

varlet/const最大的区别在于,var变量经过声明阶段后就在作用域顶部立即通过初始化阶段;而let/const在声明阶段和初始化阶段之间有暂时性死区,直到执行了声明语句,才完成初始化阶段。

The variable passes the declaration phase and right away the initialization phase at the beginning of the scope

image.png

参考链接:🔗1 🔗2

所以说变量都具有变量提升的说法是不准确的,变量都具有的过程是声明提前,声明阶段都会在作用域开始时完成,但初始化阶段是不同的