函数:一等公民与调用机制

目标

函数让代码可复用。本节课把"函数定义 + 调用"加进语言:函数是一等公民(可以赋值、可以当参数),调用时创建新的作用域并把参数绑进去。下面给出完整可运行的解释器,直接算斐波那契。

运行环境:Node.js v18+。运行方式:保存为 func.js,执行 node func.js(在 flow.js 基础上增加 func / return / 调用)。

// func.js —— 完整可运行:含函数的解释器
const KEYWORDS = new Set(['var', 'if', 'else', 'while', 'func', 'return'])
function tokenize(src) {
  const tokens = []; let i = 0
  const isD = c => c >= '0' && c <= '9'
  const isA = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'
  while (i < src.length) {
    const c = src[i]
    if (c === ' ' || c === '\n' || c === '\t' || c === '\r') { i++; continue }
    if (isD(c)) { const s = i; while (i < src.length && isD(src[i])) i++; tokens.push({ type: 'num', value: Number(src.slice(s, i)) }); continue }
    if (isA(c)) { const s = i; while (i < src.length && (isA(src[i]) || isD(src[i]))) i++; const w = src.slice(s, i); tokens.push(KEYWORDS.has(w) ? { type: 'kw', value: w } : { type: 'id', value: w }); continue }
    if (c === '<' || c === '>') { if (src[i + 1] === '=') { tokens.push({ type: 'op', value: c + '=' }); i += 2; continue } tokens.push({ type: 'op', value: c }); i++; continue }
    if (c === '=') { if (src[i + 1] === '=') { tokens.push({ type: 'op', value: '==' }); i += 2; continue } tokens.push({ type: 'eq', value: c }); i++; continue }
    if (c === '!') { if (src[i + 1] === '=') { tokens.push({ type: 'op', value: '!=' }); i += 2; continue } throw new Error('不支持单独的 !') }
    const one = { '+': 'plus', '-': 'minus', '*': 'star', '/': 'slash', '(': 'lparen', ')': 'rparen', ';': 'semi', '{': 'lbrace', '}': 'rbrace', ',': 'comma' }[c]
    if (one) { tokens.push({ type: one, value: c }); i++; continue }
    throw new Error('非法字符: ' + c)
  }
  tokens.push({ type: 'eof', value: null }); return tokens
}
function parse(src) {
  const tokens = tokenize(src); let pos = 0
  const peek = () => tokens[pos], next = () => tokens[pos++]
  const expect = t => { if (peek().type !== t) throw new Error('语法错误:期望 ' + t); return next() }
  function parseExpr() {
    let n = parseCmp()
    while (peek().type === 'plus' || peek().type === 'minus') { const op = next().value; n = { type: 'binary', op, left: n, right: parseCmp() } }
    return n
  }
  function parseCmp() {
    let n = parseTerm()
    const cmp = new Set(['<', '>', '<=', '>=', '==', '!='])
    while (cmp.has(peek().value)) { const op = next().value; n = { type: 'binary', op, left: n, right: parseTerm() } }
    return n
  }
  function parseTerm() {
    let n = parseFactor()
    while (peek().type === 'star' || peek().type === 'slash') { const op = next().value; n = { type: 'binary', op, left: n, right: parseFactor() } }
    return n
  }
  function parseFactor() {
    const t = peek()
    if (t.type === 'minus') { next(); return { type: 'unary', op: '-', operand: parseFactor() } }
    if (t.type === 'num') { next(); return { type: 'num', value: t.value } }
    if (t.type === 'id') {
      next()
      if (peek().type === 'lparen') {
        next(); const args = []
        if (peek().type !== 'rparen') { args.push(parseExpr()); while (peek().type === 'comma') { next(); args.push(parseExpr()) } }
        expect('rparen'); return { type: 'call', callee: { type: 'ident', name: t.value }, args }
      }
      if (peek().type === 'eq') { next(); return { type: 'assign', name: t.value, value: parseExpr() } }
      return { type: 'ident', name: t.value }
    }
    if (t.type === 'lparen') { next(); const e = parseExpr(); expect('rparen'); return e }
    throw new Error('无法解析表达式: ' + t.type)
  }
  function parseStmt() {
    const t = peek()
    if (t.type === 'kw' && t.value === 'var') {
      next(); const name = expect('id').value; expect('eq'); const init = parseExpr(); expect('semi'); return { type: 'var', name, init }
    }
    if (t.type === 'kw' && t.value === 'func') {
      next(); const name = expect('id').value; expect('lparen')
      const params = []
      if (peek().type !== 'rparen') { params.push(expect('id').value); while (peek().type === 'comma') { next(); params.push(expect('id').value) } }
      expect('rparen'); const body = parseStmt()
      return { type: 'func', name, params, body }
    }
    if (t.type === 'kw' && t.value === 'return') {
      next(); let v = null; if (peek().type !== 'semi') v = parseExpr(); expect('semi'); return { type: 'return', value: v }
    }
    if (t.type === 'kw' && t.value === 'if') {
      next(); expect('lparen'); const test = parseExpr(); expect('rparen')
      const cons = parseStmt(); let alt = null
      if (peek().type === 'kw' && peek().value === 'else') { next(); alt = parseStmt() }
      return { type: 'if', test, cons, alt }
    }
    if (t.type === 'kw' && t.value === 'while') {
      next(); expect('lparen'); const test = parseExpr(); expect('rparen'); const body = parseStmt()
      return { type: 'while', test, body }
    }
    if (t.type === 'lbrace') { next(); const stmts = []; while (peek().type !== 'rbrace') stmts.push(parseStmt()); expect('rbrace'); return { type: 'block', stmts } }
    const e = parseExpr(); expect('semi'); return { type: 'exprstmt', expr: e }
  }
  const prog = []; while (peek().type !== 'eof') prog.push(parseStmt()); return prog
}
class Environment {
  constructor(parent = null) { this.vars = new Map(); this.parent = parent }
  define(n, v) { this.vars.set(n, v) }
  get(n) { let e = this; while (e) { if (e.vars.has(n)) return e.vars.get(n); e = e.parent } throw new Error('未定义变量: ' + n) }
  assign(n, v) { let e = this; while (e) { if (e.vars.has(n)) { e.vars.set(n, v); return } e = e.parent } this.vars.set(n, v) }
}
function ev(node, env) {
  switch (node.type) {
    case 'num': return node.value
    case 'ident': return env.get(node.name)
    case 'unary': return -ev(node.operand, env)
    case 'binary': {
      const a = ev(node.left, env), b = ev(node.right, env)
      switch (node.op) {
        case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b
        case '<': return a < b; case '>': return a > b; case '<=': return a <= b; case '>=': return a >= b
        case '==': return a === b; case '!=': return a !== b
      }
    }
    case 'assign': { const v = ev(node.value, env); env.assign(node.name, v); return v }
    case 'var': { const v = ev(node.init, env); env.define(node.name, v); return v }
    case 'exprstmt': return ev(node.expr, env)
    case 'block': { const b = new Environment(env); let r; for (const s of node.stmts) r = ev(s, b); return r }
    case 'if': return ev(node.test, env) ? ev(node.cons, env) : (node.alt ? ev(node.alt, env) : undefined)
    case 'while': { let r; while (ev(node.test, env)) r = ev(node.body, env); return r }
    case 'func': { env.define(node.name, { type: 'UserFn', params: node.params, body: node.body, closure: env }); return node.name }
    case 'call': {
      const callee = ev(node.callee, env)
      const args = node.args.map(a => ev(a, env))
      if (typeof callee === 'function') return callee(...args)
      if (callee && callee.type === 'UserFn') {
        const fnEnv = new Environment(callee.closure)
        callee.params.forEach((p, i) => fnEnv.define(p, args[i]))
        try { ev(callee.body, fnEnv) } catch (e) { if (e && e.__ret) return e.value; throw e }
        return undefined
      }
      throw new Error('不是可调用的对象')
    }
    case 'return': { const v = node.value ? ev(node.value, env) : undefined; const err = new Error('return'); err.__ret = true; err.value = v; throw err }
  }
  throw new Error('未知节点: ' + node.type)
}
function run(src) {
  const env = new Environment()
  env.define('print', (...xs) => console.log(...xs))
  for (const s of parse(src)) ev(s, env)
}

