1、作用域链彻底搞懂:词法环境 vs 执行上下文

216 阅读3分钟

💡 本篇目标:彻底理解 JavaScript 中作用域的本质、形成机制、作用域链的运行逻辑,并通过可视化图解 + 代码演示 + 实战示例,建立你对 JS 底层执行模型的“源代码级”理解。


🎯 一、你以为的作用域,其实只是“静态规则”

❓什么是作用域(Scope)?

作用域就是变量访问的范围,决定了在什么位置能访问到某个变量

JS 是词法作用域(Lexical Scope)语言,也就是说:

  • 作用域在你写代码时就已经决定了
  • 跟你怎么调用这个函数无关

🧠 对比图:词法作用域 vs 动态作用域

类型特点举例
词法作用域静态、由代码结构决定JavaScript、C、Go
动态作用域动态、由调用栈决定bash、某些 Lisp

📦 二、执行上下文(Execution Context)vs 词法环境(Lexical Environment)

✅ 术语区分:

概念说明
执行上下文当前 JS 执行时的状态(变量、作用域链、this 等)
词法环境是执行上下文的一部分,维护变量-值映射的结构
作用域链多个词法环境通过 outer 连接形成的“查找路径”

🧩 执行过程简图:

代码结构决定:
function foo() {
  var a = 10
  function bar() {
    console.log(a)
  }
  bar()
}

执行栈过程如下:

  1. 进入全局执行上下文(创建 globalEnv)
  2. 创建 foo 函数 → 记录其词法环境指向 globalEnv
  3. 调用 foo() → 创建 fooEC,fooEnv → outer → globalEnv
  4. 调用 bar() → 创建 barEC,barEnv → outer → fooEnv

🔬 三、作用域链的运行逻辑(图解)

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

内部结构可视化(作用域链):

innerEnv → outerEnv (outer) → outerEnv (global)
   c           b                a

变量查找机制是:从当前作用域向外一层一层找,直到 global,找不到就报错


✅ 四、工程实战:作用域链是如何用于权限控制的?

示例:构建一个私有变量模块(闭包 + 作用域链)

function createCounter() {
  let count = 0 // 作用域链上的私有变量
  return {
    increment() {
      count++
      return count
    },
    getCount() {
      return count
    }
  }
}

const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.getCount()) // 1

📌 本质解析:

  • createCounter 执行后,其 count 被保存在 [[Environment]]
  • incrementgetCount 拿到的是 闭包 + 作用域链形成的访问权
  • 外部无法直接访问 count实现了封装与数据安全

🧨 五、常见误区与注意点

1. 作用域不是调用时才决定的

var a = 1
function foo() {
  console.log(a)
}
function bar() {
  var a = 2
  foo() // 输出 1,而不是 2
}
bar()

因为 foo 在定义时,其作用域链已经锁定在 global,不会因为在 bar 中调用而切换作用域。


2. 不理解作用域链会导致“闭包陷阱”

const fns = []
for (var i = 0; i < 3; i++) {
  fns.push(function () {
    console.log(i) // 全部输出 3
  })
}
fns[0]() // 3 ❌

✅ 修正方式一:使用 IIFE

for (var i = 0; i < 3; i++) {
  (function(i) {
    fns.push(function () {
      console.log(i)
    })
  })(i)
}

✅ 修正方式二:使用 let(块级作用域)

for (let i = 0; i < 3; i++) {
  fns.push(() => console.log(i)) // 输出 0,1,2
}

🚀 六、总结:你现在应该掌握了

知识点理解程度
JS 是词法作用域语言
执行上下文 vs 词法环境
作用域链查找顺序
如何利用作用域封装私有数据
容易出错的场景与修复方法

📘 下一篇《第2篇:变量提升与函数提升的编译原理剖析》将深入源码层级,揭示 JS 是如何在执行前构建作用域和变量声明的。