在javascript中计算字符串值,而不是使用eval

Calculate string value in javascript, not using eval

本文关键字:eval javascript 计算 字符串      更新时间:2023-09-26

有没有办法在不使用eval()的情况下计算存储在 JavaScript 字符串中的公式?

通常我会做这样的事情

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

那么,有谁知道eval()的替代品吗?

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

function evil(fn) {
  return new Function('return ' + fn)();
}
console.log(evil('12/5*9+9.4*2')); // => 40.4

eval 没有错,尤其是对于这样的情况。 为了安全起见,您可以先使用正则表达式清理字符串:

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

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

如果你想要另一种方法,你必须使用一个纯粹的Javascript实现,这正是eval将要做的事情。

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

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

注意:

  • 它只处理基本运算符
  • 它不检查数字的有效性(例如:除以零(
  • 它尚未实现括号操作
  • 由于所有这些原因以及更多原因,Eval将是一个更好的选择

编辑 (2017-05-26( 以使用 SO 代码段:

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>

这正是

您应该使用eval()的地方,否则您将不得不遍历字符串并生成数字。您必须使用 Number.isNaN() 方法来执行此操作。

这是分流场算法的实现,它额外支持一元前缀(例如 - (和后缀(例如 ! ( 运算符和函数(例如 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>

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

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

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

您也可以滚动自己的:)

我花了几个小时来实现所有的算术规则,而不使用eval()最后我发布了一个关于npm字符串数学的包。一切都在描述中。享受

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

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

如果你正在寻找一个语法等同于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)

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

那将是这样的(真正的实现会稍微复杂一些,特别是如果你考虑使用括号,但你明白了(

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

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

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

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

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

计算器.js由同名的桌面计算器应用程序使用。在网站上观看现场演示。

注意:此解决方案中没有使用纯硬编码的库

我的解决方案也考虑了括号,如8+6(7(-1))8+6(7(-1))

您可以^, *, /, +, -执行以下操作

要计算字符串,请使用calculate(tokenize(pieval("8+6(7(-1))").join("")))

function tokenize(s) {
    // --- Parse a calculation string into an array of numbers and operators
    const r = [];
    let token = '';
    for (const character of s) {
        if ('^*/+-'.indexOf(character) > -1) {
            if (token === '' && character === '-') {
                token = '-';
            } else {
                r.push(parseFloat(token), character);
                token = '';
            }
        } else {
            token += character;
        }
    }
    if (token !== '') {
        r.push(parseFloat(token));
    }
    return r;
}
function calculate(tokens) {
    // --- Perform a calculation expressed as an array of operators and numbers
    const operatorPrecedence = [{'^': (a, b) => Math.pow(a, b)},
               {'*': (a, b) => a * b, '/': (a, b) => a / b},
               {'+': (a, b) => a + b, '-': (a, b) => a - b}];
    let operator;
    for (const operators of operatorPrecedence) {
        const newTokens = [];
        for (const token of tokens) {
            if (token in operators) {
                operator = operators[token];
            } else if (operator) {
                newTokens[newTokens.length - 1] = 
                    operator(newTokens[newTokens.length - 1], token);
                operator = null;
            } else {
                newTokens.push(token);
            }
        }
        tokens = newTokens;
    }
    if (tokens.length > 1) {
        console.log('Error: unable to resolve calculation');
        return tokens;
    } else {
        return tokens[0];
    }
}
function pieval(input) {
  let openParenCount = 0;
  let myOpenParenIndex = 0;
  let myEndParenIndex = 0;
  const result = [];
  for (let i = 0; i < input.length; i++) {
    if (input[i] === "(") {
      if (openParenCount === 0) {
        myOpenParenIndex = i;
        // checking if anything exists before this set of parentheses
        if (i !== myEndParenIndex) {
            if(!isNaN(input[i-1])){
                result.push(input.substring(myEndParenIndex, i) + "*");
            }else{
                result.push(input.substring(myEndParenIndex, i));
            }
        }
      }
      openParenCount++;
    }
    if (input[i] === ")") {
      openParenCount--;
      if (openParenCount === 0) {
        myEndParenIndex = i + 1;
        // recurse the contents of the parentheses to search for nested ones
        result.push(pieval(input.substring(myOpenParenIndex + 1, i)));
      }
    }
  }
  // capture anything after the last parentheses
  if (input.length > myEndParenIndex) {
    result.push(input.substring(myEndParenIndex, input.length));
  }
  //console.log(cal(result))
  let response = cal(result);
  return result;
}
function cal(arr) {
  let calstr = "";
  for (let i = 0; i < arr.length; i++) {
    if (typeof arr[i] != "string") {
      if (cal(arr[i]) < 0) {
        arr[i] = `${cal(arr[i])}`;
      } else {
        arr[i] = `${cal(arr[i])}`;
      }
    }
    if (typeof arr[i] === "string") {
      calstr += arr[i];
    }
    if (i == arr.length - 1) {
      //console.log("cal" ,calstr,calculate(tokenize(calstr)) );
      return calculate(tokenize(calstr));
    }
  }
}
console.log(calculate(tokenize(pieval("8+6(7(-1))").join("")))); // ["1+",["2-",["3+4"]]]
console.log(calculate(tokenize(pieval("1+(1+(2(4/4))+4)").join("")))); // ["1+",["2-",["3+4"]]]