深入了解JavaScript执行机制:词法环境、执行上下文、词法作用域与作用域链

694 阅读7分钟

在上一篇文章中,我们初步探讨了JavaScript中的作用域、调用栈和变量提升等基本概念。这些概念是理解JavaScript执行机制的基础。然而,要想更深入地掌握JavaScript的执行机制,还需要进一步了解执行上下文、词法环境、作用域链以及ES6引入的新特性如letconst关键字的影响。 在这篇文章中,我们将继续深入探讨这些概念,帮助读者更全面地理解JavaScript的执行机制。

词法环境

  • 词法环境是JavaScript中用于存储变量和函数声明的数据结构。
  • 主要用于存储 let 和 const 声明的变量。这些变量不会被提升到整个作用域的顶部,而是在声明它们的块级作用域内有效。在声明之前访问这些变量会导致引用错误,因为它们处于暂时性死区(Temporal Dead Zone, TDZ)。
  • 块级作用域:限于代码块内的作用域,通常由 {} 包围。

示例

//hoisting
console.log(a);
console.log(b);//词法环境中的变量/常量,在声明之前不可访问
//暂时性死区 TDZ

var a=1;//变量环境
let b=2;//词法环境 
b++;//词法环境里查找b

image.png 在这个例子中,var a; 的声明被提升了,但 a = 1; 这一步初始化没有被提升,因此输出为 undefined。而let b =2存在于词法环境中,只会被提升到它们所在的块级作用域的顶部,并且在声明之前访问它们会导致引用错误。

ES6 引入了 letconst 关键字,这些关键字的行为与 var 不同。可以避免许多与变量提升相关的问题,提高代码的可读性和可维护性.

执行上下文

每个执行上下文主要包括以下几个部分:

  1. 变量环境(Variable Environment):存储 var 声明的变量和函数声明。
  2. 词法环境(Lexical Environment):存储 let 和 const 声明的变量。
  3. 作用域链(Scope Chain):作用域链是一个由多个词法环境组成的链表,每个词法环境都包含变量和函数声明的信息。
  4. this绑定

执行上下文主要分为:

全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置this的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

函数执行上下文:每当一个函数被调用时,都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序执行一系列步骤。

这里我们根据代码来分析:

function foo() {
    var a = 1;
    let b = 2;
    {
      let b = 3;
      var c = 4;//c 提升到当前执行上下文的变量环境中
      let d = 5;
      console.log(a);//1
      console.log(b)  //3,这里的b是内部块中声明的b
    }
    {
        let b = 5;// 这里再次声明了一个块级作用域的 b,但它不会影响外部的 b
    }
    console.log(b)//2,这里的b是外部声明的b
    console.log(c)//4,var 不支持块级作用域
    console.log(d)//d is not defined,块级作用域已经销毁了,查找不到
  }

foo()

a2289c30e05dd92f22af1ed572bcc6ea.png

在解释这段代码之前我们先引入一下词法作用域的概念:词法作用域(Lexical Scope),也称为静态作用域,是指变量的作用域在代码编写时就已经确定,而不是在运行时确定。在JavaScript中,词法作用域是通过函数来划分的,每个函数都有自己的作用域。

代码分析

  • 变量a和c使用 var 关键字声明的变量 ,其作用域是整个 foo 函数。即使c是在内部块中声明的。

  • let b = 2;:使用 let 关键字声明的变量 b,其作用域是 foo 函数内部的块级作用域。

  • let b = 3;:在内部块中再次使用 let 声明变量 b,这会创建一个新的块级作用域的变量 b,覆盖外部的 b

  • let d = 5;:使用 let 关键字声明的变量 d,其作用域是内部块。

  • 当代码执行到内部块时,它会输出:

  • console.log(a);:输出 1,因为变量 a 在整个 foo 函数中都是可见的。

  • console.log(b);:输出 3,因为在内部块中,变量 b 被重新声明为 3,覆盖了外部的 b

当代码执行到 foo 函数的外部块时,它会输出:

  • console.log(b);:输出 2,因为外部块中的变量 b 没有被修改。
  • console.log(c);:输出 4,因为变量 c 是使用 var 声明的,其作用域是整个 foo 函数。
  • console.log(d);:报错 d is not defined,因为变量 d 的作用域仅限于内部块,在外部块中无法访问。

作用域链

  • 在JavaScript中,作用域链是一个由多个执行上下文(Execution Context)组成的链式结构,它决定了变量和函数的可见性和生命周期。当在某个执行上下文中查找变量时,JavaScript引擎会首先在当前执行上下文中查找,如果没有找到,则会沿着作用域链向上查找,直到找到该变量或者到达全局执行上下文。

示例

function bar(){
  console.log(myname);
}

function foo(){
    var myname = 'zhangsan'
    bar()
    console.log(myname);
}
 var myname = 'lisi'
foo();
//outer是指向词法环境的指针,指向的是全局环境,所以打印的是全局环境中的myname,而不是foo函数中的myname。
  • 全局执行上下文myname 被初始化为 'lisi'
  • foo 函数的执行上下文myname 被声明并初始化为 'zhangsan'
  • bar 函数的执行上下文myname 在 bar 函数的词法环境中未找到,由outer指针沿作用域链向上查找,最终在全局词法环境中找到 myname,其值为 'lisi'
  • foo 函数继续执行myname 在 foo 函数的词法环境中找到,其值为 'zhangsan'

因此,最终的输出结果是:

lisi
zhangsan

如图所示:

7aa28c18a32632a97a81be30835113a3.png 这里的执行机制运用了执行栈的知识:

  • 当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 foo() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

  • 当从 foo() 函数内部调用 bar() 函数时,JavaScript 引擎为 bar() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 bar() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 foo() 函数的执行上下文。

  • 当 foo() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

总结

理解JavaScript的执行机制,特别是变量提升、调用栈、执行上下文、词法环境和作用域等概念,对于编写高效和无误的代码至关重要。通过合理利用这些机制,开发者可以更好地控制变量的作用域和生命周期,避免常见的陷阱和错误。

希望本文能帮助你深入理解JavaScript的执行机制,从而在实际开发中更加得心应手。