JavaScript 的底层逻辑: 深挖词法作用域、作用域链、变量提升与执行上下文

引言

在JavaScript中,理解词法作用域以及作用域链、执行上下文、变量提升是掌握这门语言的关键。这些代码深刻影响着代码的执行流程,还决定了变量的和函数的可见性与查找方式。通过深入探讨这些概念,我们可以更好地编写清晰、可维护且高效的代码。

词法作用域与作用域链

词法作用域是指变量的作用域在编写时就已经由其位置决定了,函数可以访问其定义时所在作用域及其外部作用域中的变量。

作用域链是通过一系列嵌套的词法环境形成的链式结构,用于在当前作用域中查找变量,如果找不到则沿着链向上一级作用域继续查找,直到全局作用域。

执行上下文

1. 什么是执行上下文

执行上下文(Execution Context)是JavaScript中用于管理代码执行过程中的环境和状态的一个抽象概念。每个执行上下文都包含了一系列的信息,包括变量、函数声明、作用域链以及this的值等。

2. 如何存储执行上下文

具体的执行上下文实例会被放置在一个称为调用栈(Call Stack)的数据结构中。调用栈负责跟踪当前正在执行的函数及其调用顺序。每当一个函数被调用时,其对应的执行上下文就会被推入调用栈;当函数执行完毕后,相应的执行上下文会从调用栈中弹出。

3. 执行上下文的类型

执行上下文有三种类型:全局执行上下文、函数执行上下文、eval函数执行上下文。

全局执行上下文: 这是JavaScript程序启动时创建的第一个执行上下文,它代表了全局作用域。

函数执行上下文: 每当一个函数被调用时,就会为该函数创建一个新的执行上下文。这个上下文包含了函数内部的所有局部变量、参数及其它信息。

Eval执行上下文: 当使用eval()函数执行字符串形式的代码时,会创建一个eval执行上下文。不是很常用

4. 执行上下文的组成

每个执行上下文主要由以下三部分组成:

变量环境: 它是一个记录了在该执行上下文中声明的所有变量和函数的数据结构。

词法环境: 用于存储变量、函数声明及其对应值的数据结构,并维护着对外部环境的引用,从而形成作用域链,决定了代码在执行时如何查找和访问这些标识符。

this关键字: 指定了当前执行上下文中的this所指向的对象。

变量提升(hoisting): 一把隐形的双刃剑

在JavaScript中,变量提升是一种行为,无论你在何时声明一个变量和函数,他都会将它们移动到作用域的顶部,这一过程叫做变量提升

JavaScript 复制代码
console.log(a);
console.log(func);
console.log(b); // 词法环境中的变量或常量,在申明之前不可访问
// 暂时性死区 TDZ
var a = 1;
function func() {

}
let b = 2;

var a 声明会被提升到其作用域的顶部,但赋值不会被提升。因此,在执行 console.log(a) 时,a 的值为 undefined。

函数声明 func 也会被提升,这意味着在执行任何代码之前,func 已经是一个可用的函数。

为了避免变量提升对代码可读性的影响,es6引入了let和const,使用let和const声明的变量也会被添加到变量环境中,但是它们不会立即初始化(即处于暂时性死区 TDZ, Temporal Dead Zone)。这意味着在声明之前尝试访问这些变量会导致一个引用错误(ReferenceError)。此外,let和const声明的变量具有块级作用域(block scope),只在声明它们的块内有效。

代码分析

1. 词法作用域与作用域链

JavaScript 复制代码
function foo() {
    var a = 1;
    let b = 2;
    {
      let b = 3;
      var c = 4;
      let d = 5;
      console.log(a);
      console.log(b)  
    }
    console.log(b)
    console.log(c)
    console.log(d)
  }
  foo();
变量分析
  • a和c是用var声明的,所以它们的作用域为整个foo函数。并将它们记录在变量环境中。

  • b有两次声明

    • let b = 2;是在函数的外层作用域声明的,所以它的作用域为整个foo函数中除了内层块之外的所有作用域。并将它记录在词法环境的底层。
    • let b = 3;是在块级作用域内重新声明的,并赋予了新值3。所以它的作用域为这个块级作用域,并将它添加到这个内部块的词法环境中。
  • d是用let声明的,所以它的作用域为这个块级作用域,并将它添加到这个内部块的词法环境中。

  • 根据以上分析,我们可以将foo执行上下文画出来

    根据分析,我们可以将foo执行上下文画出来

输出分析
  • 当执行 console.log(a) 和 console.log(b) 时,它们是在内层块内,由于a是用var声明的,所以直接就输出1。而b在当前块内被重新定义为3,所以输出3。
  • 当执行第二个console.log(b)时,此时处于foo函数的最外层,而最近一层的b为let b = 2,所以输出2。
  • 当执行console.log(c)时,查找c的值,由于c是用var声明的,所以它在整个foo函数中都是可见的,因此输出 4。
  • 当执行console.log(d)时,尝试查找d,但是d只在第一个内层块中有定义,所以在该块外尝试访问会导致ReferenceError。

2. 外部作用域(outer scope)

JavaScript 复制代码
function bar() {
    console.log(myname);
}
function foo() {
    var myname = 'john'
    bar()
    console.log(myname);
}
var myname = 'lisa'
foo();
执行上下文分析

如果根据直觉我们可能会认为两个输出值都为john,但在bar()中它输出的是lisa。

仔细观察上面这张图,它有一个outer指针,简单作用域可以直接从内到外查找,但如果在函数中,还要考虑outer指向的外部作用域是是什么

由于bar()是在全局作用域中定义的,所以即使在foo()中被使用,但它的outer指向的还是全局作用域,所以它查找的会是全局作用域中定义的myname = lisa,而不会经过foo()。

总结

通过分析具体的代码示例,我们看到了变量提升(hoisting)的行为,以及var、let和const声明的不同特性。特别是ES6引入的let和const,它们具有块级作用域,并且在声明之前访问会导致引用错误,从而避免了var带来的变量提升问题。

最后,通过一个复杂的例子,我们讨论了外部作用域的概念,即函数可以访问其外部作用域中的变量。这个例子展示了即使在函数内部调用另一个函数,该函数的作用域链仍然指向其定义时的外部作用域,而不是调用时的作用域。

通过这些概念的理解和实践,开发者可以更好地控制变量的作用域,避免常见的编程错误,并编写出更加健壮和易于维护的代码。

相关推荐
程序员Sunday2 小时前
Vue 3 响应式原理是什么?Proxy、ref 和 reactive 有什么区别?
前端·javascript
mldong2 小时前
一个 npm 包,九页审批中心:Vue3 项目接上工作流引擎
前端·javascript·vue.js
刘发财13 小时前
前端如何生成10000页PDF文件?
前端·javascript·开源
A接拉起00713 小时前
2026 年 JavaScript 开发者需要了解的知识
前端·javascript·后端
_馒头14 小时前
pixiJs游戏开发实践 - 跳棋小游戏
javascript·vue.js
三*一14 小时前
优化修整算法
前端·javascript·算法
楠楠子呀15 小时前
英语、西语、阿语客户各开一个口:多语言获客的分流方案
开发语言·javascript·ecmascript·跨境电商·whatsapp·营销工具
半个落月15 小时前
LangGraph.js 实战:从状态图到分支、循环、检查点与人工中断
javascript·人工智能·langchain
默_笙18 小时前
🧀 让大模型的答案"流"出来:NestJS AI 模块 + SSE + RxJS 实战
javascript·人工智能
mbshqqb18 小时前
TypeScript基础教程之同步生成器
前端·javascript·typescript