变量与环境:作用域是怎么管起来的

目标

真正的语言要有变量。本节课引入 Environment(环境) 对象来存变量,并支持嵌套作用域——函数内部能读到外部变量,这正是"作用域链"的雏形。

运行环境:Node.js v18+。运行方式:保存为 env.js,执行 node env.js。

// env.js —— 作用域环境的极简实现
class Environment {
  constructor(parent = null) {
    this.vars = new Map()
    this.parent = parent // 指向外层作用域,形成链
  }
  define(name, value) { this.vars.set(name, value) }
  has(name) {
    let env = this
    while (env) { if (env.vars.has(name)) return true; env = env.parent }
    return false
  }
  get(name) {
    let env = this
    while (env) {
      if (env.vars.has(name)) return env.vars.get(name)
      env = env.parent
    }
    throw new Error('未定义的变量: ' + name)
  }
  assign(name, value) {
    let env = this
    while (env) {
      if (env.vars.has(name)) { env.vars.set(name, value); return }
      env = env.parent
    }
    this.vars.set(name, value) // 找不到就建在顶层
  }
}

const globalEnv = new Environment()
globalEnv.define('x', 10)
const inner = new Environment(globalEnv)
inner.define('y', 20)
console.log('读 y(本层) =', inner.get('y'))
console.log('读 x(外层) =', inner.get('x'))
console.log('has(x) =', inner.has('x'), '| has(z) =', inner.has('z'))

作用域链的本质就是一个个 Environment 用 parent 指针串起来的链表。get 从自己往外层一层层找,找到了就返回。

名词解释

Environment(环境 / 作用域对象):一张"变量名 → 值"的表,外加一个指向外层环境的引用。一次函数调用、一个代码块,通常对应一个新的 Environment。
作用域链(Scope Chain):环境通过 parent 串成的链。读变量时沿链向上找,这就是"内层能看到外层、外层看不到内层"的来源。
define vs assign:define 在本层新建(声明);assign 沿链找已有变量改写(赋值)。语义不同,很多语言用不同语法(如 var vs =)区分。

课后练习

练习:给 Environment 加一个 has(name) 方法,判断变量是否在当前或上层作用域中存在(不抛错)。

答案:已包含在上方的完整代码中(has 方法沿 parent 上溯,找到返回 true,否则 false)。

总结

变量看似简单,背后是一整套"去哪找值"的规则。Environment + 父指针这个极简结构,其实就是 JavaScript 词法作用域的微观缩影:全局环境是根,每次进入函数/块就挂一个新的子环境。理解了 get 沿 parent 上溯,你就不会再被"闭包为什么能记住外部变量"困惑——因为那个外部 Environment 根本没被销毁,只是被内部函数通过 parent 牢牢抓着(第7节细讲)。作用域不是语言特性,是数据结构的必然结果。