【前端三剑客-28/Lesson46(2025-11-27)】JavaScript 作用域链与闭包机制深度解析🧠

43 阅读7分钟

🧠JavaScript 是一门基于词法作用域(Lexical Scope)的语言,其变量查找、函数执行、内存管理等核心机制都依赖于 作用域链(Scope Chain)闭包(Closure) 。本文将从底层原理出发,结合 V8 引擎的执行模型、执行上下文、调用栈、词法环境、变量提升(Hoisting)、块级作用域等多个维度,系统性地剖析 JavaScript 中作用域链的形成、查找规则、运行时行为及其在实际开发中的应用。


🔍 1. 作用域基础:什么是作用域?

作用域(Scope) 是指程序源代码中定义变量的区域,它决定了当前执行环境中哪些变量是可访问的。JavaScript 的作用域主要有以下三种:

  • 全局作用域(Global Scope) :最外层的作用域,所有未在函数或块中声明的变量都属于全局作用域。
  • 函数作用域(Function Scope) :由 function 声明创建的作用域,使用 var 声明的变量具有函数作用域。
  • 块级作用域(Block Scope) :ES6 引入,由 {} 包裹的代码块构成,使用 letconst 声明的变量具有块级作用域。

⚠️ 注意:var 不支持块级作用域,即使写在 iffor 块中,也会被提升到函数或全局作用域顶部。


📜 2. 词法作用域 vs 动态作用域

JavaScript 采用的是 词法作用域(Lexical Scope) ,也称为 静态作用域(Static Scope) 。这意味着:

函数的作用域在其 定义时 就已确定,而不是在 调用时 决定。

