如何在JavaScript中编写一个算术表达式解析器,而不使用eval或构造函数?

10

给定一个字符串:

 var str1 = "25*5+5*7";

如果不使用JavaScript中的eval或构造函数,如何编写一个名为“output”的函数来接受字符串并输出该字符串的算术值,即在本例中为160?


2
看起来这是一个毫无意义的目标,特别是有那些显然的任意限制。你实际上要做什么,为什么? - Lightness Races in Orbit
数字可以是两位数吗? - depperm
是的,数字可以是两位数。 - user3145336
1
如果你要避免使用"eval"或任何扭曲的变体,你需要实现一个解析器,用它来评估表达式。请参考这个SO答案,了解如何编写递归下降解析器;你可以在JavaScript中很容易地完成这个任务:https://dev59.com/v3E95IYBdhLWcg3wlu6z#2336769 - Ira Baxter
2
Closer(s): 这明显是编程问题,可以提供有用的答案(参见此处的几个示例)。您的关闭原因是错误的。 - Ira Baxter
显示剩余4条评论
5个回答

12

这是一个完整的优先级表达式求值器,遵循我在对题目的评论中链接到的递归解析思想。

为了实现这个功能,首先我为我想要处理的表达式编写了一个简单的BNF语法:

sum =  product | sum "+" product | sum "-" product ;
product = term | product "*" term | product "/" term ;
term = "-" term | "(" sum ")" | number ;

这本身需要一些经验,才能简单明了地完成。如果您没有BNF的经验,您会发现它非常有用,可以描述复杂的项目流,例如表达式、消息、编程语言等。

使用该语法,我按照其他消息中概述的过程编写了以下代码。显然它是由语法驱动的愚蠢机械方式,因此如果您具有该语法,则编写起来相当容易。

(未经测试。我不是JavaScript编码器。这肯定会包含一些语法/语义小问题。编码大约花费了我15分钟。)

var SE="Syntax Error";

