词法分析器:把源码切成 Token 流
目标
当你写下 var x = 1 + 2 * 3; 时,计算机并不知道这是"代码"。它先要被切成一块块带语义的词(Token)。本节课你亲手写一个 Lexer(词法分析器),理解"字符串 → 词流"这一步到底发生了什么。
运行环境:Node.js v18+(无需任何第三方依赖)。
运行方式:把下面代码保存为 lexer.js,执行 node lexer.js。
// lexer.js —— 把源码字符串切成 Token 流
const KEYWORDS = new Set(['var', 'if', 'else', 'while', 'func', 'return', 'print'])
function tokenize(src) {
const tokens = []
let i = 0
const isDigit = (c) => c >= '0' && c <= '9'
const isAlpha = (c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'
while (i < src.length) {
const c = src[i]
// 1) 跳过空白
if (c === ' ' || c === '\n' || c === '\t' || c === '\r') { i++; continue }
// 2) 数字(连续数字拼成一个 NUMBER)
if (isDigit(c)) {
const start = i
while (i < src.length && isDigit(src[i])) i++
tokens.push({ type: 'NUMBER', value: Number(src.slice(start, i)) })
continue
}
// 3) 标识符 / 关键字(字母或下划线开头,后接字母数字)
if (isAlpha(c)) {
const start = i
while (i < src.length && (isAlpha(src[i]) || isDigit(src[i]))) i++
const word = src.slice(start, i)
if (KEYWORDS.has(word)) tokens.push({ type: 'KEYWORD', value: word })
else tokens.push({ type: 'IDENT', value: word })
continue
}
// 4) 单字符运算符
const one = {
'+': 'PLUS', '-': 'MINUS', '*': 'STAR', '/': 'SLASH',
'(': 'LPAREN', ')': 'RPAREN', '=': 'EQ', ';': 'SEMI',
}[c]
if (one) { tokens.push({ type: one, value: c }); i++; continue }
throw new Error('无法识别的字符: ' + c + ' (位置 ' + i + ')')
}
tokens.push({ type: 'EOF', value: null }) // 哨兵:表示源码结束
return tokens
}
// ===== 调用示例 =====
const code = 'var x = 1 + 2 * 3;'
console.log(JSON.stringify(tokenize(code)))运行后你会看到一串 Token:KEYWORD(var)、IDENT(x)、EQ(=)、NUMBER(1)、PLUS(+)……这正是编译器读你代码时"看到"的东西。
名词解释
Token(词法单元):源码里不可再分的最小语义单位,比如一个数字 42、一个变量名 x、一个运算符 +。它通常带 type(是什么种类的词)和 value(具体值)。
Lexer / 词法分析器:把原始字符流切成 Token 流的程序。它解决"哪里是一个词的边界"的问题,但不关心词与词之间的语法关系(那是 Parser 的活)。
常用方法:tokenize(src) 输入源码、输出 Token 数组;很多实现还会顺手跳过空白与注释(本例跳过空白)。
课后练习
练习:给 Lexer 增加对 ==(双等号相等判断)的识别,使其输出一个 OP(==) 的 Token。
答案:把 = 单独处理,遇到 == 合并成一个 Token;下面是可独立运行的小例子:
function tokenizeWithEq(src) {
const tokens = []; let i = 0
while (i < src.length) {
const c = src[i]
if (c === '=') {
if (src[i + 1] === '=') { tokens.push({ type: 'OP', value: '==' }); i += 2; continue }
tokens.push({ type: 'EQ', value: '=' }); i++; continue
}
tokens.push({ type: 'CHAR', value: c }); i++; continue
}
return tokens
}
console.log(tokenizeWithEq('a == b'))总结
词法分析是所有编程语言处理的第一道门。它看似只是"切字符串",却奠定了对整个语言的第一个判断:哪些字符组合是合法的"词"。现实中你几乎不会手写 Lexer——会用正则工具(如 Flex)或现成的 tokenizer,但亲手写一遍的价值在于:你会真正理解"为什么 1..0 在某些语言里是语法错误""为什么注释能被安全删掉而不影响语义"。Token 流是把"给人读的文本"翻译成"给机器处理的结构"的关键一步,没有它,后面的语法树就无从建起。