有的时候我们不得不面临不可使用eval函数或者new function,但是又需要将一个字符串作为代码运行的尴尬场景,比如小程序考虑到其安全性问题,就禁止使用;这种情况下我们就需要一个表达式解析器来实现功能了。
我们将以一个逻辑运算表达式解析器为例,深入探讨 JavaScript 如何进行表达式解析与求值。在很多计算场景中,特别是需要动态公式处理的情况下,合理地解析和求解运算表达式是至关重要的。我们将通过一个具体的代码示例,一步步展示如何对逻辑运算表达式进行标记化、解析,并最终计算运算结果。
表达式的标记化是处理字符串表达式的第一步,旨在将其分解为可理解的单元或"token"。如下所示的 tokenize
函数利用正则表达式匹配各种符号——变量、数字、操作符。
function tokenize(expression) {
const tokens = expression.match(/\>=|\<=|\>|\<|\===|\!==|\==|\!=|&&|\|\||\!|\(|\)|-?\d+(\.\d+)?|\b\w+\b/g) || [];
return tokens.map(token => {
if (token.match(/^\b\w+\b$/)) {
return { type: "VARIABLE", value: token };
} else if (token.match(/^-?\d+(\.\d+)?$/)) {
return { type: "NUMBER", value: Number(token) };
} else {
return { type: "OPERATOR", value: token };
}
});
}
在解析过程中,确保追踪到期望类型的 token 是有益的。expect
函数检查 token 的类型是否与预期匹配,并处理任何不符合预期的情况。
function expect(tokens, expectedType, expectedValue) {
if (tokens.length === 0) throw new Error("Unexpected end of expression");
let token = tokens.shift();
if (token.type !== expectedType || (expectedValue !== undefined && token.value !== expectedValue)) {
throw new Error(`Expected ${expectedType} but found ${token.type}`);
}
return token;
}
在标记化之后的步骤是解析。解析过程通过递归将输入的 token 序列转换成语法树结构,在本例逻辑运算的需求下,我们主要需要考虑的是各种逻辑运算符号的执行优先级,以及有括号()的情况下优先执行括号内内容,因此使用了递归下降解析算法:这种算法是一种自顶向下的解析方法,主要通过递归的方式处理各种优先级的表达式和结构。
下面的 parseExpression
函数示范如何实现。
function parseExpression(tokens) {
return parseOr();
function parseOr() {
let left = parseAnd();
while (tokens[0] && tokens[0].value === '||') {
tokens.shift();
let right = parseAnd();
left = left || right;
}
return left;
}
function parseAnd() {
let left = parseComparison();
while (tokens[0] && tokens[0].value === '&&') {
tokens.shift();
let right = parseComparison();
left = left && right;
}
return left;
}
function parseComparison() {
let left = parseAddition();
while (tokens[0] && ['>', '<', '>=', '<=', '===', '!==', '==', '!='].includes(tokens[0].value)) {
let op = tokens.shift().value;
let right = parseAddition();
switch (op) {
case '>':
left = left > right;
break;
case '<':
left = left < right;
break;
case '>=':
left = left >= right;
break;
case '<=':
left = left <= right;
break;
case '===':
left = left === right;
break;
case '!==':
left = left !== right;
break;
case '==':
left = left == right;
break;
case '!=':
left = left != right;
break;
}
}
return left;
}
function parseAddition() {
return parsePrimary();
}
function parsePrimary() {
if (tokens.length === 0) {
throw new Error("Unexpected end of expression");
}
let token = tokens.shift();
if (token.type === "NUMBER" || token.type === "VARIABLE") {
return token.value;
} else if (token.type === "OPERATOR" && token.value === "(") {
//处理括号
let value = parseExpression(tokens);
expect(tokens, "OPERATOR", ")");
return value;
} else {
throw new Error("Invalid syntax");
}
}
}
最后,表达式求值的过程涉及到实际执行前面得到的语法树,并计算最终结果。evaluate
函数获取一个 token 列表并调用 parseExpression
函数来求值。
function evaluate(expression, variables) {
let tokens = tokenize(expression);
return parseExpression(tokens, variables);
}
在以下的应用中,我们演示了如何使用上述定义的函数来对一个含有逻辑条件的表达式进行求值:
const formula = "{ANALYSIS_ZHKKLRL}<5 && {ANALYSIS_ZHKKLRL}>=0";
const variables = {
ANALYSIS_ZHKKLRL: -2.345,
};
const result = evaluate(parseAndEvaluate(formula, variables));
console.log(result);
在 evaluate
调用中,传入了变量ANALYSIS_ZHKKLRL
的值为 -2.345
。根据公式中的条件逻辑,结果将输出表达式求值的结果。
如上所述,理解并实现了表达式的标记化、解析和求值过程不仅对于实现复杂的计算功能至关重要,也为开发者提供更多构建灵活和强大应用程序的途径。JavaScript 凭借其灵活性和强大的内置对象,使得我们可以相对简单地实现这些功能。希望通过这篇博客,你能对 JavaScript 表达式的处理有一个更清晰的理解,并在你自己的项目中实施它。