一、词法分析基础:什么是词法分析、词法分析器的作用、与语法分析器的关系

好,咱们今天正式开始聊前端编译的第一道关卡——词法分析。

说实话,我刚开始接触编译原理时,觉得词法分析这玩意儿挺简单的。不就是把代码拆成一个个单词吗?后来真正动手写了一个词法分析器,才发现里面门道不少。今天我就把自己踩过的坑和积累的经验,跟你好好聊聊。

1.1 什么是词法分析?

词法分析,说白了就是把源代码字符串,转换成有意义的词法单元(Token)序列

你想想看,我们写的代码在计算机眼里就是一长串字符。比如:

let age = 25 + 3;

这串字符里,有字母、有数字、有符号。计算机怎么知道 let 是一个关键字,age 是一个变量名,25 是一个数字,+ 是一个运算符?

嗯,这就是词法分析器要干的事。它逐个字符地扫描源代码,按照语言的词法规则,把这些字符组合成一个个有意义的词法单元

我个人习惯把词法分析比作「拆积木」——把一整段代码拆成最小的、有独立含义的积木块。每个积木块就是一个 Token。

核心概念:Token

Token 是词法分析的最小单位,通常包含:

  • 类型:比如关键字、标识符、数字、运算符、分隔符等
  • :具体的文本内容,比如 "let"、"age"、"25"
  • 位置信息:行号、列号,方便报错时定位

1.2 词法分析器的作用

词法分析器(也叫 Lexer 或 Scanner)到底干了哪些活?我总结了一下,主要有这么几个:

  1. 扫描字符:从左到右、逐个字符地读取源代码
  2. 识别 Token:根据词法规则,把字符序列组合成 Token
  3. 跳过无用内容:比如空格、换行、注释,这些对语法分析没意义,直接扔掉
  4. 记录位置:每个 Token 的行号和列号都要记下来,方便后面报错
  5. 处理错误:遇到非法字符时,要能识别并报告

举个例子,咱们用 JavaScript 写一个简单的词法分析器片段:

function lexer(input) {
  const tokens = [];
  let i = 0;
  
  while (i < input.length) {
    const ch = input[i];
    
    // 跳过空格
    if (ch === ' ' || ch === '\n') {
      i++;
      continue;
    }
    
    // 识别数字
    if (/\d/.test(ch)) {
      let num = '';
      while (i < input.length && /\d/.test(input[i])) {
        num += input[i];
        i++;
      }
      tokens.push({ type: 'NUMBER', value: num });
      continue;
    }
    
    // 识别标识符或关键字
    if (/[a-zA-Z]/.test(ch)) {
      let word = '';
      while (i < input.length && /[a-zA-Z0-9]/.test(input[i])) {
        word += input[i];
        i++;
      }
      const type = word === 'let' ? 'KEYWORD' : 'IDENTIFIER';
      tokens.push({ type, value: word });
      continue;
    }
    
    // 识别运算符
    if (ch === '+') {
      tokens.push({ type: 'OPERATOR', value: '+' });
      i++;
      continue;
    }
    
    // 遇到不认识的东西
    throw new Error(`Unexpected character: ${ch} at position ${i}`);
  }
  
  return tokens;
}

console.log(lexer('let age = 25 + 3'));
// 输出: [
//   { type: 'KEYWORD', value: 'let' },
//   { type: 'IDENTIFIER', value: 'age' },
//   { type: 'OPERATOR', value: '=' },
//   { type: 'NUMBER', value: '25' },
//   { type: 'OPERATOR', value: '+' },
//   { type: 'NUMBER', value: '3' }
// ]

我的经验之谈:写词法分析器时,最容易踩的坑是「最长匹配」原则。比如 >= 应该被识别成一个 Token(大于等于),而不是两个独立的 >=。我曾经在这个问题上吃过亏,调试了半天才发现是匹配顺序的问题。

1.3 词法分析与语法分析的关系

这两个家伙是什么关系?我打个比方你就明白了。

词法分析器就像语文老师,它负责把一句话拆成一个个词语(Token)。而语法分析器就像语法老师,它负责检查这些词语组合在一起是否符合语法规则。

举个例子,对于代码 let 123 = x;

  • 词法分析器会输出:KEYWORD(let)NUMBER(123)OPERATOR(=)IDENTIFIER(x)SEMICOLON(;)
  • 语法分析器拿到这些 Token 后,会发现「关键字后面直接跟数字」不符合语法规则,于是报错

它们之间的关系可以用一张图来表示:

源代码 "let age = 25 + 3;" 词法分析器 (Lexer) 逐个字符扫描 Token序列 KEYWORD IDENTIFIER ... 语法分析器 (Parser) 检查语法结构 AST 抽象语法树

从这张图你能看到,词法分析是语法分析的前置步骤。词法分析器把源代码「嚼碎」成 Token,然后喂给语法分析器。语法分析器再根据语法规则,把这些 Token 组装成一棵抽象语法树(AST)。

注意:虽然词法分析和语法分析是分开的两个阶段,但有些编译器会把它们合并在一起,边扫描边解析。这种做法叫「单遍编译」,效率更高,但实现起来也更复杂。我个人建议初学者先把两者分开实现,等理解了再考虑优化。

1.4 为什么需要词法分析?

你可能会问:为什么不直接让语法分析器处理原始字符串?

嗯,原因有这么几个:

  • 简化设计:语法分析器不用关心空格、注释这些细节,专注处理语法结构就行
  • 提高效率:词法分析可以用更简单的算法(比如有限状态机)快速处理字符
  • 方便报错:词法分析阶段就能发现非法字符,提前报错,不用等到语法分析
  • 模块化:词法分析和语法分析解耦,可以独立修改和测试

我记得有一次在项目中,需要支持一种新的注释格式。因为词法分析和语法分析是分开的,我只需要改词法分析器的代码,语法分析器完全不用动。这种模块化的好处,在实际工程中特别明显。

1.5 词法分析器的输出

词法分析器的输出就是 Token 流。每个 Token 通常包含以下信息:

字段 说明 示例
type Token 的类型 KEYWORD, IDENTIFIER, NUMBER
value Token 的文本值 "let", "age", "25"
line 所在行号 1
column 所在列号 5

避坑指南:我曾经在写词法分析器时,忘了记录 Token 的位置信息。结果语法分析报错时,只能报「第几行有错误」,但具体是哪个 Token 出问题完全不知道。后来我花了整整一天才定位到 bug。从那以后,我写任何词法分析器,第一件事就是加上位置信息。

1.6 小结

词法分析是整个编译过程的第一步,也是最容易理解的一步。它把源代码字符串转换成 Token 序列,为后续的语法分析打好基础。

说白了,词法分析器就是一个「拆词器」。它不关心这些词组合在一起有没有意义,只关心每个词本身是不是合法的。至于词与词之间的组合规则,那是语法分析器的事。

下一章,我们会深入讨论如何用正则表达式有限状态机来构建一个真正的词法分析器。到时候我会分享一些我在实际项目中用到的技巧和踩过的坑。


公众号:蓝海资料掘金营,微信deep3321