javascript - 如何计算 javascript 中字符串的内容?

我正在构建一个应该计算字符串内容的计算器。为此,我使用了一个 Function 对象,但是,在运行代码时,我得到了一个未定义的 value。我假设这与 Function 对象的全局范围有关,但我看不到调试该函数中发生的事情的方法。将其传递给局部变量可以解决问题,但我不知道如何解决。

let addListeners = function () {
    screens = document.querySelectorAll("[class=screen]");
    operationsButtons = document.querySelectorAll("[class^=operations_button]");

    initAttributes();
    addNumberButtonListeners();
    addOperationsListeners();
    addOtherButtons();
}


function addNumberButtonListeners() {
    numberButtons = document.querySelectorAll("[id^=number]");
    numberButtons.forEach(button => {
        button.addEventListener("click", function () {
            let buttonNumber = button.innerText;
            screens.forEach(screen => {
                screen.numberLast = true;
                if (screen.isDefault) {
                    screen.innerText = buttonNumber;
                    screen.isDefault = false;
                    if (screen.id == "little_screen") {
                        screen.value = screen.innerText;
                    }
                }
                else {
                    screen.innerText += buttonNumber;
                    if (screen.id == "little_screen") {
                        screen.value = screen.innerText;
                    }
                }

            })

        })
    });
}

function addOperationsListeners() {
    let littlescreen = document.querySelector("[id=little_screen]");
    let bigscreen = document.querySelector("[id=big_screen]");
    operationsButtons.forEach(button => {
        button.addEventListener("click", function () {


            try {
                if (littlescreen.numberLast == false) throw button.innerText;
                littlescreen.innerText = (littlescreen.innerText + button.innerText);
                bigscreen.isDefault = true;
                littlescreen.numberLast = false;
                littlescreen.value = littlescreen.innerText;
            }

            catch (e) {
                let str = littlescreen.innerText;
                littlescreen.innerText = (str.slice(0, -1) + button.innerText);
                littlescreen.value = littlescreen.innerText;
                console.log(e + " twice");
            }
        })

    })
}


function addOtherButtons() {
    allClear = function () {
        button = document.querySelector("[id=all_clear]");
        button.addEventListener("click", function () {
            screens.forEach(screen => {
                screen.innerText = "0";
                screen.isDefault = true;
                if (screen.id == "big_screen") {
                    screen.numberLast = false;
                }
            })

        })
    }

    equalsButton = function () {
        let littlescreen = document.querySelector("[id=little_screen]");
        button = document.querySelector("[id=equals]");
        button.addEventListener("click", function () {
            screens.forEach(screen => {
                screen.isDefault = true;
                if (screen.id == "big_screen") {
                    screen.numberLast = false;
                    // Function I can't get to work.
                    //littlescreen.innertext is string to be calculated.

                    console.log(littlescreen.innerText);
                    let calculate = function () {
                    screen = document.querySelector("[id=little_screen]");
                    screen.innerText = screen.innerText.slice(0, -1);
                    return screen.innerText;
                    }
                    console.log(calculate());

                }
                else {
                    littlescreen.innerText = (littlescreen.innerText + button.innerText);
                }
            })

        })
    }

    //add pow()
    //add decimal
    //add +/-
    allClear();
    equalsButton();
}



function initAttributes() {
    screens.forEach(screen => {
        Object.defineProperty(screen, "isDefault", {
            value: true,
            writable: true,
        });

        if (screen.id == "little_screen") {
            Object.defineProperty(screen, "numberLast", {
                value: false,
                writable: true,
            });
            Object.defineProperty(screen, "value", {
                value: null,
                writable: true,
            });
        }
        console.log(screen);
    });
}



addListeners()
.container{
    display: flex;
    justify-content: center;
}

.calccontainer{
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;

    height: 100vh;
    width:calc((2/3) * 100vh);

    background: #D6D1B1;
    border: 3px solid gray;
    border-radius: 20px;
}

.screencontainer{
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    align-items: center;

    height: 20vh;
    width: 80%;

    background: #eef5db;
    border: 3px solid gray;
    border-radius: 20px;

    margin: 5%;
    margin-top: 7%;

}

#little_screen, #big_screen{
    width: 80%;
    text-align: right;
    font-family: 'Seven Segment', sans-serif;
    overflow: hidden;
}

#little_screen{
    height: 25%;
    font-size: 5vh;
}

#big_screen{
    height: 50%;
    font-size: 10vh;
}

.buttoncontainer{
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;

    height: 70vh;
    width: 80%;

    border-radius: 20px;

    margin: 0 5% 5% 5%;
}

.number_button, .operations_button, .other_button{
    display: flex;
    justify-content: center;
    align-items: center;
    height: 10vh;
    width: 20%;

    border: 3px solid gray;
    border-radius: 15px;

}
#all_clear{
    width: 46%;
    flex-shrink: 0;

    background: #E3C498;
}

