【高频考点精讲】JavaScript中的访问者模式:从AST解析到数据转换的艺术

大家好呀!今天想和大家聊聊一个既实用又有点"高冷"的设计模式------访问者模式。这个模式在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题(速记版)

觉得有用的话别忘了【点赞 】【收藏 】【分享】给朋友,有什么问题在评论区留言,我们下期再见!👋

相关推荐
Moment几秒前
表单验证太复杂?用 Zod 让它变得简单又安全
前端·javascript·typescript
codingandsleeping几秒前
Git 子模块 (Submodule) 使用介绍
前端·git
云端看世界几秒前
ECMAScript 引用记录
前端·javascript
前端涂涂6 分钟前
nodejs导入文件模块和导入文件夹
前端
陈随易9 分钟前
pnpm v10防恶意软件立大功,Bun一个PR最高7万奖金
前端·后端·程序员
云端看世界11 分钟前
ECMAScript 环境记录
前端·javascript
云端看世界16 分钟前
ECMAScript 执行上下文概念和基本操作
前端·javascript·ecmascript 6
Moment16 分钟前
纯前端如何判断用户访问设备以及浏览器?
前端·javascript·面试
Thomas游戏开发17 分钟前
Unity3D动态遮挡剔除技术详解
前端·unity3d·游戏开发
樊小肆17 分钟前
Vue3 在线 PDF 编辑 2.0 撤回、反撤回
前端·vue.js·开源