function parse(str) { // returns integer expression result or SE
   var text=str;
   var scan=1;
   return parse_sum();

   function parse_sum() { 
      var number, number2;
      if (number=parse_product()==SE) return SE;
      while (true) {
        skip_blanks();
        if (match("+") {
           number2=parse_product();
           if (number2==SE) return SE;
           number+=number2;
        }
        else if (match('-')) {
                { number2=parse_product();
                  if (number2==SE) return SE;
                  number-=number2;
                } 
             else return number;
      }
   }

   function parse_product() {
      var number, number2;
      if (number=parse_number()==SE) return SE;
      while (true) {
        if (match("*") {
            number2=parse_term();
            if (number2==SE) return SE;
            number*=number2;
          }
          else if (match('/')) {
                  number2=parse_term();
                  if (number2==SE) return SE;
                  number/=number2;
               }
               else return number; 
      }
   }

   function parse_term() {
      var number;
      skip_blanks();
      if (match("(")) {
         number=parse_sum();
         if (number=SE) return SE;
         skip_blanks();
         if (!match(")") return SE;
      }
      else if match("-") {
              number= - parse_term();
           }
           else if (number=parse_number()==SE) return SE;
      return number;
   }

   function skip_blanks() {
      while (match(" ")) { };
      return;
    }

    function parse_number() {
       number=0;
       if (is_digit()) {
          while (is_digit()) {}
          return number;
        }
        else return SE;
    }

    var number;
    function is_digit() { // following 2 lines are likely wrong in detail but not intent
       if (text[scan]>="0" && text[scan]<="9") {
          number=number*10+text[scan].toInt();
          return true;
       }
       else return false;
    }

   function match(c) {
       if (text[scan]==c)
          { scan++; return true }
       else return false;
    }
 }

编写此类解析器/评估器非常简单。请参见我的SO回答,介绍如何构建解析器(它链接到如何构建评估器的内容)。


4

这是一个具有*优先级高于+的简单解析器。我尽可能地使它教育性强。如果您特别雄心勃勃,可以自行添加除法和减法,或者括号。

function parse(str) {
    var signs = ["*", "+"];             // signs in the order in which they should be evaluated
    var funcs = [multiply, add];                     // the functions associated with the signs
    var tokens = str.split(/\b/);          // split the string into "tokens" (numbers or signs)
    for (var round = 0; round < signs.length; round++) {              // do this for every sign
        document.write("tokens at this point: " + tokens.join(" ") + "<BR>");
        for (var place = 0; place < tokens.length; place++) {        // do this for every token
            if (tokens[place] == signs[round]) {                             // a sign is found
                var a = parseInt(tokens[place - 1]);        // convert previous token to number
                var b = parseInt(tokens[place + 1]);            // convert next token to number
                var result = funcs[round](a, b);               // call the appropriate function
                document.write("calculating: " + a + signs[round] + b + "=" + result + "<BR>");
                tokens[place - 1] = result.toString();          // store the result as a string
                tokens.splice(place--, 2);      // delete obsolete tokens and back up one place
            }
        }
    }
    return tokens[0];                      // at the end tokens[] has only one item: the result

    function multiply(x, y) {                       // the functions which actually do the math
        return x * y;
    }

    function add(x, y) {                            // the functions which actually do the math
        return x + y;
    }
}

var str = "25*5+5*7";
document.write("result: " + str + " = " + parse(str));


@IraBaxter 它具有优先级;如果您按正确顺序添加/和-,它将具有/+-优先级。我只是想为OP提供一个简单的解析器示例,让他能够理解并建立在此基础上,而不是向他展示一个完美的示例。因此,因为它在“现实世界”中没有用处而被投票降低评分有点傻。 - m69 ''snarky and unwelcoming''
没关系,我犯了个错误;我只是在你的代码中错过了优先级管理。我以为它太简单了。这表明优雅有时会对你不利 :-{ - Ira Baxter
@IraBaxter 有人在评论中放了一个关于解析器的问题链接,但那个问题非常技术性和复杂,我认为提问者可能会放弃尝试编写解析器的想法;我只是想展示你可以在10分钟内组合出一些简单有效的东西。 - m69 ''snarky and unwelcoming''
@IraBaxter 哎呀,我甚至没有注意到是你 :-) - m69 ''snarky and unwelcoming''

2
你可以使用 math.js 的表达式解析器来实现:

var str1= "25*5+5*7"
document.write(str1 + ' = ' + math.eval(str1)); 
// output: "25*5+5*7 = 160"
<script src="http://cdnjs.cloudflare.com/ajax/libs/mathjs/2.1.1/math.min.js"></script>


OP明确要求不使用eval的答案。 - Ira Baxter
2
答案不使用 eval,而是使用 math.js 的表达式解析器,math.eval(expr) - Jos de Jong
3
a) 它被称为 "eval"。 b) 它和 eval 做的事情相同。 您的回答不符合问题的要求。 - Ira Baxter
2
啊,抱歉,你是对的,函数应该被称为“output”以符合问题。我们可以这样做:var output = math.eval; var res = output("25*5+5*7")。只是开个玩笑。无论如何,我的答案完全回答了问题。虽然问题本身有歧义。如果@user3145336的意思是“我怎样才能自己编写表达式解析器?”,那么你的答案是正确的。但是大多数情况下,这些问题的意思是“我怎样才能安全地评估表达式而不必使用不安全的eval函数?”。那就是我的解释。在这种情况下,math.js的表达式解析器是完美的选择。 - Jos de Jong
1
我能看到你的解释;也许你应该在回答中表明这一点。反对eval可能会产生恶劣的副作用,这是提出这个问题的原因。大多数以这种方式提问的人似乎都太初级了,无法理解这一点。而他没有提出这个问题。所以我按照字面意思阅读他的问题。 - Ira Baxter
哈哈,其实你并不是只字不差地阅读了他的问题,你和我一样进行了解释,只是方向不同而已。这个问题根本没有提到编写解析器,它只是谈论如何在不使用“eval”的情况下获取表达式的结果。无论如何,我想我们永远不会知道@user3145336实际上想表达什么。 - Jos de Jong

0

您可以创建新脚本:

function parse(str) {
  var s = document.createElement('script');
  s.text = "window.result = " + str;
  document.body.appendChild(s); // Run script
  document.body.removeChild(s); // Clean up
  return result;                // Return the result
}
document.body.innerHTML = parse("5*5+5*5");

或者使用事件处理程序内容属性:

function parse(str) {
  var el = document.createElement('div');
  el.setAttribute('onclick', "this.result = " + str);
  el.onclick();     // Run script
  return el.result; // Return the result
}
document.body.innerHTML = parse("5*5+5*5");

请注意,这些方法是不安全的,就像eval一样邪恶,但更丑陋。因此,我不建议使用它们。

@depperm 是的,这将创建一个名为“result”的全局变量,并且您可以随意使用它。 - Oriol
也许我没有定义清楚。您能否编写一个函数结果,接受字符串并输出字符串的算术运算结果? - user3145336
@user3145336,是的,只需要将代码包装在一个函数中即可。请参见修改后的代码。 - Oriol
这看起来比 eval 本身更严重,因为它具有与原始 eval 相同的潜在风险(可能会导致注入),而且还修改了DOM :)。 eval 并不邪恶,只是被误解了。 - Tiberiu C.
@tiberiu.corbu 是的,它比eval更糟糕,但它不是eval,因此它满足问题的要求 :) - Oriol
显示剩余3条评论

0
如果需要使用小数,可以使用 decimal.js-extensions-evaluate npm 软件包,它与 decimal.js 相结合,提供了一个强大的解析器,用于带有小数的表达式(是的,0.1 + 0.2 = 0.3 而不是 0.30000000000000004)。
使用示例:
安装这两个软件包并使用以下代码:
import Decimal from 'decimal.js';
import { evaluate } from 'decimal.js-extensions-evaluate';
 
evaluate.extend(Decimal);
 
let result = Decimal.evaluate('0.1 + 0.2');
 
console.log(result);                     // '0.3'

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