控制流:if / while 与代码块
目标
表达式语言还不够用。本节课把语言升级成能写"程序":支持 if / else、while 循环、用 {} 包裹的代码块,以及变量重新赋值。我们用它算出 1 到 10 的和。
运行环境:Node.js v18+。运行方式:保存为 flow.js,执行 node flow.js。
// flow.js —— 完整可运行:词法 + 语法 + 环境 + 控制流
const KEYWORDS = new Set(['var', 'if', 'else', 'while'])
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' }[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 === '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 === '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 }
}
throw new Error('未知节点: ' + node.type)
}
function run(src) {
const env = new Environment()
let last
for (const s of parse(src)) last = ev(s, env)
return { last, env }
}
// ===== 调用示例:求 1 到 10 的和 =====
const { env } = run(`
var sum = 0;
var i = 1;
while (i <= 10) {
sum = sum + i;
i = i + 1;
}
`)
console.log('1 到 10 的和 =', env.get('sum')) // 55名词解释
控制流(Control Flow):程序执行的路径分支,靠 if / while / for 等语句改变"下一步执行哪一段"。没有它,代码只能从上往下直线跑。
代码块(Block):用 {} 括起来的一组语句,拥有自己的作用域。进入块时新建 Environment,离开即销毁——这正是 let 块级作用域的来源。
重新赋值(assign):x = x + 1 不是声明,而是沿作用域链找到已有 x 并改写。它和 var x = ...(声明)走的是 assign / define 两条不同路径。
课后练习
练习:用本课的解释器算 1 到 100 中所有偶数的和。
答案:把上面的调用示例换成:
const { env } = run(`
var s = 0;
var i = 1;
while (i <= 100) {
if (i != (i / 2) * 2) { s = s + i; }
i = i + 1;
}
`)
console.log('偶数和 =', env.get('s')) // 2550总结
控制流让"表达式语言"升级为"能解决问题的程序语言"。关键设计有两点:其一,if / while 的 body 通过 parseStmt 复用,所以 if 里可以再套 while、再套 if——语法的递归性在这里体现为"语句能无限嵌套";其二,代码块每次进入都 new 一个 Environment,这让"局部变量只在块内有效"成为天然结果,不需要特殊规则。你写的 while 循环能正确累加,本质上是解释器在反复 ev(node.body),而 assign 沿作用域链把新值写回同一个 i。语言的能力,就是这样一层层叠出来的。