// ===== 调用示例:递归斐波那契 =====
run(`
func fib(n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}
print(fib(10));
`)

调用函数时新建 fnEnv,其父作用域设成 closure(函数定义处的作用域)。参数被 define 进 fnEnv,于是函数体里能用到参数——这就是"传参"的全部秘密。

名词解释

一等公民(First-class):函数能像数字一样被赋值给变量、当作参数传递、作为返回值。我们的 func 定义就是把一个 UserFn 对象 define 进环境,因此它天然是一等公民。
调用(Call):执行函数体的过程。核心动作是"开新作用域 + 绑参数 + 跑函数体"。return 用抛特殊错误的方式"提前跳出"函数体。
UserFn vs 原生函数:UserFn 是我们语言里的函数(由 AST 定义);原生函数是宿主语言(JS)的函数(如 print)。call 节点对两者分别处理。

课后练习

练习:用本课解释器写阶乘 fact(n) 并打印 fact(5)。

答案:把调用示例换成 func fact(n) { if (n < 2) return 1; return n * fact(n - 1); } print(fact(5)); 即可,输出 120。

总结

函数是编程语言的分水岭。本课最该记住的一句话是:调用就是"开一个挂着参数的新作用域,然后跑函数体"。closure 这个字段看似不起眼,却决定了函数能"记住"定义时的环境(引用),而不是调用时的——它是下一节闭包的伏笔。把函数做成一等公民,你的语言立刻拥有了抽象能力:复杂逻辑能被命名、复用、组合。递归 fib 能跑通,正是因为每次 fib(n-1) 调用都开新作用域、各自持有自己的 n,互不干扰。语言的表达力,从这里开始指数级增长。