在JavaScript中将字符串作为数学表达式进行求值

123

如何解析和计算字符串中的数学表达式(例如'1+1'),而不使用 eval(string) 来返回其数值?

以该示例为例,我想让函数接受'1+1'并返回2


10
非常相似,但可能不是你所询问的:(Function("return 1+1;"))()。 (注:该代码为JavaScript语言,执行结果为2。) - Gumbo
27个回答

98
你可以使用JavaScript Expression Evaluator库,它允许你做像这样的事情:
Parser.evaluate("2 ^ x", { x: 3 });

或者mathjs,它允许使用如下的功能:

math.eval('sin(45 deg) ^ 2');

我最终选择了mathjs作为我的一个项目。


27

使用 Function() 简单而优雅

function parse(str) {
  return Function(`'use strict'; return (${str})`)()
}

document.write( "1+2+3", '=' , parse("1+2+3"), '<br>');
document.write( "1 + 2 * 3", '=' , parse("1 + 2 * 3"), '<br>');

注意:不要在生产环境中使用


8
我不明白这怎么比 eval 好。在你运行这个服务器端代码之前,要注意 parse('process.exit()') 的出现。 - Basti
1
哎呀,我不知道为什么这不是最佳答案,它简单快捷,可以评估任何有效的JS表达式(不仅仅是数学),而且不依赖于第三方库。 - vakarami
2
请注意,这可能是一个严重的安全风险,具体取决于用户输入的来源。它会解释任何 JavaScript 代码,如 parse('alert("hello")') - thibpat
无论是否使用正则表达式,这听起来对我来说都像是一个安全噩梦 - 我不认为这个答案应该得到那么多的赞同。我并不羡慕下一个程序员将不得不修改那个正则表达式以支持新的要求,然后测试这是否会打开漏洞。我认为在这种情况下,最好咬紧牙关,采用由操作系统社区彻底测试和维护的专用库。 - Philippe Hebert
1
我明白这一点,你也明白。然而,从谷歌搜索进入这个网站的普通用户很可能不会明白这一点,这就是为什么将具有非常重大安全影响的任何建议都标记为重要的原因。 - Rory McCrossan
显示剩余12条评论

26

你可以轻松地执行加号或减号操作:

function addbits(s) {
  var total = 0,
      s = s.match(/[+\-]*(\.\d+|\d+(\.\d+)?)/g) || [];
      
  while (s.length) {
    total += parseFloat(s.shift());
  }
  return total;
}

var string = '1+23+4+5-30';
console.log(
  addbits(string)
)

更加复杂的数学使得 eval 更具吸引力 - 且写起来肯定更简单。


2
+1 - 可能比我选择的更为普遍,但对于我的情况不起作用,因为我可能会有类似1+-2这样的内容,而且我希望正则表达式也排除无效语句(我认为你的正则表达式将允许像“+3+4+”这样的内容)。 - wheresrhys
我在下面发布了一个更新的答案,其中包含更短的正则表达式,并允许运算符之间有空格。 - Stefan Gabos
你可以使用 .pop 而不是 .shift,因为你想要添加所有数字。.pop 的时间复杂度为 O(1),而 .shift 的时间复杂度为 O(n)。 - abdullah ajibade

19

需要有人来解析这个字符串。如果不是解释器(通过eval),那么就需要你编写一个解析程序来提取数值、运算符和任何其他你想在数学表达式中支持的内容。

因此,没有(简单的)方法可以不使用eval。如果你担心安全性(因为你解析的输入不是来自你控制的源),也许你可以在将其传递给eval之前检查输入的格式(通过白名单正则表达式过滤器)?


1
不是安全问题让我困扰(我已经有一个正则表达式来处理了),而是浏览器的负载,因为我必须处理很多这样的字符串。一个自定义解析器可能比eval()更快吗? - wheresrhys
12
你为什么认为你用JS编写的解析器会比系统提供的那个(经过优化,可能是用C或C++编写的)更快呢? - Mehrdad Afshari
4
使用eval无疑是最快的方法来实现这一点。然而,正则表达式通常不足以确保安全。 - levik
1
@wheresrhys:为什么你有很多这样的字符串?它们是由程序生成的吗?如果是,最简单的方法是在它们被转换为字符串之前计算结果。否则,就需要编写自己的解析器了。 - Phil H

16

作为对@kennebec的出色答案的替代方案,使用一个较短的正则表达式,并允许在运算符之间加入空格。

function addbits(s) {
    var total = 0;
    s = s.replace(/\s/g, '').match(/[+\-]?([0-9\.\s]+)/g) || [];
    while(s.length) total += parseFloat(s.shift());
    return total;
}

使用方法如下:

addbits('5 + 30 - 25.1 + 11');

更新

这是一个更优化的版本。

function addbits(s) {
    return (s.replace(/\s/g, '').match(/[+\-]?([0-9\.]+)/g) || [])
        .reduce(function(sum, value) {
            return parseFloat(sum) + parseFloat(value);
        });
}