✅ 示例对比(来自 1.js

function bar() {
  console.log(myName); // 输出 '极客时间'
}
function foo() {
  var myName = '极客邦';
  bar(); // 在 foo 中调用 bar
}
var myName = '极客时间';
foo();

尽管 bar() 是在 foo() 内部调用的,但由于 bar 定义在全局作用域中,它的作用域链为:

bar 的作用域 → 全局作用域

因此,myName 会从全局作用域中查找到 '极客时间',而非 foo 中的 '极客邦'

这与 动态作用域(如 Bash 脚本)完全不同——动态作用域会在 调用栈 中向上查找变量,而 JavaScript 永远只看 函数定义的位置


🧱 3. 执行上下文与调用栈

JavaScript 引擎(如 V8)在执行代码时,会通过 执行上下文(Execution Context) 来管理变量、作用域和 this

执行上下文的生命周期

每个执行上下文分为两个阶段:

  1. 创建阶段(Creation Phase)

    • 创建变量对象(Variable Object, VO)
    • 建立作用域链(Scope Chain)
    • 确定 this 指向
  2. 执行阶段(Execution Phase)

    • 变量赋值
    • 函数调用
    • 表达式求值

调用栈(Call Stack)

  • 全局代码执行 → 创建 全局执行上下文,压入栈底。
  • 调用函数 → 创建 函数执行上下文,压入栈顶。
  • 函数返回 → 执行上下文弹出栈。

📌 最底部永远是全局执行上下文,函数执行上下文按调用顺序压栈。


🔗 4. 作用域链的本质

作用域链(Scope Chain) 是一个由多个 词法环境(Lexical Environment) 组成的链表,用于变量查找。

作用域链的构成

  • 每个函数在 定义时 会捕获其所在环境的词法环境,并存储在内部属性 [[Scope]] 中。
  • 当函数被调用时,新的执行上下文会将其 变量对象(VO) 添加到 [[Scope]] 链的前端,形成完整的作用域链。

例如:

var a = 1;
function outer() {
  var b = 2;
  function inner() {
    var c = 3;
    console.log(a, b, c); // 1, 2, 3
  }
  inner();
}
outer();

inner 的作用域链为:

inner 的 VO → outer 的 VO → 全局 VO

查找规则(自内向外)

  1. 在当前作用域查找变量;
  2. 若未找到,沿作用域链向上查找父级作用域;
  3. 直到全局作用域;
  4. 若仍未找到,抛出 ReferenceError

🔄 作用域链是 静态的,在 编译阶段(词法分析) 就已确定,与运行时调用位置无关。


🎒 5. 闭包:作用域链的“背包”

闭包(Closure) 是指一个函数能够访问并操作其 词法作用域外部 的变量,即使该外部函数已经执行完毕。

闭包形成的条件(来自 readme.md

  1. 函数嵌套;
  2. 内部函数被 返回传递到外部
  3. 内部函数 引用了外部函数的变量

✅ 示例(来自 3.js

function foo() {
  var myName = '极客世界';
  let test1 = 1;
  const test2 = 2;

  var innerBar = {
    getName: function() {
      console.log(test1); // 1
      return myName;
    },
    setName: function(newName) {
      myName = newName;
    }
  };

  return innerBar; // 返回对象,内部方法形成闭包
}

var bar = foo(); // foo 执行完毕,执行上下文出栈
bar.setName('极客邦');
console.log(bar.getName()); // '极客邦'

虽然 foo() 已经执行结束,其执行上下文从调用栈弹出,但 getNamesetName 仍然能访问 myNametest1,因为:

这些变量被“打包”进了一个 闭包背包(Closure Bag) ,随函数一起保留在内存中。

这个“背包”就是 外部函数的词法环境,通过作用域链维持对自由变量(Free Variables)的引用。

📦 自由变量:不是在当前函数中声明,但被当前函数使用的变量。


🧪 6. 实际案例深度分析

📌 案例一:变量查找路径(来自 2.js

function bar() {
  var myName = '极客世界';
  var test1 = 100;
  if (1) {
    let myName = 'Chrome 浏览器';
    console.log(test); // 输出 1
  }
}

function foo() {
  var myName = '极客邦';
  let test = 2;
  {
    let test = 3;
    bar(); // 调用 bar
  }
}

var myName = '极客时间';
let myAge = 18;
let test = 1;
foo();

输出:1

分析过程:

  1. bar() 定义在全局作用域 → 其作用域链为:bar → 全局
  2. bar 内部没有声明 test,于是沿作用域链查找。
  3. bar 的父级是 全局作用域(不是 foo!),所以查到 let test = 1
  4. 输出 1

❗ 关键点:bar 的作用域链 不包含 foo,因为词法作用域只看定义位置。


📌 案例二:闭包与私有状态(来自 note.md

function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    getCount: () => count
  };
}

const counter = createCounter();
counter.increment(); // 1
counter.getCount();  // 1
  • count 是私有变量,外部无法直接访问。
  • incrementgetCount 通过闭包捕获 count
  • 每次调用 createCounter() 都会创建 新的闭包环境,彼此独立。

⚙️ 7. V8 引擎底层机制补充

编译阶段 vs 执行阶段

  • 编译阶段(词法分析)

    • 解析代码结构;
    • 确定函数声明、变量声明;
    • 构建作用域链(静态);
    • 处理 变量提升(Hoisting)
  • 执行阶段

    • 创建执行上下文;
    • 压入调用栈;
    • 执行代码,赋值、调用函数;
    • 弹出上下文,可能触发垃圾回收(除非被闭包引用)。

变量提升(Hoisting)

  • var 声明会被提升到函数或全局顶部(仅声明,不赋值);
  • let/const 也有提升,但处于 暂时性死区(TDZ) ,不可访问;
  • 函数声明整体提升(包括函数体)。

💡 变量提升的存在,正是因为作用域和变量对象在 编译阶段 就已构建完成。


🧩 8. 作用域链与块级作用域

ES6 引入 let/const 后,JavaScript 支持 块级作用域

{
  let x = 1;
  const y = 2;
}
console.log(x); // ReferenceError
  • 块级作用域由 {} 创建;
  • 每个块都有自己的 词法环境
  • 作用域链会包含这些嵌套的块环境。

例如:

function demo() {
  let a = 1;
  if (true) {
    let b = 2;
    console.log(a); // 1(向上查找)
  }
}

作用域链:if 块 → demo 函数 → 全局


🛠️ 9. 性能优化与最佳实践

✅ 优化策略

  • 避免深层嵌套:作用域链越长,变量查找越慢;
  • 缓存外层变量
// 低效
function slow() {
  for (let i = 0; i < 1e6; i++) {
    console.log(document.title); // 每次遍历作用域链
  }
}

// 高效
function fast() {
  const title = document.title; // 缓存到局部
  for (let i = 0; i < 1e6; i++) {
    console.log(title);
  }
}

✅ 现代 JS 最佳实践

  • 优先使用 const / let
  • 避免全局变量污染;
  • 使用 模块模式ES6 模块 封装逻辑;
  • 利用闭包实现 私有状态
  • 箭头函数继承外层 this,简化上下文绑定。

🧠 10. 总结:作用域链的核心思想

  • 作用域 是变量的可访问范围;
  • 词法作用域 是静态的,由代码结构决定;
  • 作用域链 是变量查找的路径,从内到外;
  • 闭包 是作用域链在函数返回后的延续;
  • 执行上下文 是运行时的作用域载体;
  • 调用栈 管理上下文的生命周期;
  • V8 引擎 在编译阶段就构建好作用域结构。

🌟 正如《你不知道的JS》所言:“JavaScript 的复杂性,很大程度上来自于它的作用域规则和闭包机制。理解了这些,你就掌握了 JavaScript 的精髓。”

掌握作用域链,不仅能写出更健壮、高效的代码,还能深入理解 React Hooks、Vue 响应式系统、模块打包器等现代前端技术的底层逻辑。


📚 参考资料

  • 《JavaScript语言精粹》— Douglas Crockford
  • 《你不知道的JS》— Kyle Simpson
  • 《JavaScript高级程序设计》— Matt Frisbie
  • ECMAScript 官方规范
  • MDN Web Docs - JavaScript 作用域与闭包