[id^="number"], #positive_negative, #decimal {
    background: #BAE9C4;
}

[id^="button"] {
    background: #F0B67F;
}

#equals{
    background: #fe5f55;
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Page Title</title>
  <link rel="stylesheet" href="style.css">
  <script src="script.js" defer></script>
  <link href="http://fonts.cdnfonts.com/css/seven-segment" rel="stylesheet">
</head>

<body style="margin: 0;">
  <!-- Removes white border around page-->
  <div class="container" id="container">

    <div class="calccontainer" id="calccontainer">

      <div class="screencontainer" id="screencontainer">

        <div class="screen" id="little_screen">0</div>
        <div class="screen" id="big_screen">0</div>
      </div>


      <div class="buttoncontainer" id="innercontainer">

        <div class="other_button" id="all_clear">AC</div>
        <div class="other_button" id="button2">Xy</div>
        <div class="operations_button" id="button3">/</div>
        <div class="number_button" id="number7">7</div>
        <div class="number_button" id="number8">8</div>
        <div class="number_button" id="number9">9</div>
        <div class="operations_button" id="button7">*</div>
        <div class="number_button" id="number4">4</div>
        <div class="number_button" id="number5">5</div>
        <div class="number_button" id="number6">6</div>
        <div class="operations_button" id="button11">-</div>
        <div class="number_button" id="number1">1</div>
        <div class="number_button" id="number2">2</div>
        <div class="number_button" id="number3">3</div>
        <div class="operations_button" id="button15">+</div>
        <div class="other_button" id="positive_negative">+/-</div>
        <div class="number_button" id="number0">0</div>
        <div class="other_button" id="decimal">.</div>
        <div class="other_button" id="equals">=</div>
      </div>
    </div>
  </div>
</body>

</html>

我知道使用 eval() 会起作用,但我试图远离它。我有一个console.log,它给出了应该计算的字符串的value,它会触发你通过按下等号按钮来尝试计算的东西。

编辑:所以我做了一些更改并且不再未定义,但是现在我只是将字符串传递给我而无需计算。

