资讯动态

mathjs 表达式解析与求值完全指南:evaluate、compile、parse 与 Parser 实战

发布时间:2026/9/22 11:27:11 来源:尧图企业网站定制
mathjs 表达式解析与求值完全指南evaluate、compile、parse 与 Parser 实战【免费下载链接】mathjsAn extensive math library for JavaScript and Node.js项目地址: https://gitcode.com/gh_mirrors/ma/mathjs导读表达式解析与求值是 mathjs 的核心能力它提供了一套接近 JavaScript 但更贴合数学习惯的表达式语法如^表示幂、支持单位换算与隐式乘法并给出四条由浅入深的使用路径——math.evaluate即算即得、math.compile一次编译多次求值、math.parse获取可遍历可导出的表达式树、math.parser()携带内存作用域的状态化解析器。读完本文你将掌握这四种 API 的调用方式、作用域Scope对象与 Map 的传入机制、解析器内部的分词与语法树构建流程以及序列化与安全相关的边界注意事项。四种解析求值方式概览在 docs/expressions/parsing.md 中mathjs 官方将表达式的解析与求值归纳为四种途径方式入口特点函数math.evaluate(expr [, scope])src/expression/function/evaluate.js一步到位解析、编译、求值一气呵成函数math.compile(expr)src/expression/function/compile.js编译后返回带evaluate方法的代码对象可反复求值函数math.parse(expr)src/expression/parse.js返回表达式树根节点可编译、遍历、导出math.parser()创建解析器src/expression/Parser.js自带内存作用域提供evaluate及变量增删查改的便捷方法从源码可以确认四者的调用关系evaluate的实现是parse(expr).compile().evaluate(scope)见 evaluate.jscompile的实现是parse(expr).compile()见 compile.js而Parser.evaluate则是evaluate(expr, this.scope)见 Parser.js。因此parse → compile → evaluate 是整条链路的内核其余 API 都是其上的封装。math.evaluate一步到位的即时求值语法签名math.evaluate(expr) math.evaluate(expr, scope) math.evaluate([expr1, expr2, expr3, ...]) math.evaluate([expr1, expr2, expr3, ...], scope)第一个参数接受单个表达式字符串也可以是字符串数组源码中通过typed分派到Array | Matrix重载并用deepMap逐项求值见 evaluate.js因此传入Matrix同样可行。第二个可选参数是scope作用域用于存放和解析表达式中的变量与函数。官方推荐使用 JavaScript 原生Map同时也接受普通对象或任何实现了Map接口具备get、set、keys、has方法的自定义类。表达式中的赋值如c 2.3 4.5会写回该作用域。基础示例// evaluate expressions math.evaluate(sqrt(3^2 4^2)) // 5 math.evaluate(sqrt(-4)) // 2i math.evaluate(2 inch to cm) // 5.08 cm math.evaluate(cos(45 deg)) // 0.7071067811865476 // provide a scope let scope { a: 3, b: 4 } math.evaluate(a * b, scope) // 12 math.evaluate(c 2.3 4.5, scope) // 6.8 scope.c // 6.8注意示例中的2 inch to cm与cos(45 deg)它们展示了 mathjs 表达式语法独有的能力——单位换算to运算符与带单位的三角函数这些是纯 JavaScript 不具备的。未传 scope 时的内部行为当省略 scope 时源码会调用createEmptyMap()新建一个空的Map参与求值见 evaluate.js赋值结果因此被丢弃。若想让表达式中的赋值持久化务必显式传入 scope。math.compile一次编译反复求值语法签名math.compile(expr) math.compile([expr1, expr2, expr3, ...])compile是「先 parse 再 compile」的快捷方式。它返回一个包含evaluate([scope])方法的代码对象const code math.compile(expr) // compile an expression const result code.evaluate([scope]) // evaluate the code with an optional scope传入数组时返回由多个代码对象组成的数组源码中同样用deepMap逐项处理见 compile.js。为什么需要编译一个表达式只需要编译一次之后可以反复求值、且每次可以使用不同的 scope。当你在循环、事件回调或热路径中反复计算同一表达式时这种「先编译、后求值」的模式能显著减少重复的解析与代码生成开销。// parse an expression into a node, and evaluate the node const code1 math.compile(sqrt(3^2 4^2)) code1.evaluate() // 5 let scope { a: 3, b: 4 } const code2 math.compile(a * b) code2.evaluate(scope) // 12 scope.a 5 // 修改作用域后无需重新编译 code2.evaluate(scope) // 20编译背后的机制Node.compile()是整条链路的真正执行者见 Node.js。其内部逻辑是调用各节点实现类如OperatorNode、SymbolNode的_compile(mathWithTransform, {})方法把表达式树预编译成一个接受(scope, args, context)的 JavaScript 闭包函数把能在编译期确定的计算尽可能提前算好返回{ evaluate }对象evaluate内部通过createMap(scope)将传入的对象包装成内部可用的 Map 接口见 map.js并调用_validateScope校验作用域中是否有保留关键字见 Node.js。因此code.evaluate(scope)与node.evaluate(scope)在效果上是等价的——后者的实现就是this.compile().evaluate(scope)见 Node.js。math.parse获取表达式树语法签名math.parse(expr) math.parse([expr1, expr2, expr3, ...])parse返回表达式的根节点Node节点树可以进一步编译、求值、遍历与导出const node math.parse(expr) // parse expression into a node tree const code node.compile() // compile the node tree const result code.evaluate([scope]) // evaluate the code with an optional scope节点 API 的完整说明见 docs/expressions/expression_trees.md。完整示例// parse an expression into a node, and evaluate the node const node1 math.parse(sqrt(3^2 4^2)) const code1 node1.compile() code1.evaluate() // 5 // provide a scope const node2 math.parse(x^a) const code2 node2.compile() let scope { x: 3, a: 2 } code2.evaluate(scope) // 9 // change a value in the scope and re-evaluate the node scope.a 3 code2.evaluate(scope) // 27导出为字符串与 LaTeX解析后的表达式树支持两种文本导出node.toString()输出带括号的数学文本node.toTex()输出 LaTeX 代码可配合浏览器中的 MathJax 等渲染库实现公式的漂亮排版。// parse an expression const node math.parse(sqrt(x/x1)) node.toString() // returns sqrt((x / x) 1) node.toTex() // returns \sqrt{ {\frac{x}{x} }{1} }解析器的内部实现从字符到节点树math.parse的实现在 src/expression/parse.js 中其内部采用递归下降解析器recursive descent parser核心流程分三层1. 词法分析TokenizationgetToken逐个读取字符并归类为 Token。源码中定义了TOKENTYPE枚举NULL空、DELIMITER分隔符/运算符、NUMBER数字、SYMBOL符号、UNKNOWN未知会直接抛语法错误。DELIMITERS表中列出了全部支持的运算符与定界符包括 - * / ^ % !、矩阵定界符[ ] { } ( )、点运算符.* ./ .^、移位 、比较 ! 、逻辑 | ^|、空值合并??、可选链?.等NAMED_DELIMITERS则把mod、to、in、and、xor、or、not识别为命名运算符见 parse.js。词法层还负责处理#注释、空白、0b/0o/0x进制字面量、科学计数法2.3e-4、Unicode 数学符号含希腊字母与数学字母数字符号的代理对判断等。2. 语法分析Parsing按优先级从低到高逐级下降解析parseStart → parseBlock → parseAssignment → parseConditional → parseLogicalOr/Xor/And → parseBitwiseOr/Xor/And → parseRelational → parseShift → parseConversion → parseRange → parseAddSubtract → ...。每个函数消费对应优先级的 Token 并构造对应的节点类型AssignmentNode变量赋值a 2/3、矩阵子集赋值A[1,2] 4、函数赋值f(x) x^2、ConditionalNode三元? :、OperatorNode二元/一元运算、RelationalNode链式比较a b c、RangeNode区间start:step:end、BlockNode多行/分号分隔的块等。所有节点类位于 src/expression/node/。3. 语义构造解析完成后parseStart会检查表达式末尾是否有残留 Token若有则抛出「Unexpected operator / Unexpected part」的语法错误见 parse.js。值得注意的解析细节to与in都被映射为to运算in是to的别名见 parse.js但当in处于表达式末尾后面跟])},;之一时会被识别为英寸单位而非换算运算符见 parse.js——这正是2 inch to cm中单位语法能够成立的原因。此外解析器还支持math.parse(expr, options)传入自定义节点options.nodes见 parse.js用于扩展语法。math.parser()携带作用域的状态化解析器创建与函数清单const parser math.parser()与静态函数不同Parser内部维护一个常驻内存的 scope源码中通过Object.defineProperty定义为只读的Map见 Parser.js所有求值都读写该作用域。Parser提供以下方法方法作用clear()彻底清空解析器的 scopeevaluate(expr)求值一个表达式返回结果get(name)从 scope 中取出变量或函数不存在时返回undefinedgetAll()返回包含 scope 中全部变量的普通对象getAllAsMap()返回包含 scope 中全部变量的Mapremove(name)从 scope 中删除变量或函数set(name, value)在 scope 中设置变量或函数对应实现见 Parser.jsgetAll通过toObject(this.scope)把 Map 转成对象set会先用isValidVariableName校验变量名仅允许字母、下划线、$、Unicode 字母与数字见 Parser.jsremove调用scope.delete(name)。使用示例// create a parser const parser math.parser() // evaluate expressions parser.evaluate(sqrt(3^2 4^2)) // 5 parser.evaluate(sqrt(-4)) // 2i parser.evaluate(2 inch to cm) // 5.08 cm parser.evaluate(cos(45 deg)) // 0.7071067811865476 // define variables and functions parser.evaluate(x 7 / 2) // 3.5 parser.evaluate(x 3) // 6.5 parser.evaluate(f(x, y) x^y) // f(x, y) parser.evaluate(f(2, 3)) // 8 // get and set variables and functions const x parser.get(x) // x 3.5 const f parser.get(f) // function const g f(3, 3) // g 27 parser.set(h, 500) parser.evaluate(h / 2) // 250 parser.set(hello, function (name) { return hello, name ! }) parser.evaluate(hello(user)) // hello, user! // clear defined functions and variables parser.clear()这段示例体现了Parser的核心价值f(x, y) x^y定义的自定义函数不仅能在解析器内被调用还能被parser.get(f)取出来当作普通 JavaScript 函数使用同理parser.set注入的 JavaScript 函数也能在表达式语法中直接调用。math.parser()是new Parser()的便捷封装二者等价见 Parser.js 的注释说明。此外Parser是 mathjs 的类之一其类工厂声明在 src/expression/Parser.js 中带isClass: true标记。Scope作用域机制深入作用域的本质与三种形态作用域Scope是表达式读写变量与函数的数据结构通过math.evaluate、math.compile、math.parse求值或simplify等函数传入。根据 docs/expressions/parsing.md 的说明它有三种形态普通 JavaScript 对象Plain Object使用最方便Map更安全官方推荐任意实现了get/set/has/keys方法的自定义类最灵活。无论哪种形态mathjs 都保证尽量复用你传入的同一个对象不复制使你能在表达式执行后收集脚本写入的变量定义。内部包装机制从 src/utils/map.js 的源码可以确认两条关键机制ObjectWrappingMap内部函数只能使用 Map 接口因此当 scope 是普通对象时mathjs 会将其包装为ObjectWrappingMap见 map.js。包装层通过getSafeProperty/setSafeProperty读写属性并过滤__proto__等危险属性见 map.js这正是对象形态 scope 安全性设计的落点。PartitionedMap当表达式中定义了自定义函数如f(x) x^2时求值阶段会创建子作用域createSubScope(parentScope, args)把函数参数读写到临时 Map其余变量读写回原 scope见 src/utils/scope.js 与 map.js。这样函数体内对参数名的赋值不会污染外部作用域而其他变量仍能读写原对象。安全边界黑名单与保留关键字使用普通对象作为 scope 时evaluate会拒绝表达式使用黑名单中的符号防止 mathjs 表达式逃逸到 JavaScript例如访问constructor、__proto__等危险属性改用Map作为 scope 则可以摆脱该黑名单的约束让表达式自由定义任意名称的变量与函数无论哪种形态_validateScope都会检查 scope 中是否包含保留关键字end见 Node.js 与 src/expression/keywords.js存在即抛错。自定义作用域实战官方在 examples/advanced/custom_scope_objects.js 中给出了四种作用域用法裸对象、原生Map、只实现最小 Map 接口的CustomMap、以及带父作用域链的AdvancedCustomMap。自定义类只需实现get/set/has/keys四个方法即可被 mathjs 当作 scope 使用这让「链式父作用域」「惰性解析」「按需加载变量」等灵活场景成为可能。更完整的表达式安全讨论见 docs/expressions/security.md。序列化把解析器状态存下来再还原mathjs 的所有数据类型都可以被序列化因此包含变量的 scope 也可以安全序列化。但存在一个例外表达式解析器允许定义自定义函数如f(x) x^2这类函数不能脱离作用域单独序列化——它可能绑定着作用域中的其他变量。好在Parser提供了完整的序列化支持。其toJSON方法会把 scope 中的变量与表达式函数分别导出为variables与functions函数以syntax expr字符串形式保存外部注入的非表达式函数会被拒绝序列化见 Parser.jsfromJSON则反向重建变量直接set函数重新evaluate定义见 Parser.js。const parser math.parser() // evaluate some expressions parser.evaluate(w 2) parser.evaluate(f(x) x^w) parser.evaluate(c f(3)) // 9 // serialize the parser with its state const str JSON.stringify(parser) // deserialize the parser again const parser2 JSON.parse(str, math.reviver) parser.evaluate(f(4)) // 16注意两个细节反序列化必须使用math.reviver实现见 src/json/reviver.js它会识别 JSON 中的mathjs: Parser标记并调用Parser.fromJSONf(x) x^w能跨序列化保持正确正是因为f被以定义表达式字符串存储反序列化时在新的解析器作用域中重新求值定义其闭包引用也随之重建。四条路径的选型建议场景推荐 API一次性计算、交互式 REPL、调试math.evaluate同一表达式在循环/回调中反复执行追求性能math.compile或math.parse(...).compile()需要遍历、变换、导出表达式树如公式编辑器、符号化简math.parse需要跨多次求值保持变量与函数状态如计算器会话、脚本环境math.parser()相关表达式能力的延伸阅读表达式语法、表达式树与节点 API、表达式安全。所有 API 的 TypeScript 类型声明可在 types/index.d.ts 中查阅。【免费下载链接】mathjsAn extensive math library for JavaScript and Node.js项目地址: https://gitcode.com/gh_mirrors/ma/mathjs创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价