访问者模式
实现访问者模式,分离数据结构与操作
问题
什么是访问者模式?如何在 JavaScript 中实现?
解答
访问者模式将数据结构与作用于结构上的操作分离,使得可以在不修改数据结构的前提下添加新的操作。
基本实现
// 元素接口 - 接受访问者
class Element {
accept(visitor) {
throw new Error('子类必须实现 accept 方法');
}
}
// 具体元素:文章
class Article extends Element {
constructor(title, wordCount) {
super();
this.title = title;
this.wordCount = wordCount;
}
accept(visitor) {
visitor.visitArticle(this);
}
}
// 具体元素:视频
class Video extends Element {
constructor(title, duration) {
super();
this.title = title;
this.duration = duration; // 分钟
}
accept(visitor) {
visitor.visitVideo(this);
}
}
// 访问者接口
class Visitor {
visitArticle(article) {}
visitVideo(video) {}
}
// 具体访问者:统计访问者
class StatsVisitor extends Visitor {
constructor() {
super();
this.totalWords = 0;
this.totalDuration = 0;
}
visitArticle(article) {
this.totalWords += article.wordCount;
console.log(`统计文章: ${article.title}, ${article.wordCount} 字`);
}
visitVideo(video) {
this.totalDuration += video.duration;
console.log(`统计视频: ${video.title}, ${video.duration} 分钟`);
}
getStats() {
return {
totalWords: this.totalWords,
totalDuration: this.totalDuration
};
}
}
// 具体访问者:导出访问者
class ExportVisitor extends Visitor {
constructor() {
super();
this.result = [];
}
visitArticle(article) {
this.result.push({
type: 'article',
title: article.title,
readTime: Math.ceil(article.wordCount / 300) + ' 分钟'
});
}
visitVideo(video) {
this.result.push({
type: 'video',
title: video.title,
duration: video.duration + ' 分钟'
});
}
getResult() {
return this.result;
}
}
// 对象结构 - 管理元素集合
class ContentLibrary {
constructor() {
this.elements = [];
}
add(element) {
this.elements.push(element);
}
// 接受访问者遍历所有元素
accept(visitor) {
this.elements.forEach(element => element.accept(visitor));
}
}
// 使用示例
const library = new ContentLibrary();
library.add(new Article('JavaScript 基础', 1500));
library.add(new Article('React 入门', 2000));
library.add(new Video('Vue 教程', 30));
library.add(new Video('Node.js 实战', 45));
// 使用统计访问者
const statsVisitor = new StatsVisitor();
library.accept(statsVisitor);
console.log(statsVisitor.getStats());
// { totalWords: 3500, totalDuration: 75 }
// 使用导出访问者 - 无需修改元素类
const exportVisitor = new ExportVisitor();
library.accept(exportVisitor);
console.log(exportVisitor.getResult());
实际应用:AST 遍历
// 简化的 AST 节点
class NumberNode {
constructor(value) {
this.value = value;
}
accept(visitor) {
return visitor.visitNumber(this);
}
}
class BinaryNode {
constructor(operator, left, right) {
this.operator = operator;
this.left = left;
this.right = right;
}
accept(visitor) {
return visitor.visitBinary(this);
}
}
// 计算访问者
class CalculateVisitor {
visitNumber(node) {
return node.value;
}
visitBinary(node) {
const left = node.left.accept(this);
const right = node.right.accept(this);
switch (node.operator) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
}
}
}
// 打印访问者
class PrintVisitor {
visitNumber(node) {
return String(node.value);
}
visitBinary(node) {
const left = node.left.accept(this);
const right = node.right.accept(this);
return `(${left} ${node.operator} ${right})`;
}
}
// 构建 AST: (1 + 2) * 3
const ast = new BinaryNode(
'*',
new BinaryNode('+', new NumberNode(1), new NumberNode(2)),
new NumberNode(3)
);
const calculator = new CalculateVisitor();
const printer = new PrintVisitor();
console.log(ast.accept(printer)); // ((1 + 2) * 3)
console.log(ast.accept(calculator)); // 9
函数式实现
// 更简洁的函数式写法
const createVisitor = (handlers) => ({
visit(node) {
const handler = handlers[node.type];
if (!handler) {
throw new Error(`未知节点类型: ${node.type}`);
}
return handler(node, this);
}
});
// 节点定义
const num = (value) => ({ type: 'number', value });
const binary = (op, left, right) => ({ type: 'binary', op, left, right });
// 计算访问者
const calcVisitor = createVisitor({
number: (node) => node.value,
binary: (node, visitor) => {
const l = visitor.visit(node.left);
const r = visitor.visit(node.right);
const ops = { '+': (a, b) => a + b, '*': (a, b) => a * b };
return ops[node.op](l, r);
}
});
// 使用
const expr = binary('+', num(1), binary('*', num(2), num(3)));
console.log(calcVisitor.visit(expr)); // 7
关键点
- 双重分派:通过
accept和visit两次调用确定具体操作 - 开闭原则:添加新操作只需新增访问者,无需修改元素类
- 适用场景:对象结构稳定但操作经常变化,如 AST 处理、DOM 遍历
- 缺点:添加新元素类型需要修改所有访问者
- 前端应用:Babel 插件、ESLint 规则、编译器实现
目录