Appearance
Acorn 解析器课程计划
课程概述
本课程将带你从零开始实现一个 JavaScript 解析器,理解 Acorn 的核心原理。
课程目标
- 理解词法分析的原理
- 掌握语法分析的方法
- 学习 AST 的构建和遍历
- 能够构建自己的解析器
课程结构
8 节课,每节 20-40 分钟,总时长约 90-120 分钟。
第 1 课:Acorn 简介
目标
- 了解 Acorn 的背景
- 理解解析器的概念
- 掌握 Acorn 的核心功能
内容
Acorn 背景
- 什么是 Acorn
- 为什么需要解析器
- Acorn 的历史
核心概念
- 词法分析
- 语法分析
- AST(抽象语法树)
解析器的应用
- 代码转换
- 代码分析
- 代码格式化
实践步骤
bash
# 1. 创建项目
mkdir acorn-course
cd acorn-course
# 2. 初始化项目
npm init -y
# 3. 安装 Acorn
npm install acorn --save预期输出
✓ 项目初始化完成
✓ Acorn 安装完成总结
- ✅ 理解了解析器的概念
- ✅ 掌握了核心功能
- ✅ 了解了应用场景
第 2 课:词法分析
目标
- 理解词法分析的原理
- 实现词法分析器
- 学习标记类型
内容
词法分析
- 什么是词法分析
- 标记类型
- 词法分析算法
实现词法分析器
- 识别标记
- 处理空白
- 处理注释
测试词法分析器
- 单元测试
- 边界测试
实践步骤
bash
# 1. 创建词法分析器
cat > src/tokenizer.js << 'EOF'
export function tokenize(source) {
const tokens = []
let pos = 0
while (pos < source.length) {
const char = source[pos]
// 跳过空白
if (/\s/.test(char)) {
pos++
continue
}
// 识别标记
if (/[a-zA-Z_$]/.test(char)) {
tokens.push(readIdentifier(source, pos))
pos += tokens[tokens.length - 1].value.length
} else if (/[0-9]/.test(char)) {
tokens.push(readNumber(source, pos))
pos += tokens[tokens.length - 1].value.length
} else {
// 其他标记
tokens.push({ type: 'Punctuator', value: char })
pos++
}
}
return tokens
}
function readIdentifier(source, pos) {
let value = ''
while (pos < source.length && /[a-zA-Z0-9_$]/.test(source[pos])) {
value += source[pos]
pos++
}
return { type: 'Identifier', value }
}
function readNumber(source, pos) {
let value = ''
while (pos < source.length && /[0-9.]/.test(source[pos])) {
value += source[pos]
pos++
}
return { type: 'Numeric', value: parseFloat(value) }
}
EOF
# 2. 创建测试
cat > test/tokenizer.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { tokenize } from '../src/tokenizer.js'
describe('词法分析器测试', () => {
it('应该解析代码', () => {
const tokens = tokenize('const a = 1')
assert.ok(Array.isArray(tokens))
})
})
EOF
# 3. 运行测试
npm test预期输出
✓ 应该解析代码总结
- ✅ 实现了词法分析器
- ✅ 理解了标记类型
- ✅ 掌握了词法分析算法
第 3 课:语法分析
目标
- 理解语法分析的原理
- 实现语法分析器
- 学习 AST 构建
内容
语法分析
- 什么是语法分析
- AST 结构
- 语法分析算法
实现语法分析器
- 构建节点
- 构建表达式
- 构建语句
测试语法分析器
- 单元测试
- 复杂测试
实践步骤
bash
# 1. 创建语法分析器
cat > src/parser.js << 'EOF'
import { tokenize } from './tokenizer.js'
export function parse(source) {
const tokens = tokenize(source)
let pos = 0
function parseExpression() {
return parseAssignment()
}
function parseAssignment() {
const left = parseAdditive()
if (match('=')) {
consume()
const right = parseAssignment()
return {
type: 'AssignmentExpression',
operator: '=',
left,
right
}
}
return left
}
function parseAdditive() {
let left = parseMultiplicative()
while (match('+') || match('-')) {
const operator = consume().value
const right = parseMultiplicative()
left = {
type: 'BinaryExpression',
operator,
left,
right
}
}
return left
}
function parseMultiplicative() {
let left = parsePrimary()
while (match('*') || match('/')) {
const operator = consume().value
const right = parsePrimary()
left = {
type: 'BinaryExpression',
operator,
left,
right
}
}
return left
}
function parsePrimary() {
if (match('Identifier')) {
return consume()
}
if (match('Numeric')) {
return consume()
}
if (match('(')) {
consume()
const expr = parseExpression()
expect(')')
return expr
}
}
function match(type) {
return pos < tokens.length && tokens[pos].type === type
}
function consume() {
return tokens[pos++]
}
function expect(type) {
if (!match(type)) {
throw new Error(`Expected ${type}, got ${tokens[pos]?.type}`)
}
return consume()
}
return parseExpression()
}
EOF
# 2. 创建测试
cat > test/parser.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
describe('语法分析器测试', () => {
it('应该解析表达式', () => {
const ast = parse('1 + 2')
assert.strictEqual(ast.type, 'BinaryExpression')
})
})
EOF
# 3. 运行测试
npm test预期输出
✓ 应该解析表达式总结
- ✅ 实现了语法分析器
- ✅ 理解了 AST 结构
- ✅ 掌握了语法分析算法
第 4 课:AST 遍历
目标
- 理解 AST 遍历的原理
- 实现 AST 遍历器
- 学习节点访问
内容
AST 遍历
- 什么是遍历
- 遍历策略
- 访问者模式
实现遍历器
- 深度优先遍历
- 节点访问
- 路径操作
测试遍历器
- 单元测试
- 复杂测试
实践步骤
bash
# 1. 创建遍历器
cat > src/traverse.js << 'EOF'
export function traverse(ast, visitor) {
function visit(node) {
if (!node) return
// 访问当前节点
if (visitor.enter) {
visitor.enter(node)
}
// 遍历子节点
for (const key in node) {
const child = node[key]
if (Array.isArray(child)) {
child.forEach(visit)
} else if (typeof child === 'object' && child !== null) {
visit(child)
}
}
// 离开节点
if (visitor.leave) {
visitor.leave(node)
}
}
visit(ast)
}
EOF
# 2. 创建测试
cat > test/traverse.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
import { traverse } from '../src/traverse.js'
describe('遍历器测试', () => {
it('应该遍历 AST', () => {
const ast = parse('1 + 2')
let visited = false
traverse(ast, {
enter(node) {
if (node.type === 'BinaryExpression') {
visited = true
}
}
})
assert.strictEqual(visited, true)
})
})
EOF
# 3. 运行测试
npm test预期输出
✓ 应该遍历 AST总结
- ✅ 实现了 AST 遍历
- ✅ 理解了遍历策略
- ✅ 掌握了节点访问
第 5 课:代码生成
目标
- 理解代码生成的原理
- 实现代码生成器
- 学习代码格式化
内容
代码生成
- 什么是代码生成
- 生成算法
- 格式化选项
实现代码生成器
- AST 到代码
- 格式化
- 压缩
测试代码生成器
- 单元测试
- 格式化测试
实践步骤
bash
# 1. 创建代码生成器
cat > src/generator.js << 'EOF'
export function generate(ast, options = {}) {
const { minify = false } = options
let code = ''
function visit(node) {
switch (node.type) {
case 'Identifier':
code += node.value
break
case 'Numeric':
code += node.value
break
case 'BinaryExpression':
visit(node.left)
code += node.operator
visit(node.right)
break
case 'AssignmentExpression':
visit(node.left)
code += node.operator
visit(node.right)
break
// ... 其他节点类型
}
}
visit(ast)
if (minify) {
code = code.replace(/\s+/g, ' ').trim()
}
return code
}
EOF
# 2. 创建测试
cat > test/generator.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
import { generate } from '../src/generator.js'
describe('代码生成器测试', () => {
it('应该生成代码', () => {
const ast = parse('1 + 2')
const code = generate(ast)
assert.ok(typeof code === 'string')
})
})
EOF
# 3. 运行测试
npm test预期输出
✓ 应该生成代码总结
- ✅ 实现了代码生成器
- ✅ 理解了生成算法
- ✅ 掌握了代码格式化
第 6 课:作用域分析
目标
- 理解作用域分析的原理
- 实现作用域分析器
- 学习变量声明
内容
作用域分析
- 什么是作用域
- 作用域类型
- 作用域链
实现作用域分析
- 变量声明
- 变量引用
- 作用域嵌套
测试作用域分析
- 单元测试
- 复杂测试
实践步骤
bash
# 1. 创建作用域分析器
cat > src/scope.js << 'EOF'
export function analyzeScope(ast) {
const scopes = []
let currentScope = null
function enterScope() {
const scope = {
parent: currentScope,
declarations: new Set(),
references: new Set()
}
scopes.push(scope)
currentScope = scope
}
function exitScope() {
currentScope = currentScope.parent
}
function declare(name) {
if (currentScope) {
currentScope.declarations.add(name)
}
}
function reference(name) {
if (currentScope) {
currentScope.references.add(name)
}
}
traverse(ast, {
enter(node) {
if (node.type === 'Identifier') {
reference(node.value)
}
}
})
return scopes
}
EOF
# 2. 创建测试
cat > test/scope.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { parse } from '../src/parser.js'
import { analyzeScope } from '../src/scope.js'
describe('作用域分析测试', () => {
it('应该分析作用域', () => {
const ast = parse('a + b')
const scopes = analyzeScope(ast)
assert.ok(Array.isArray(scopes))
})
})
EOF
# 3. 运行测试
npm test预期输出
✓ 应该分析作用域总结
- ✅ 实现了作用域分析
- ✅ 理解了作用域类型
- ✅ 掌握了变量声明
第 7 课:错误处理
目标
- 理解错误处理的原理
- 实现错误处理器
- 学习错误恢复
内容
错误处理
- 什么是错误
- 错误类型
- 错误恢复
实现错误处理
- 错误检测
- 错误报告
- 错误恢复
测试错误处理
- 单元测试
- 错误测试
实践步骤
bash
# 1. 创建错误处理器
cat > src/error.js << 'EOF'
export class ParseError extends Error {
constructor(message, pos, source) {
super(message)
this.name = 'ParseError'
this.pos = pos
this.source = source
}
getLocation() {
const lines = this.source.substring(0, this.pos).split('\n')
const line = lines.length
const column = lines[lines.length - 1].length + 1
return { line, column }
}
}
export function reportError(error) {
const location = error.getLocation()
console.error(`Error at line ${location.line}, column ${location.column}:`)
console.error(error.message)
}
EOF
# 2. 创建测试
cat > test/error.test.js << 'EOF'
import { describe, it } from 'node:test'
import assert from 'node:assert'
import { ParseError } from '../src/error.js'
describe('错误处理测试', () => {
it('应该创建错误', () => {
const error = new ParseError('Test error', 0, 'test')
assert.ok(error instanceof ParseError)
})
})
EOF
# 3. 运行测试
npm test预期输出
✓ 应该创建错误总结
- ✅ 实现了错误处理
- ✅ 理解了错误类型
- ✅ 掌握了错误恢复
第 8 课:总结与扩展
目标
- 总结所有课程
- 回顾关键概念
- 提供扩展建议
内容
课程总结
- 回顾所有功能
- 总结关键概念
- 展示完整代码
扩展建议
- 添加更多功能
- 优化性能
- 支持更多特性
下一步
- 学习 Acorn 源码
- 参与开源项目
- 构建自己的解析器
总结
通过本课程,你学会了:
- ✅ Acorn 简介
- ✅ 词法分析
- ✅ 语法分析
- ✅ AST 遍历
- ✅ 代码生成
- ✅ 作用域分析
- ✅ 错误处理
下一步
- 学习 Acorn 源码
- 添加更多功能
- 参与开源项目
参考资源

扫描二维码关注"架构师AI杜"公众号,获取更多技术内容和最新动态
