在上一篇文章中,我们研究了解释器模式的理论,了解了 AST 树是什么以及如何抽象终结符和非终结符表达式。这次,让我们抛开理论,看看如何将这种模式应用到我们每天都在使用的严肃商业项目中!
剧透:您现在可能正在使用解释器模式,只需在浏览器中阅读此文本即可!
业界使用此模式的最引人注目、也许也是最重要的示例之一是 JavaScript。这种语言最初是“在膝盖上”创建的,如今,由于解释的概念,它可以在数十亿台设备上运行。
改变互联网的 10 天
JavaScript 的历史充满了传奇。 1995 年,Brendan Eich 在 Netscape Communications 工作时,接到的任务是创建一种可以直接在浏览器 (Netscape Navigator) 中运行的简单脚本语言,以使网页具有交互性。管理层想要一种类似于当时超级流行的 Java 语法的东西,但不是针对专业工程师,而是针对网页设计师。
Eich 仅用了10 天来编写该语言的第一个原型,该语言当时被称为 Mocha(然后是 LiveScript,出于营销原因才称为 JavaScript)。这种热潮并非偶然:微软紧随其后,同时也在积极准备自己的脚本语言 VBScript,以便嵌入 Internet Explorer 浏览器中。 Netscape 迫切需要发布回应,以免在迫在眉睫的浏览器战争中落败。
根本没有时间将复杂的编译器编写成机器代码。对于 Eich 来说,显而易见且最快的解决方案是经典解释器的架构。
第一个解释器(SpiderMonkey)的工作方式如下:
- 它从页面读取脚本的文本源代码。
- 词法分析器将文本分解为标记。
- 解析器构建了一个抽象语法树(AST)。就解释器模式而言,这棵树由终端表达式(字符串,数字如42)和非终端(函数调用,语句如If、While)组成。
- 然后虚拟机一步步“遍历”这棵树,在每个节点执行嵌入其中的指令(调用类似于 Interpret() 的方法)。
上下文和对象
还记得在经典实现中我们必须传递给 Interpret(Context context) 方法的 Context 对象吗?解释器需要它来存储当前的内存状态。
就 JavaScript 而言,顶层上下文的角色由全局对象(例如浏览器中的窗口)扮演。例如,当您的 AST 节点尝试通过 document.write(“Hello”) 将文本写入屏幕时,解释器会访问其上下文(文档对象)并调用所需的内部浏览器 API。
多亏了解释器,JavaScript 才能够如此轻松地与 DOM(文档对象模型)进行交互 – 这些都只是上下文中可由树节点访问的对象。
解释器的演变:JIT 编译
从历史上看,浏览器中的 JS 长期以来一直是“纯粹”的解释器。而这有一个很大的缺点——速度慢。每次执行脚本时解析树并缓慢遍历每个节点会减慢复杂的 Web 应用程序的速度。
随着 2008 年 Google V8 引擎(内置于 Chrome 中)的出现,一场革命发生了。工程师们意识到,对于现代网络来说,一个解释器是不够的。引擎变得更加复杂:它仍然构建 AST 树,但现在使用 JIT(即时)编译。
现代 JS 引擎(V8、SpiderMonkey)的工作方式就像一个复杂的管道:
- 快速而愚蠢的基本解释器立即开始执行您的 JS 代码,甚至无需等待它编译(经典模式在这里仍然有效)。
- 同时,引擎会监控代码的“热门”部分(被调用数千次的循环或函数)。
- 这些部分由 JIT 编译器直接编译为优化的机器代码,绕过缓慢的解释器。
正是解释器的即时启动和编译的计算能力的结合让 JavaScript 占领了世界,成为服务器 (Node.js) 和移动应用程序 (React Native) 的语言。
游戏行业的翻译
尽管 C++ 在重型计算领域占据主导地位,但解释器模式仍然是游戏开发中用于创建游戏逻辑的行业标准。为了什么?这样游戏设计师就可以制作游戏,而无需“放弃”引擎或需要不断重新编译引擎的风险。
一个很好的历史例子是UnrealScript – 虚幻竞技场和战争机器游戏的逻辑是在虚幻引擎1、2和3中编写的语言。文本被编译成紧凑的抽象机器字节码,然后由引擎的虚拟机逐步(解释)。
可视化图形脚本(蓝图)
如今,文本已被可视化编程所取代 – 虚幻引擎 4 和 5 中的蓝图系统。
如果您曾经在虚幻引擎中打开过蓝图,您就会看到很多通过电线连接的节点。从架构上来说,整个蓝图图是绘制在屏幕上的巨大抽象语法树 (AST):
- 终端表达式:常量节点。例如,仅存储数字 42 或字符串的节点。它们在解释时返回特定值。
- 非终止表达式:计算节点(添加)或流控制节点(分支)。它们具有参数输入,解释器首先对其进行递归计算,然后将结果作为输出引脚生成。
而上下文在这里的作用是由特定游戏对象(Actor)实例的记忆来扮演的。解释器机器安全地“行走”通过该图,请求数据并执行转换。
解释器还用在什么地方?
解释器模式几乎可以在任何需要执行动态指令的复杂系统中找到。以下只是商业软件中的一些示例:
- 解释型编程语言(Python、Ruby、PHP)。它们的整个运行时基于经典模式。例如,CPython 参考实现首先将您的 .py 脚本解析为 AST,将其编译为字节码,然后一个巨大的虚拟机(计算循环)逐步解释该字节码。
- Java 虚拟机 (JVM)。 最初,Java 代码不是编译成机器指令,而是编译成字节码。当您运行应用程序时,JVM 充当解释器(尽管使用积极的 JIT 编译,就像在 V8 中一样)。
- 数据库和 SQL 当您在 PostgreSQL 或 MySQL 中发出 SQL 查询(SELECT * FROM users)时,数据库引擎充当解释器。它执行词法分析,构建 AST 查询树,生成执行计划,然后通过迭代表的行来逐字“解释”该计划。
- 正则表达式 (RegEx)。任何正则表达式引擎都会在内部将字符串模式(例如 ^\d{3}-\d{2}$)解析为状态图 (NFA/DFA 自动机),然后内部解释器会通过该状态图,将每个输入字符与该图的顶点进行匹配。
- Unity Shader Graph / 虚幻材质编辑器 – 将视觉节点解释为模块化着色器代码 (GLSL/HLSL)。
- Blender 几何节点 – 解释数学和几何运算以按程序实时生成 3D 模型。
总计
解释器模式早已超出了“编写自己的计算器”的范围。这是最有力的行业标准。从每天在浏览器幕后执行数十亿字节代码的 JavaScript 引擎,到无需 C++ 知识即可构建复杂逻辑的游戏设计者,解释器仍然是现代 IT 开发中最重要的架构概念之一。