【问题标题】:Calculate string value in javascript, not using eval在javascript中计算字符串值,而不是使用eval
【发布时间】:2011-09-22 16:21:51
【问题描述】:

有没有一种方法可以在不使用eval 的情况下计算存储在 JavaScript 中的字符串中的公式?

通常我会做类似的事情

var apa = "12/5*9+9.4*2";
alert(eval(apa));

那么,有人知道eval 的替代品吗?

【问题讨论】:

  • eval 没有任何问题,只要你能确定你正在评估的字符串包含什么。
  • 我同意菲利克斯的观点。任何其他方式都不会那么有效。但是,我提供了一个纯 JS 解决方案(没有 eval)。
  • 如果您想知道,在 Web 应用程序中使用 eval不会有安全风险。如果有人想将代码注入您的 Web 应用程序,他们可以在 chrome 调试器中打开它并修改代码!唯一需要担心安全性的就是如果你在服务器上使用了 eval(),比如 node.js

标签: javascript eval


【解决方案1】:

嗯,你可以使用Function-constructor:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function

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

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

【讨论】:

  • 你,先生,真棒。直到你的帖子我才知道这件事。 +1 对我的支持,我已经在一个项目中使用了它。再见 eval()!
  • 但请记住,以这种方式使用 Function 构造函数与 eval() 类似,因为风险可能相当。在这种情况下,由于假定字符串是安全且可信的,我们希望使用 eval() 或 Function 构造函数是合理的。
  • @davisec52 这就是为什么我称它为 eviiil ;)
  • Chrome 的内容安全策略仍然会抛出 EvalError
  • 我正在寻找一种从字符串路径中获取 json 子树的方法。这行得通!谢谢@yckart!
【解决方案2】:

eval 没有任何问题,尤其是在这种情况下。为了安全起见,您可以先使用正则表达式清理字符串:

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

【讨论】:

    【解决方案3】:

    这正是您应该使用 eval 的地方,否则您将不得不遍历字符串并生成数字。你将不得不使用 isNaN 方法来做到这一点。

    【讨论】:

    • 仅当字符串来自可信来源时。
    • 2021 年 12 月,java 世界被一个 log4j 漏洞所困扰。正是因为这个。
    【解决方案4】:

    Eval 是为这样的条件而构建的。

    如果您想要另一种方法,则必须使用 eval 将要执行的确切操作的纯 Javascript 实现。

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

    这是我想出的一个简单的基本示例(更新 (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, '');:^不需要转义?
    【解决方案5】:

    这是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);
        }
        // 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);
                const [token, bad] = match || [")", undefined],
                    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 }
    ]
    
    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
    • 谢谢。如果您无法使用默认运算符使其工作,我建议您提出一个新问题,指出您卡在哪里。
    【解决方案6】:

    如果您不想使用 eval,则必须使用现有的表达式评估器库。

    http://silentmatt.com/javascript-expression-evaluator/

    http://www.codeproject.com/KB/scripting/jsexpressioneval.aspx

    你也可以自己滚动一个:)

    【讨论】:

      【解决方案7】:

      我花了几个小时在不使用 eval() 的情况下实现所有算术规则,最后我在 npm string-math 上发布了一个包。一切都在描述中。享受

      【讨论】:

        【解决方案8】:

        如果您正在寻找与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)

        【讨论】:

          【解决方案9】:

          此解决方案还剪切空格并检查重复的运算符

          例如' 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

          【讨论】:

          • 不支持大括号,对吗?无论如何,真正大胆的尝试!
          【解决方案10】:

          你不能,最多你可以做一些反驳,比如解析数字,然后用开关分开操作,然后制作它们。除此之外,在这种情况下我会使用 eval。

          类似于(真正的实现会稍微复杂一些,特别是如果您考虑使用括号,但您明白了)

              function operate(text) {
                  var values = text.split("+");
          
                  return parseInt(values[0]) + parseInt(values[1]);
              }
          
              alert(operate("9+2"));
          

          我仍然认为您可以做出的最佳选择是使用 eval,因为您能够信任字符串的来源。

          【讨论】:

            【解决方案11】:

            GitHub 上还有一个开源实现,evaluator.js 和一个NPM package

            来自自述文件: Evaluator.js 是一个用于评估数学表达式的小型零依赖模块。

            支持所有主要的操作、常量和方法。此外,Evaluator.js 智能地报告无效语法,例如误用运算符、缺少操作数或不匹配的括号。

            Evaluator.js 被同名的桌面计算器应用程序使用。在网站上查看live demo

            【讨论】:

              【解决方案12】:
              stringToMath(str);
              

              我使用stringToMath(str) 而不是eval(),它可以正常计算简单的字符串值。

              【讨论】:

              • stringToMath 是什么?
              猜你喜欢
              • 1970-01-01
              • 2019-02-13
              • 1970-01-01
              • 1970-01-01
              • 2013-09-23
              • 1970-01-01
              相关资源
              最近更新 更多