javascript 路线图—聊一聊 Hoisting(中)

499 阅读4分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

let 和 const 的 hoisting

在 ES6 里面新增两个声明变量的关键字 let 和 const,分别解决了 var 作用域问题以及一个静态变量。那么在使用了 let 和 const 关键字声明变量是否也支持变量提升(Hoisting),可以看下面的代码。

console.log(l1)
let l1;
ReferenceError: Cannot access 'l1' before initialization

这是因为声明了变量 li 并没有为其初始化 undefined,处于 temporal dead zone 优雅翻译为暂时性失控区域,这么翻译。其实这里只是声明还没有初始化为其赋值为 undefined 所以会抛出上面的异常。

temporal dead zone

这里对个解释一下方便大家理解 let 和 const 的 hoisting,这个名词就是为了解释 let 和 const 的 hoisting 而提出,这是一个时间上的概念,而非空间上的概念,指的是在提升之后和赋值之前的区域,对于 var 来来说提升后就将其初始化为 undefined 而 let 和 const 却只有提升而没有初始化。

function fn1() {
    fn2() // c 的 TDZ 起始
    let c = 3 // c 的 TDZ 结束
    function fn2(){
      console.log(c)
    }
}
fn1()

当进入 fn1 函数里,c 就处于 TDZ,所以执行到 fn2 就会抛出 ReferenceError: Cannot access 'c' before initialization

为什么我们需要 Hoisting

我们想一想为什么我们需要 Hoisting ,其实对于变量可以先声明然后再去使用这个当然是一个不错习惯,值得提倡的好习惯。不过对于函数是否有必要先声明再去调用,其实函数声明提升是 JavaScript 有点,没有必要在文件前面声明一大堆函数然后再去调用。 而且因为如果没有 Hoisting 就无法实现函数互调,所以需要 Hoisting。

function loop(n){
    if (n>1) {
      logEvenOrOdd(--n)
    }
}

function logEvenOrOdd(n) {
console.log(n, n % 2 ? 'Odd' : 'Even')
loop(n)
}

loop(10)

如果没有 Hoisting 就无法实现 A 中调用 B,B 中还可以调用 A。 通过这里例子想必大家都理解了为什么我们需要 Hoisting 了吧。

JavaScript 执行上下文

接下来有必要介绍一下 JavaScript 执行上下文,以深入了解JavaScript代码如何被执行。

let x = 3;

function add2(a){
    return a + 2;
}

const y = add2(x);

console.log(y); // 5
  • 首先将 3 赋值给 x
  • 声明函数 add2 将其参数加上 2
  • 将 x 作为参数传入到 add2 后返回值赋值给 y
  • 通过 console.log 将 y 输出

代码看起来直截了当的。然而,在幕后,JavaScript 做了很多事情。今天我们将重点看一看执行上下文。

当一个 JavaScript 引擎执行一个脚本时,都会创建执行上下文。每个执行上下文有两个阶段:创建阶段和执行阶段。

创建阶段

当一个脚本第一次执行时,JavaScript 引擎会创建一个全局执行上下文。在这个创建阶段,执行了以下任务。

  • 创建一个全局对象,即网络浏览器中的 window 或 Node.js 中的 global
  • 创建一个指向 global 对象的 this 对象
  • 开辟一个内存堆,用于存储变量和函数引用
  • 在内存堆中存储函数声明,在全局执行环境中存储变量,初始值为 undefiined

在我们的例子中,在创建阶段,JavaScript引擎在全局执行上下文中存储了变量x和y以及函数声明 add2()。此外,变量x和y初始化为 `undefined。

js_execution_001.png

到此就完成了创建阶段,进入了下一个阶段执行阶段

执行阶段

在执行阶段,JavaScript 引擎逐行执行代码。在这个阶段,变量赋值并执行函数调用。

js_execution_002.png

对于每个函数调用,JavaScript 引擎都会创建一个新的函数执行上下文。函数执行上下文与全局执行上下文类似,但并不是创建全局对象,而是创建参数对象,其中包含对传入函数的所有参数的引用。

js_execution_003.png

在例子中,函数执行上下文创建了引用所有传入函数的参数的 arguments 对象,将这个值设置为全局对象,并将a参数初始化为 undefined

在函数执行上下文的执行阶段,为参数 a 赋值 3,并将结果(5)返回给全局执行上下文。

js_execution_005.png

为了跟踪所有的执行上下文,包括全局执行上下文和函数执行上下文,JavaScript 引擎使用了一个名为调用栈的数据结构。