1
这很完美,只要你只需要加法和减法。如此少的代码,如此多的产品!请放心,它正在被用于好的方面 :) - Ultroman the Tacoman
1
在表达式中,*和/没有优先级。计算顺序是从左到右。 - huykon225
括号和优先级更高的算术运算怎么处理? - tnsaturday
@tnsaturday会有更多的代码 :) - Stefan Gabos

10
我为了同样的目的创建了BigEval
在解决表达式时,它的表现与Eval()完全相同,并支持%、^、&、**(幂)和!(阶乘)等运算符。 您还可以在表达式中使用函数和常量(或称变量)。表达式按照PEMDAS顺序解决,这在包括JavaScript在内的编程语言中很普遍。
var Obj = new BigEval();
var result = Obj.exec("5! + 6.6e3 * (PI + E)"); // 38795.17158152233
var result2 = Obj.exec("sin(45 * deg)**2 + cos(pi / 4)**2"); // 1
var result3 = Obj.exec("0 & -7 ^ -7 - 0%1 + 6%2"); //-7

如果你需要处理任意精度的数字,也可以使用大数库进行算术计算。


9

我正在寻找用于计算数学表达式的JavaScript库,并找到了这两个有前途的候选者:

  • JavaScript表达式求值器:更小,希望更轻量级。允许代数表达式、替换和一些函数。

  • mathjs:还允许使用复数、矩阵和单位。专为在浏览器JavaScript和Node.js中使用而构建。


我现在已经测试了JavaScript表达式求值器,它似乎非常出色。(mathjs可能也很好,但对于我的目的来说似乎有点太大了,而且我也喜欢JSEE中的替换功能。) - Itangalo

8
这是我刚刚组合起来解决这个问题的一个小函数 - 它通过逐个字符分析字符串来构建表达式(实际上相当快)。它可以处理任何数学表达式(仅限于+,-,*,/ 运算符),并返回结果。它还可以处理负值和无限数量的运算。
唯一剩下的“待办”任务是确保先计算*和/,然后再计算+和-。稍后将添加该功能,但现在它可以满足我的需求。
/**
* Evaluate a mathematical expression (as a string) and return the result
* @param {String} expr A mathematical expression
* @returns {Decimal} Result of the mathematical expression
* @example
*    // Returns -81.4600
*    expr("10.04+9.5-1+-100");
*/ 
function expr (expr) {

    var chars = expr.split("");
    var n = [], op = [], index = 0, oplast = true;

    n[index] = "";

    // Parse the expression
    for (var c = 0; c < chars.length; c++) {

        if (isNaN(parseInt(chars[c])) && chars[c] !== "." && !oplast) {
            op[index] = chars[c];
            index++;
            n[index] = "";
            oplast = true;
        } else {
            n[index] += chars[c];
            oplast = false;
        }
    }

    // Calculate the expression
    expr = parseFloat(n[0]);
    for (var o = 0; o < op.length; o++) {
        var num = parseFloat(n[o + 1]);
        switch (op[o]) {
            case "+":
                expr = expr + num;
                break;
            case "-":
                expr = expr - num;
                break;
            case "*":
                expr = expr * num;
                break;
            case "/":
                expr = expr / num;
                break;
        }
    }

    return expr;
}

1+2*3 应该返回 7,而不是 9。 - tanguy_k

8
我最近用C#完成了这个任务(没有对我们来说的Eval()...)通过在逆波兰表达式中评估表达式(这是容易的部分)。难点实际上是解析字符串并将其转换为逆波兰表达式。我使用了Shunting Yard算法,因为维基百科和伪代码上有一个很好的例子。我发现两者都很容易实现,并且如果您还没有找到解决方案或正在寻找替代方案,我建议使用它们。

你能提供一些示例或维基百科的链接吗? - LetynSOFT
@LetynSOFT 伪代码可以在这里找到链接 - UnderscoreA

4
你可以使用 for 循环来检查字符串是否包含任何无效字符,然后使用 try...catch 和 eval 来检查计算是否会出现错误,例如像 eval("2++") 这样的错误。

function evaluateMath(str) {
  for (var i = 0; i < str.length; i++) {
    if (isNaN(str[i]) && !['+', '-', '/', '*', '%', '**'].includes(str[i])) {
      return NaN;
    }
  }
  
  
  try {
    return eval(str)
  } catch (e) {
    if (e.name !== 'SyntaxError') throw e
    return NaN;
  }
}

console.log(evaluateMath('2 + 6'))

或者你可以使用Math.eval来代替函数。

Math.eval = function(str) {
  for (var i = 0; i < str.length; i++) {
    if (isNaN(str[i]) && !['+', '-', '/', '*', '%', '**'].includes(str[i])) {
      return NaN;
    }
  }
  
  
  try {
    return eval(str)
  } catch (e) {
    if (e.name !== 'SyntaxError') throw e
    return NaN;
  }
}

console.log(Math.eval('2 + 6'))


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接