在JavaScript中计算字符串值,不使用eval函数

70
有没有一种方法可以在JavaScript中计算存储在字符串中的公式,而不使用eval()函数?
通常我会做类似这样的事情:
var apa = "12/5*9+9.4*2";
console.log(eval(apa));

那么,有人知道替代eval()的方法吗?


9
只要你能确保所评估的字符串包含的内容是可靠的,使用 eval 没有任何问题。 - Felix Kling
1
我同意Felix的观点。任何其他方法都不会像这种方式那样高效。然而,我提供了一个纯JS解决方案(无需eval)。 - vol7ron
8
如果你有疑问,使用 eval 在一个 Web 应用程序中并不是安全风险。如果有人想要向你的 Web 应用程序注入代码,他们可以只需在 Chrome 调试器中打开它并修改代码!唯一需要担心安全的时候是在服务器上使用 eval(),例如 node.js。 - John Henckel
12个回答

140

你可以使用Function() 构造函数:

function evil(fn) {
  return new Function('return ' + fn)();
}

console.log(evil('12/5*9+9.4*2')); // => 40.4


10
您太棒了!在您发布的帖子中,我才知道这件事。我给您点赞并已在项目中使用它。再见eval()! - Michael
14
请注意,以这种方式使用Function构造函数类似于eval(),风险可能是相当的。在这种情况下,因为假定字符串是安全且可信的,我们期望使用eval()或Function构造函数都是合理的。 - davisec52
6
这就是为什么我把它称为“邪恶的”原因 ;) - yckart
6
Chrome的内容安全策略仍会抛出EvalError异常。 - alib_15
1
在这里创建构造函数没有意义,而且会占用不必要的资源。结果与没有“new”相同。只有在极少数情况下,如果此函数evil()本身声明了一些希望避免的变量,那么它们会有所不同。但是,出于变量查找的原因,这种方法比eval()更快。这里有一个很好的解释和更多示例。 - papo
显示剩余2条评论

59

eval没有问题,特别是在这种情况下。你可以用正则表达式先对字符串进行清理以确保安全:

