zhangdizhangdi

解释器模式

定义

解释器模式(Interpreter Pattern)的核心野心是:“干脆发明一种新的小语言,让业务人员自己写,然后我来负责翻译和执行。”

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

摩斯密码... --- ... 本身只是点和划,电报员(解释器)按照密码本(文法定义)将其解释为 SOS

痛点:当某类极其特定的问题发生频率极高,如果每次都用 JS 代码去硬编码写逻辑,会非常死板。比如:让非技术人员在后台配置一串类似 (A 且 B) 或 C 的复杂折扣计算规则。
解决:将这些规则抽象成一种 特定领域语言(DSL, Domain Specific Language)。定义好各种终结符(如变量 A、B)和非终结符(如且、或),然后写一个解释器去动态求值。

对比

🥊 对比:解释器 vs 组合模式 (Composite Pattern)

在实现上,解释器模式构建出的抽象语法树(AST)本质上就是一个组合模式的树状结构。
区别在于目的:组合模式是为了“统一处理整体和部分的关系”;而解释器模式是为了 “对这棵树进行求值/计算 (Evaluate)”

与设计原则的关系

  1. 单一职责原则 (Single Responsibility Principle)
    • 每一个语法规则(如“加法”、“减法”、“变量提取”)都被封装进一个独立的节点类中。如果要修改加法的逻辑,只改加法类即可。
  2. 开闭原则 (Open-Closed Principle)
    • 如果要引入一种新的语法(比如“乘法”),只需新增一个乘法类,其他老的语法解析代码完全不用动。

实现(手搓一个微型计算器)

要理解解释器,最经典的方式就是用它来解析并计算一个数学表达式。
我们要计算 a + b - c。在解释器模式中,我们认为公式已经被解析成了一棵树(AST),然后我们让树的根节点执行 interpret() 方法。

javascript
// 1. 上下文环境 (Context):存放全局变量和数据
class Context {
  constructor() {
    this.variables = {}; // 存储变量映射,如 { a: 10, b: 5 }
  }
  assign(varName, value) {
    this.variables[varName] = value;
  }
  lookup(varName) {
    return this.variables[varName];
  }
}

// 2. 抽象表达式接口
class AbstractExpression {
  interpret(context) { throw new Error("必须实现 interpret"); }
}

// 3. 终结符表达式 (Terminal) —— 树的叶子节点,也就是变量
class VarExpression extends AbstractExpression {
  constructor(name) {
    super();
    this.name = name;
  }
  // 解释变量:直接去环境里查它的值
  interpret(context) {
    return context.lookup(this.name); 
  }
}

// 4. 非终结符表达式 (Non-Terminal) —— 树的枝干节点,即运算符 (+, -)
class AddExpression extends AbstractExpression {
  constructor(left, right) {
    super();
    this.left = left;   // 左侧表达式
    this.right = right; // 右侧表达式
  }
  // 解释加法:分别求左右两边的值,然后相加
  interpret(context) {
    return this.left.interpret(context) + this.right.interpret(context);
  }
}

class SubExpression extends AbstractExpression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }
  interpret(context) {
    return this.left.interpret(context) - this.right.interpret(context);
  }
}

// ================= 使用测试 =================
// 我们要计算的公式是:a + b - c

// 1. 准备上下文变量
const ctx = new Context();
ctx.assign('a', 10);
ctx.assign('b', 5);
ctx.assign('c', 2);

// 2. 构造语法树 (现实中这步通常由 Parser/词法分析器 自动生成)
// 语法树结构: Sub( Add(a, b), c )
const varA = new VarExpression('a');
const varB = new VarExpression('b');
const varC = new VarExpression('c');

const addExpr = new AddExpression(varA, varB); // a + b
const subExpr = new SubExpression(addExpr, varC); // (a + b) - c

// 3. 开始解释求值!
const result = subExpr.interpret(ctx);
console.log(`计算结果: ${result}`); // 输出: 13 (即 10 + 5 - 2)

前端应用

前面说了,你不怎么会在业务里手写解释器,但你 每天都在疯狂调用别人写好的解释器

1. 正则表达式 (Regular Expressions)

/^[a-z]+@[0-9]+\.com$/ 这是一串天书。
JS 引擎内部内置了一个正则解释器。当你调用 regex.test('abc@123.com') 时,解释器会把正则解析成状态机,然后对目标字符串进行求值验证。

2. Vue / React 的模板解析器 (Template Engine)

Vue 中的 <div v-if="isShow">{{ user.name + '!' }}</div> 并不是合法的 HTML。

  • Vue 的底层包含了一个小型的解释器。
  • 它读取你的模板字符串(特定的微语言),结合你的组件 data(上下文 Context),解释执行后,最终输出合法的虚拟 DOM(VNode)和真实 HTML。

3. 原生的黑魔法:eval()new Function()

JavaScript 引擎本身就是一个庞大的解释器(V8 等)。当你使用 eval("1 + 2 * 3") 时,你其实就是直接把字符串丢给了 JS 自身的终极解释器去执行。

⚠️ 警告:由于安全(XSS注入)和性能问题,前端极其抵制使用 eval。因此,当我们真遇到需要动态执行逻辑的场景时,往往需要自己写一个“安全的小型解释器”。

4. 低代码平台 (Low-Code) 的逻辑编排引擎 (JSONLogic)

这是目前前端高级开发中最常遇到需要手写或引入解释器的场景!
在低代码平台中,业务人员通过拖拽生成了一段逻辑,由于不能直接存 JS 代码(不安全),通常会将其序列化为一个 JSON 格式的抽象语法树。

例如著名的跨端逻辑库 JSONLogic
前端收到后端的 JSON 规则:{ "and" : [ { "<" : [ { "var" : "temp" }, 110 ] }, { "==" : [ { "var" : "pie.filling" }, "apple" ] } ] }
前端引擎引入 JSONLogic 库(本质上就是一个前端的解释器),传入 data = { temp: 100, pie: { filling: 'apple' } },解释器会自动求值返回 true

5. 复杂的搜索查询语句 (DSL)

像 Jira 的搜索框,支持输入 project = "TEST" AND status IN ("Done", "In Progress")
前端通常需要引入一个轻量级的词法分析器,将这串 JQL (Jira Query Language) 语句解析成语法树,然后用解释器模式将其翻译为对后端接口发起的 JSON 过滤参数。

总结

  • 适用场景:当业务中出现大量重复、规则多变且具有强烈逻辑关系(与、或、非、加、减、嵌套)的问题时,与其写成百上千个 if-else,不如发明一种“简单的配置规则语言(DSL)”,然后写一个解释器去执行它。
  • 性能陷阱:对于极度复杂的语法,解释器模式构建的类和树层级会呈爆炸式增长,执行效率低下。此时在工业界,通常会抛弃纯手写的设计模式,转而使用专业的 编译器生成工具(Parser Generator,如 ANTLR、Jison、Peg.js) 来自动生成解释器。