访问者模式
定义
在日常的前端业务开发(如写 Vue/React UI 组件)中,你几乎碰不到需要手写 访问者模式(Visitor Pattern) 的场景。但在 前端工程化、底层工具链(Babel、ESLint、Webpack) 领域,它却是绝对的主宰。
封装一些作用于某种数据结构中各元素的操作。它可以在不改变数据结构的前提下,定义作用于这些元素的新的操作。
动物园的访客:假设有一座动物园,里面有猴子、狮子(稳定的数据结构中的具体元素)。现在有不同职业的人来访问它们:
- 如果是饲养员(Visitor A) 来访问,对猴子的操作是“喂香蕉”,对狮子是“喂肉”。
- 如果是兽医(Visitor B) 来访问,对猴子的操作是“量体温”,对狮子是“打麻醉做体检”。
- 如果是游客(Visitor C) 来访问,操作就是“拍照”。
核心精髓:动物本身的种类是非常稳定的(很少突然新增外星生物),但是针对这些动物的“操作/行为”是千变万化的。访问者模式把这些千变万化的行为从动物类中抽离了出来。
痛点:如果把“喂食”、“体检”、“拍照”这些业务逻辑全部塞进具体的动物类里面,动物类会变得极其臃肿,而且每次新增一种行为(比如“查户口”),都要把所有动物类改一遍。
解决:让数据结构(动物)只做一件事:对外提供一个接待入口(accept 方法),允许访问者进来。具体的业务操作完全交给具体的访问者类去实现。
与设计原则及核心机制
- 倾斜的开闭原则 (Open-Closed Principle):
- 新增操作(新增访问者)极其容易:这是它最大的优点。直接写一个新的 Visitor 类即可,数据结构一行代码都不用改。
- 新增元素极其困难:这是它最大的缺点。如果动物园突然引进了一只“企鹅”,那你必须去修改所有已存在的 Visitor 类(给饲养员、兽医、游客都要加上
visit企鹅的方法),这违反了开闭原则。因此它只适用于数据结构极其稳定的场景。
- 单一职责原则 (Single Responsibility Principle):
- 将“数据结构的存储”与“数据上的算法逻辑”完全剥离开来。
- 核心机制:双重分派 (Double Dispatch):
- 大多数高级语言(包括 JS)只支持单分派(根据对象的实际类型决定调用哪个方法)。
- 访问者模式通过两步回调实现了双重分派:第一步,元素调用
visitor.visit(this)把自己暴露给访问者;第二步,访问者根据传入的this类型,执行对应的逻辑。
经典 OOP 实现
因为 JavaScript 是弱类型语言,不支持像 Java 那样通过方法重载(同名方法接受不同参数类型)来实现动态绑定,所以我们在 JS 中通常需要显式地指定方法名(如 visitMonkey)。
// ================= 1. 数据结构(稳定不变) =================
// 动物抽象类
class Animal {
// 提供一个“接待访问者”的接口,要求子类必须实现
accept(visitor) {}
}
class Monkey extends Animal {
constructor(name) { super(); this.name = name; }
accept(visitor) {
// 关键机制:双分派。把自己(this)交出去
visitor.visitMonkey(this);
}
}
class Lion extends Animal {
constructor(name) { super(); this.name = name; }
accept(visitor) {
visitor.visitLion(this);
}
}
// ================= 2. 访问者(千变万化) =================
// 饲养员访问者
class FeederVisitor {
visitMonkey(monkey) {
console.log(`饲养员喂猴子 ${monkey.name} 吃香蕉 🍌`);
}
visitLion(lion) {
console.log(`饲养员喂狮子 ${lion.name} 吃生肉 🥩`);
}
}
// 兽医访问者
class VetVisitor {
visitMonkey(monkey) {
console.log(`兽医给猴子 ${monkey.name} 测量心率 🩺`);
}
visitLion(lion) {
console.log(`兽医给狮子 ${lion.name} 远距离吹麻醉镖 🎯`);
}
}
// ================= 使用测试 =================
// 1. 构建稳定的对象结构
const elements =[new Monkey("吉吉国王"), new Lion("辛巴")];
// 2. 实例化各种不同的访问者
const feeder = new FeederVisitor();
const vet = new VetVisitor();
// 3. 让结构去“接待”访问者
console.log('--- 到了开饭时间 ---');
elements.forEach(animal => animal.accept(feeder));
console.log('\n--- 到了体检时间 ---');
elements.forEach(animal => animal.accept(vet));
前端应用
刚才提到,日常写页面用不到访问者模式,但如果你要做前端基建、写插件、做编译分析,这就是必须跨越的一座大山。
1. 前端编译器的灵魂 —— AST(抽象语法树)与 Babel
这是访问者模式在前端最伟大、最著名的应用。
当 Babel 编译你的 JavaScript 代码时(比如把 ES6 转成 ES5),它的过程分为三步:
- Parse:把源码解析成一棵极其庞大的 AST(抽象语法树)。树上的节点有
Identifier(标识符)、IfStatement(if语句)、FunctionDeclaration(函数声明)等。 - Transform:遍历这棵树,对节点进行修改(这就是我们要干活的地方)。
- Generate:把修改后的 AST 重新转成代码字符串。
这棵 AST 就是前面提到的极其稳定的数据结构(因为 JS 语言标准的节点类型是不怎么变的)。
而我们写的 每一个 Babel 插件,本质上就是一个 Visitor(访问者)!
你看 Babel 插件的标准写法,完全就是状态机+访问者模式的融合:
// 一个真实的 Babel 插件骨架(本质上就是一个 Visitor 访问者对象)
module.exports = function(babel) {
return {
// 这个对象就是访问者 Visitor
visitor: {
// 当遍历到“标识符节点”时,执行此逻辑 (相当于 visitIdentifier)
Identifier(path) {
if (path.node.name === 'foo') {
path.node.name = 'bar'; // 把代码里所有的 foo 变量改成 bar
}
},
// 当遍历到“箭头函数节点”时,执行此逻辑 (相当于 visitArrowFunctionExpression)
ArrowFunctionExpression(path) {
// ...执行将箭头函数转为普通 function 的逻辑
}
}
};
};
你看,Babel 内部已经写好了如何去遍历 AST(类似于上面例子中的 elements.forEach(accept)),你只需要提供一个 Visitor 对象。在这个对象里,节点名字就是你的 visitXXX 方法。Babel 遍历到什么节点,就会去你的 Visitor 里找对应的处理函数。
2. ESLint 插件开发
ESLint 的底层原理和 Babel 高度相似。它也是将你的代码解析成 AST,然后让你编写规则。
一条 ESLint 规则(Rule),实质上也就是一个 Visitor 访问者。
// ESLint 规则示例:禁止使用 console
module.exports = {
create: function(context) {
// 返回一个 Visitor 对象
return {
// 访问者访问到“成员表达式 (比如 console.log)”时
MemberExpression(node) {
if (node.object.name === "console") {
context.report({
node,
message: "不允许使用 console,请删除!"
});
}
}
};
}
};
3. Vue / React 的模板编译器 (Template Compiler)
Vue 把你的 <template> 解析成 AST,也是通过访问者模式(Vite/Vue 源码底层的 transform 函数内部也采用了 Visitor 设计),专门写一些访问者去访问诸如 v-if、v-for 的节点指令,并将其转换为对应的渲染函数(Render Function)。
总结
访问者模式非常极端。
- 不要用它:如果你处理的是普通的业务对象(比如订单表单、商品列表),且你的数据结构类型天天在变。
- 必须用它:如果你处理的是一棵包含多种稳定类型的复杂树状结构(如 AST、DOM 树、组织架构树、文件目录系统),并且你需要在这棵树上执行各种各样毫不相干的独立操作(如提取、转换、检查、打包)。
理解访问者模式,是你从“前端页面仔”跨向“前端工程化/架构师(理解 AST 机制)”的重要门票。