大家好呀!今天想和大家聊聊一个既实用又有点"高冷"的设计模式------访问者模式。这个模式在AST解析、Babel插件开发中无处不在,但很多同学可能一直没搞明白它到底妙在哪里。
一、生活中的访问者模式
想象一下你开了一家奶茶店,店里来了几位顾客:
- 程序员小王:只看配料表里的咖啡因含量
- 健身达人小李:只关心热量和糖分
- 环保人士小张:只检查杯子是不是可降解材料
每个顾客都只"访问"他们关心的部分,这就是访问者模式的精髓------将操作与对象结构分离。
二、什么是访问者模式?
官方定义太拗口,我用人话解释:访问者模式允许你在不修改对象结构的情况下,定义新的操作。
举个代码例子,假设我们有一个简单的DOM树:
javascript
class Element {
constructor(name, children = []) {
this.name = name
this.children = children
}
// 关键方法:接受访问者
accept(visitor) {
visitor.visit(this)
this.children.forEach(child => child.accept(visitor))
}
}
class TextNode {
constructor(content) {
this.content = content
}
accept(visitor) {
visitor.visit(this)
}
}
现在我们可以创建不同的访问者来做不同的事情:
javascript
// 创建一个打印访问者
class Printer {
visit(node) {
if (node instanceof Element) {
console.log(`元素节点: ${node.name}`)
} else {
console.log(`文本节点: ${node.content}`)
}
}
}
// 使用示例
const domTree = new Element('div', [
new Element('p', [new TextNode('Hello')]),
new TextNode('World')
])
domTree.accept(new Printer())
// 输出:
// 元素节点: div
// 元素节点: p
// 文本节点: Hello
// 文本节点: World
三、为什么需要访问者模式?
1. 场景分析
假设我们要实现一个Babel插件,需要对AST进行多种操作:
- 代码压缩(删除注释、缩短变量名)
- 代码转换(ES6转ES5)
- 代码分析(计算复杂度)
没有访问者模式时,我们可能这样写:
javascript
function traverse(ast) {
// 处理变量声明
if (ast.type === 'VariableDeclaration') {
// 压缩逻辑
// 转换逻辑
// 分析逻辑
}
// 处理函数声明...
}
所有逻辑耦合在一起,像一锅大杂烩!
2. 访问者模式的优势
使用访问者模式后:
javascript
// 压缩访问者
class Minifier {
VariableDeclaration(node) {
// 只关心压缩逻辑
}
}
// 转换访问者
class Transformer {
VariableDeclaration(node) {
// 只关心转换逻辑
}
}
// 分别应用
ast.visit(new Minifier())
ast.visit(new Transformer())
就像把瑞士军刀的不同工具拆分开,每个工具专注一件事。
四、实战:实现一个简易Babel插件
让我们用访问者模式实现一个真实案例:把所有console.log替换为alert。
1. 定义AST节点类型
javascript
const ast = {
type: 'Program',
body: [{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'console' },
property: { type: 'Identifier', name: 'log' }
},
arguments: [{ type: 'Literal', value: 'Hello' }]
}
}]
}
2. 创建转换访问者
javascript
class ConsoleToAlertVisitor {
// 处理成员表达式(console.log)
MemberExpression(node) {
if (node.object.name === 'console' && node.property.name === 'log') {
node.object.name = '' // 清空console
node.property.name = 'alert' // 改为alert
}
}
}
// 简单的遍历函数
function traverse(node, visitor) {
if (typeof visitor[node.type] === 'function') {
visitor[node.type](node)
}
// 递归遍历子节点
for (const key in node) {
if (typeof node[key] === 'object' && node[key] !== null) {
traverse(node[key], visitor)
}
}
}
// 应用访问者
traverse(ast, new ConsoleToAlertVisitor())
console.log(JSON.stringify(ast, null, 2))
转换后的AST中,console.log已经被替换成了alert!
五、访问者模式在流行库中的应用
1. Babel中的访问者
Babel的插件系统就是基于访问者模式:
javascript
export default function() {
return {
visitor: {
Identifier(path) {
// 处理所有标识符
},
FunctionDeclaration(path) {
// 处理函数声明
}
}
}
}
2. ESLint中的访问者
ESLint规则也是类似原理:
javascript
module.exports = {
create(context) {
return {
VariableDeclarator(node) {
if (node.id.name.length < 3) {
context.report(node, '变量名太短啦!')
}
}
}
}
}
六、访问者模式的优缺点
👍 优点:
- 符合开闭原则:新增操作不用修改原有结构
- 职责分离:每种访问者只关注自己的逻辑
- 集中管理:相关操作集中在同一个访问者中
👎 缺点:
- 破坏封装:需要暴露对象内部结构
- 增加复杂度:简单场景可能过度设计
- 不适用于频繁变更的结构:每次结构变化都要改所有访问者
七、课后思考题
下面这段代码使用访问者模式实现了一个简单的计算器,但输出结果不符合预期,你能找出问题并修复吗?
javascript
class NumberNode {
constructor(value) {
this.value = value
}
accept(visitor) {
return visitor.visitNumber(this)
}
}
class AddNode {
constructor(left, right) {
this.left = left
this.right = right
}
accept(visitor) {
return visitor.visitAdd(this)
}
}
class CalculatorVisitor {
visitNumber(node) {
return node.value
}
visitAdd(node) {
return node.left.accept(this) + node.right.accept(this)
}
}
// 计算: 1 + 2 + 3
const expr = new AddNode(
new NumberNode(1),
new AddNode(new NumberNode(2), new NumberNode(3))
)
console.log(expr.accept(new CalculatorVisitor())) // 预期输出6,实际输出?
把你的答案写在评论区吧!我会随机抽几位同学的答案进行点评哦~
八、最后总结
访问者模式就像是一个专业团队中的专家顾问:
- AST是你要审计的公司
- 每个访问者是不同领域的专家(财务、法律、技术)
- 专家们轮流审计,各自出具专业报告
这下应该理解了吧!
掌握这个模式后,你再去看Babel插件、ESLint规则,会有种豁然开朗的感觉。下次遇到需要处理复杂对象结构的场景,不妨考虑下访问者模式这个神器!
往期精选
【初级】前端开发工程师面试100题(一)
【初级】前端开发工程师面试100题(二)
【初级】前端开发工程师的面试100题(速记版)
觉得有用的话别忘了【点赞 】【收藏 】【分享】给朋友,有什么问题在评论区留言,我们下期再见!👋