// strip anything other than digits, (), -+/* and .
var str = "12/5*9+9.4*2".replace(/[^-()\d/*+.]/g, '');
console.log(eval(str));


31

Eval是为这样的情况而构建的。

如果你想使用另一种方法,你需要使用一个纯JavaScript实现与eval完全相同的事情。

  • 困难的部分不是数字和运算符的解析
  • 困难的部分应用运算顺序和递归控制

这是我想出的一个快速基本示例(已更新(2011-06-26):更简洁的输入框)。
http://jsfiddle.net/vol7ron/6cdfA/

注意:

  • 它只处理基本运算符
  • 它不检查数字的有效性(例如:除以零)
  • 它还没有实现括号操作
  • 出于所有这些原因和更多原因,eval将是更好的选择

编辑(2017-05-26)使用SO Snippet:

function calculate(input) {

  var f = {
    add: '+',
    sub: '-',
    div: '/',
    mlt: '*',
    mod: '%',
    exp: '^'
  };

  // Create array for Order of Operation and precedence
  f.ooo = [
    [
      [f.mlt],
      [f.div],
      [f.mod],
      [f.exp]
    ],
    [
      [f.add],
      [f.sub]
    ]
  ];

  input = input.replace(/[^0-9%^*\/()\-+.]/g, ''); // clean up unnecessary characters

  var output;
  for (var i = 0, n = f.ooo.length; i < n; i++) {

    // Regular Expression to look for operators between floating numbers or integers
    var re = new RegExp('(\\d+\\.?\\d*)([\\' + f.ooo[i].join('\\') + '])(\\d+\\.?\\d*)');
    re.lastIndex = 0; // take precautions and reset re starting pos

    // Loop while there is still calculation for level of precedence
    while (re.test(input)) {
      output = _calculate(RegExp.$1, RegExp.$2, RegExp.$3);
      if (isNaN(output) || !isFinite(output)) 
        return output; // exit early if not a number
      input = input.replace(re, output);
    }
  }

  return output;

  function _calculate(a, op, b) {
    a = a * 1;
    b = b * 1;
    switch (op) {
      case f.add:
        return a + b;
        break;
      case f.sub:
        return a - b;
        break;
      case f.div:
        return a / b;
        break;
      case f.mlt:
        return a * b;
        break;
      case f.mod:
        return a % b;
        break;
      case f.exp:
        return Math.pow(a, b);
        break;
      default:
        null;
    }
  }
}
label {
  display: inline-block;
  width: 4em;
}
<div>
  <label for="input">Equation: </label>
  <input type="text" id="input" value="12/5*9+9.4*2-1" />
  <input type="button" 
         value="calculate" 
         onclick="getElementById('result').value = calculate(getElementById('input').value)" />
</div>

<div>
  <label for="result">Result: </label>
  <input type="text" id="result" />
</div>


^ 不需要转义吗?:(/[^0-9%^*\/()\-+.]/g, ''); - Timo

26

这正是你应该使用 eval() 的地方,否则你将不得不循环遍历字符串并生成数字。你需要使用 Number.isNaN() 方法来完成。


7
仅当字符串来自可信来源时。 - Felix Kling
3
2021年12月,Java世界因log4j漏洞而受到震动。就是因为这个原因。 - rgbk21

26

这里是一个实现 Shunting-yard algorithm 的代码,它还支持一元前缀(例如 -)和后缀(例如 !)运算符以及函数(例如 sqrt())表示法。更多的运算符/函数可以通过 Calculation.defineOperator 方法轻松定义:

"use strict";
class Calculation {
    constructor() {
        this._symbols = {};
        this.defineOperator("!", this.factorial,      "postfix", 6);
        this.defineOperator("^", Math.pow,            "infix",   5, true);
        this.defineOperator("*", this.multiplication, "infix",   4);
        this.defineOperator("/", this.division,       "infix",   4);
        this.defineOperator("+", this.last,           "prefix",  3);
        this.defineOperator("-", this.negation,       "prefix",  3);
        this.defineOperator("+", this.addition,       "infix",   2);
        this.defineOperator("-", this.subtraction,    "infix",   2);
        this.defineOperator(",", Array.of,            "infix",   1);
        this.defineOperator("(", this.last,           "prefix");
        this.defineOperator(")", null,                "postfix");
        this.defineOperator("min", Math.min);
        this.defineOperator("sqrt", Math.sqrt);
        this.defineOperator("pi", Math.PI); // A constant
    }
    // Method allowing to extend an instance with more operators and functions:
    defineOperator(symbol, f, notation = "func", precedence = 0, rightToLeft = false) {
        // Store operators keyed by their symbol/name. Some symbols may represent
        // different usages: e.g. "-" can be unary or binary, so they are also
        // keyed by their notation (prefix, infix, postfix, func):
        if (notation === "func") precedence = 0;
        this._symbols[symbol] = Object.assign({}, this._symbols[symbol], {
            [notation]: {
                symbol, f, notation, precedence, rightToLeft, 
                argCount: 1 + (notation === "infix")
            },
            symbol,
            regSymbol: symbol.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&')
                + (/\w$/.test(symbol) ? "\\b" : "") // add a break if it's a name 
        });
    }
    last(...a)           { return a[a.length-1] }
    negation(a)          { return -a }
    addition(a, b)       { return a + b }
    subtraction(a, b)    { return a - b }
    multiplication(a, b) { return a * b }
    division(a, b)       { return a / b }
    factorial(a) {
        if (a%1 || !(+a>=0)) return NaN
        if (a > 170) return Infinity;
        let b = 1;
        while (a > 1) b *= a--;
        return b;
    }
    calculate(expression) {
        let match;
        const values = [],
            operators = [this._symbols["("].prefix],
            exec = _ => {
                let op = operators.pop();
                values.push(op.f(...[].concat(...values.splice(-op.argCount))));
                return op.precedence;
            },
            error = msg => {
                let notation = match ? match.index : expression.length;
                return `${msg} at ${notation}:\n${expression}\n${' '.repeat(notation)}^`;
            },
            pattern = new RegExp(
                // Pattern for numbers
                "\\d+(?:\\.\\d+)?|" 
                // ...and patterns for individual operators/function names
                + Object.values(this._symbols)
                        // longer symbols should be listed first
                        .sort( (a, b) => b.symbol.length - a.symbol.length ) 
                        .map( val => val.regSymbol ).join('|')
                + "|(\\S)", "g"
            );
        let afterValue = false;
        pattern.lastIndex = 0; // Reset regular expression object
        do {
            match = pattern.exec(expression);
            let [token, bad] = match || [")", undefined];
            // Replace constant names (like PI) with corresponding value
            if (typeof this._symbols[token]?.func?.f === "number") token = this._symbols[token].func?.f;
            const notNumber = this._symbols[token],
                notNewValue = notNumber && !notNumber.prefix && !notNumber.func,
                notAfterValue = !notNumber || !notNumber.postfix && !notNumber.infix;
            // Check for syntax errors:
            if (bad || (afterValue ? notAfterValue : notNewValue)) return error("Syntax error");
            if (afterValue) {
                // We either have an infix or postfix operator (they should be mutually exclusive)
                const curr = notNumber.postfix || notNumber.infix;
                do {
                    const prev = operators[operators.length-1];
                    if (((curr.precedence - prev.precedence) || prev.rightToLeft) > 0) break; 
                    // Apply previous operator, since it has precedence over current one
                } while (exec()); // Exit loop after executing an opening parenthesis or function
                afterValue = curr.notation === "postfix";
                if (curr.symbol !== ")") {
                    operators.push(curr);
                    // Postfix always has precedence over any operator that follows after it
                    if (afterValue) exec();
                }
            } else if (notNumber) { // prefix operator or function
                operators.push(notNumber.prefix || notNumber.func);
                if (notNumber.func) { // Require an opening parenthesis
                    match = pattern.exec(expression);
                    if (!match || match[0] !== "(") return error("Function needs parentheses")
                }
            } else { // number
                values.push(+token);
                afterValue = true;
            }
        } while (match && operators.length);
        return operators.length ? error("Missing closing parenthesis")
                : match ? error("Too many closing parentheses")
                : values.pop() // All done!
    }
}
Calculation = new Calculation(); // Create a singleton

// I/O handling
function perform() {
    const expr = document.getElementById('expr').value,
        result = Calculation.calculate(expr);
    document.getElementById('out').textContent = isNaN(result) ? result : '=' + result;
}
document.getElementById('expr').addEventListener('input', perform);
perform();

// Tests
const tests = [
    { expr: '1+2', expected: 3 },
    { expr: '1+2*3', expected: 7 },
    { expr: '1+2*3^2', expected: 19 },
    { expr: '1+2*2^3^2', expected: 1025 },
    { expr: '-3!', expected: -6 },
    { expr: '12---11+1-3', expected: -1 },
    { expr: 'min(2,1,3)', expected: 1 },
    { expr: '(2,1,3)', expected: 3 },
    { expr: '4-min(sqrt(2+2*7),9,5)', expected: 0 },
    { expr: '2,3,10', expected: 10 },
    { expr: 'pi*2', expected: Math.PI*2 },
]

for (let {expr, expected} of tests) {
    let result = Calculation.calculate(expr);
    console.assert(result === expected, `${expr} should be ${expected}, but gives ${result}`);
}
#expr { width: 100%; font-family: monospace }
Expression: <input id="expr" value="min(-1,0)+((sqrt(16)+(-4+7)!*---4)/2)^2^3"><p>
<pre id="out"></pre>


真是太棒了!您能否实现 * 作为默认操作符呢?例如:(1)1 报错,但应该被视为 (1)*1 - Edvard Rejthar
3
谢谢。如果您无法使用默认运算符使其正常工作,我建议您提出一个新问题,指出您卡住的具体位置。 - trincot

10

可能是最好的答案。其他答案要么使用ify正则表达式,要么使用eval()并移除了一步。 - Michael M.

4

我花了几个小时实现了所有算术规则,没有使用eval(),最终我在npm上发布了一个包string-math。一切都在说明中。享受吧。


4

这个解决方案还可以剪辑空格并检查重复的运算符。

例如:' 1+ 2 *2' // 5,但是' 1 + +2* 2 ' // Error

function calcMe(str) {
  const noWsStr = str.replace(/\s/g, '');
  const operators = noWsStr.replace(/[\d.,]/g, '').split('');
  const operands = noWsStr.replace(/[+/%*-]/g, ' ')
                          .replace(/\,/g, '.')
                          .split(' ')
                          .map(parseFloat)
                          .filter(it => it);

  if (operators.length >= operands.length){
    throw new Error('Operators qty must be lesser than operands qty')
  };

  while (operators.includes('*')) {
    let opIndex = operators.indexOf('*');
    operands.splice(opIndex, 2, operands[opIndex] * operands[opIndex + 1]);
    operators.splice(opIndex, 1);
  };
  while (operators.includes('/')) {
    let opIndex = operators.indexOf('/');
    operands.splice(opIndex, 2, operands[opIndex] / operands[opIndex + 1]);
    operators.splice(opIndex, 1);
  };
  while (operators.includes('%')) {
    let opIndex = operators.indexOf('%');
    operands.splice(opIndex, 2, operands[opIndex] % operands[opIndex + 1]);
    operators.splice(opIndex, 1);
  };

  let result = operands[0];
  for (let i = 0; i < operators.length; i++) {
    operators[i] === '+' ? (result += operands[i + 1]) : (result -= operands[i + 1])
  }
  return result
}

这个比 @vol7ron 的解决方案表现更好。 请查看 JSBenchmark


1
花括号不被支持,我是对的吗?无论如何,这是一个真正大胆的尝试! - Alex Yu

3
你最多只能像解析数字一样,使用switch分离操作并进行计算。除此之外,我会在这种情况下使用eval。一个真正的实现会更加复杂,特别是如果你考虑使用括号,但你可以理解这个思路。
代码示例:

function operate(text) {
  var values = text.split("+");

  return parseInt(values[0]) + parseInt(values[1]);
}

console.log(operate("9+2"));

然而,我认为你最好的选择是使用eval,前提是你能够信任字符串的来源。


3
如果你正在寻找一个与eval有相同语法的等价物,你可以使用new Function。在作用域方面有些微小的差异,但它们大部分的行为都是相似的,包括暴露给许多相同的安全风险:

let str = "12/5*9+9.4*2"

let res1 = eval(str)
console.log('res1:', res1)

let res2 = (new Function('return '+str)())
console.log('res2:', res2)


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