let calculate = function () {
                        screen = document.querySelector("[id=little_screen]");
                        screen.innerText = screen.innerText.slice(0, -1);
                        return screen.innerText;

回答1

您通常不希望执行“不受信任的代码”,这就是“eval is bad”规则的原因。然而,运行不受信任的代码的方法并不重要,因此使用 eval 或创建“新函数”并不重要。

但是,在您目前似乎拥有的应用程序中,唯一可以执行的代码是通过按下计算器按钮编写的代码。除非该代码以某种方式被共享,或者有办法用任意代码初始化应用程序,否则使用 eval 没有真正的风险。可能发生的最糟糕的情况是用户在自己的计算机上执行自己的恶意代码(不是很好的黑客技能)。

如果代码以某种方式持续存在,则您需要在将其传递给 eval 之前对其进行清理。您可以通过对其执行正则表达式并删除字符类 [^0-9+\-*\/\.] 中的所有内容来做到这一点,据我所知,使用字符 +-*/.09

const code = 'alert("bad stuff"); 1+1';
const safeCode = code.replaceAll(/[^0-9+\-*\/\.]/g, '');
console.log(eval(safeCode));
// expected output: 2

一旦你想集成更复杂的东西,比如 sqrt() 函数,这种方法就不再适用了,你开始进入标记器(这些将字符串分成可管理的部分)、解析器(这些将这些部分排序为分层树)和解释器(这些评估树)。如今,解析器通常是在所谓的“解析器生成器”的帮助下编写的。这些是特殊程序,它们从您要创建的语言的正式声明开始,为解析器生成编程代码。

维基百科整理了一个解析器生成器的比较列表https://en.wikipedia.org/wiki/Comparison_of_parser_generators .

AFAIK,在输入字符串上运行大多数生成的解析器的输出是“AST”或“CST”(它们是“抽象语法树”和“具体语法树”)。两者之间的区别本质上取决于它们与给定代码的相似程度。在这个 https://stackoverflow.com/questions/1888854/what-is-the-difference-between-an-abstract-syntax-tree-and-a-concrete-syntax-tre 中有更详细的说明。然后,AST 通常被解释(直接执行)或编译为机器码或字节码,或“转译”为另一种语言(如“Typescript”到“Javascript”,或任何语言到 https://it.wikipedia.org/wiki/LLVM )。

当然,由于解析器生成器的输出是“只是代码”(尽管通常是不可读的代码),因此完全可以手动编写。有许多类型的“解析器”(取决于它们需要解析的代码的复杂性),但是这些天您想要手动编写的一种类型可能是 https://en.wikipedia.org/wiki/Recursive_descent_parserhttps://craftinginterpreters.com/ 找到它(我真诚地希望它在未来很多年都不会出现;但也许:对所有人来说,帮我一个忙并支持 pdf 版本 - 我们不想松开这颗宝石)。

我已经为您编写了一个非常简单的示例扫描器、解析器和解释器(此处为简化目的),它在此处运行简单的数学表达式(如您的应用程序中的表达式):

class Scanner {
  tokens = []
  start = 0
  current = 0
  source = ''
  
  scanTokens(source) {
    this.source = source
    this.tokens = []
    this.start = 0
    this.current = 0
    
    while(!this.isAtEnd()) {
      this.start = this.current
      this.scanToken()
    }
    
    return this.tokens
  }
  
  isAtEnd() {
    return this.current >= this.source.length
  }
  
  scanToken() {
    const c = this.advance()
    switch(c) {
      case '+':
      case '-':
      case '*':
      case '/':
        this.tokens.push({type: c})
        break;
        
      default:
        if(this.isDigit(c)) {
          this.number()
        } else {
          throw 'Unknown Token: ' + c
        }
    }
  }
  
  advance() {
    this.current += 1;
    return this.source.charAt(this.current - 1)
  }
  
  peek() {
    if(this.isAtEnd()) return undefined;
    
    return this.source.charAt(this.current)
  }
  
  peekNext() {
    if(this.isAtEnd()) return undefined;
    
    return this.source.charAt(this.current + 1)
  }
  
  isDigit(c) {
    return c >= '0' && c <= '9'
  }
  
  number() {
    while(this.isDigit(this.peek())) {
      this.advance()
    }
    
    if(this.peek() === '.' && this.isDigit(this.peekNext())) {
      this.advance() 
      while(this.isDigit(this.peek())) {
        this.advance()
      }
    }
    
    this.tokens.push({
      type: 'number', 
      value: parseFloat(this.source.substring(this.start, this.current))
    })
  }
}

class Parser {
  tokens = null
  current = 0
  
  parse(tokens) {
    this.tokens = tokens;
    this.current = 0;
    
    return this.expression()
  }
  
  expression() {
    return this.sumOrSubtraction()
  }
  
  sumOrSubtraction() {
    let left = this.multiplicationOrDivision()
    let op;
    
    while(op = this.match('+', '-')) {
      const right = this.multiplicationOrDivision();
      left = {
        type: 'binary',
        operator: op.type, 
        left: left,
        right: right
      }
    }
    
    return left;
  }
  
  multiplicationOrDivision() {
    let left = this.unary()
    let op;
    
    while(op = this.match('*', '/')) {
      const right = this.unary();
      left = {
        type: 'binary',
        operator: op.type, 
        left: left,
        right: right
      }
    }
    
    return left;
  }
  
  unary() {
    let op = this.match('-')
    if(op) {
      return {
        type: 'unary',
        operator: '-',
        right: this.unary()
      }
    }
    
    const right = this.consume('number', 'number expected')
    return {
      type: 'number',
      value: right.value
    }
  }
  
  consume(type, error) {
    if(this.check(type)) {
      return this.advance()
    }
    
    throw error
  }
  
  match(...types) {
    if(this.check(...types)) {
      return this.advance()
    }
    
    return false
  }
  
  check(...types) {
    if (this.isAtEnd()) return false
    
    return types.some(t => t === this.tokens[this.current].type)
  }
  
  isAtEnd() {
    return this.current >= this.tokens.length;
  }
  
  advance() {
    const v = this.tokens[this.current]
    this.current += 1
    return v;
  }
}

function evaluate(node) {
  switch(node.type) {
    case 'number': return node.value
    case 'binary': 
      switch(node.operator) {
        case '+': return evaluate(node.left) + evaluate(node.right);
        case '-': return evaluate(node.left) - evaluate(node.right);
        case '*': return evaluate(node.left) * evaluate(node.right);
        case '/': return evaluate(node.left) / evaluate(node.right);
      }
      
      throw 'unknown binary operator ' + node.operator
    case 'unary': 
      switch(node.operator) {
        case '-': return -evaluate(node.right)
      }
      throw 'unknown unary operator ' + node.operator
  }
  
  throw 'unkwown node type ' + node.type
}

const expr = '-100+7-9+4*2*3.265';

console.log('For the expression', expr, '...')

const tokens = new Scanner().scanTokens(expr)
console.log('The tokens are:', tokens)
const ast = new Parser().parse(tokens)
console.log('The ast nodes are:', ast)
console.log('Interpreter Result', evaluate(ast))
console.log('Eval Result', eval(expr))

控制台输出显示初始字符串、扫描的标记、解析的树和结果(以及与“eval”进行的比较)。