JavaScript科学计算器三角函数功能失效问题(分词替代eval)
问题描述
我正在用JavaScript开发一款科学计算器,基础计算功能正常,但三角函数等科学计算功能无法工作。之前用eval()实现计算逻辑,出于安全考虑改成了分词(tokenization)方法,现在三角函数计算失效。

完整代码
JavaScript部分
const userInput = document.getElementById('input'); const userOutput = document.getElementById('output'); const clearbtn = document.getElementById('clear'); const calBtn = document.getElementById('calcBtn'); const btns = document.querySelectorAll('input[type="button"]'); function calc() { btns.forEach(button => { button.addEventListener('click', () => { const btnDataType = button.dataset.type; if (btnDataType === 'number' || btnDataType === 'operator') { userInput.value += button.value; } else if (btnDataType === 'Soperator') { userInput.value += `${button.value}(`; } calBtn.addEventListener("click", () => { let tokens = userInput.value.match(/(\d+(\.\d+)?)|[+\-*/%()]|sin|cos|tan/g); if (!tokens) { console.log("Error: Invalid input"); return; } console.log("Tokens:", tokens); for (let i = 0; i < tokens.length; i++) { if (['sin', 'cos', 'tan'].includes(tokens[i]) && tokens[i + 1] === '(') { let argStart = i + 2; let argEnd = tokens.indexOf(')', argStart); if (argEnd === -1) { console.log("Error: Missing closing parenthesis"); return; } let angle = parseFloat(tokens.slice(argStart, argEnd).join('')); let result; if (tokens[i] === 'sin') { result = Math.sin(angle * (Math.PI / 180)); } else if (tokens[i] === 'cos') { result = Math.cos(angle * (Math.PI / 180)); } else if (tokens[i] === 'tan') { result = Math.tan(angle * (Math.PI / 180)); } tokens.splice(i, argEnd - i + 1, result.toString()); i--; } } for (let i = 0; i < tokens.length; i++) { if (['*', '/', '%'].includes(tokens[i])) { let left = parseFloat(tokens[i - 1]); let right = parseFloat(tokens[i + 1]); let result = tokens[i] === '*' ? left * right : tokens[i] === '/' ? left / right : left % right; tokens.splice(i - 1, 3, result.toString()); i--; } } for (let i = 0; i < tokens.length; i++) { if (['+', '-'].includes(tokens[i])) { let left = parseFloat(tokens[i - 1]); let right = parseFloat(tokens[i + 1]); let result = tokens[i] === '+' ? left + right : left - right; tokens.splice(i - 1, 3, result.toString()); i--; } } userOutput.value = tokens[0]; }); }); }); document.getElementById("cut").addEventListener('click', () => { userInput.value = userInput.value.slice(0, -1); }); clearbtn.addEventListener('click', () => { userInput.value = ''; userOutput.value = ''; }); } calc();
HTML部分
<!-- header section --> <header> <nav> <a href="">Logo</a> <button>contribute here</button> </nav> </header> <!-- main --> <main> <section> <!-- calculator wrapper div --> <div> <!-- input and output screens --> <div> <input type="text" placeholder="Type to Calculate" id="input"> <input type="text" placeholder="Output" readonly id="output"> <button id="clear">Clear</button> </div> <!-- wrapper that contains number btns , operators --> <div> <!-- numbers --> <div> <input type="button" value="0" id="0" data-type="number"> <input type="button" value="1" id="1" data-type="number"> <input type="button" value="2" id="2" data-type="number"> <input type="button" value="3" id="3" data-type="number"> <input type="button" value="4" id="4" data-type="number"> <input type="button" value="5" id="5" data-type="number"> <input type="button" value="6" id="6" data-type="number"> <input type="button" value="7" id="7" data-type="number"> <input type="button" value="8" id="8" data-type="number"> <input type="button" value="9" id="9" data-type="number"> </div> <!-- operators btns --> <div> <input type="button" value="+" id="+" data-type="operator"> <input type="button" value="-" id="-" data-type="operator"> <input type="button" value="*" id="*" data-type="operator"> <input type="button" value="/" id="/" data-type="operator"> <input type="button" value="%" id="%" data-type="operator"> <button id="calcBtn">Calculate</button> <button id="cut">X</button> </div> </div> </div> <!-- scientific calculator --> <div> <input type="button" value="Sin" id="sin" data-type="Soperator"> <input type="button" value="Cos" id="cos" data-type="Soperator"> <input type="button" value="Tan" id="tan" data-type="Soperator"> <input type="button" value="sin⁻¹" id="sin-in" data-type="Soperator"> <input type="button" value="cos⁻¹" id="cos-in" data-type="Soperator"> <input type="button" value="tan⁻¹" id="tan-in" data-type="Soperator"> </div> </section> <section> </section> </main> <script src="cal.js"></script>
修复方案
问题根源
- 大小写不匹配:分词正则匹配的是小写
sin|cos|tan,但按钮输入的是大写开头的Sin(、Cos(、Tan(,导致分词时无法识别三角函数关键字,处理逻辑完全没触发。 - 事件重复绑定:计算按钮的点击事件被嵌套在所有按钮的点击监听里,每次点击任意按钮都会给计算按钮新增一次事件绑定,导致多次执行计算逻辑。
修改后的完整代码
JavaScript部分
const userInput = document.getElementById('input'); const userOutput = document.getElementById('output'); const clearbtn = document.getElementById('clear'); const calBtn = document.getElementById('calcBtn'); const btns = document.querySelectorAll('input[type="button"]'); function calc() { btns.forEach(button => { button.addEventListener('click', () => { const btnDataType = button.dataset.type; if (btnDataType === 'number' || btnDataType === 'operator') { userInput.value += button.value; } else if (btnDataType === 'Soperator') { // 统一转成小写格式,同时把反函数符号转成易识别的格式 const funcName = button.value.toLowerCase().replace('⁻¹', '-1'); userInput.value += `${funcName}(`; } }); }); // 将计算按钮事件移到外层,避免重复绑定 calBtn.addEventListener("click", () => { // 扩展正则,支持三角函数及反函数 let tokens = userInput.value.match(/(\d+(\.\d+)?)|[+\-*/%()]|sin|cos|tan|sin-1|cos-1|tan-1/g); if (!tokens) { userOutput.value = "输入无效"; return; } // 先处理三角函数(包括反函数) for (let i = 0; i < tokens.length; i++) { const func = tokens[i]; if ((['sin', 'cos', 'tan', 'sin-1', 'cos-1', 'tan-1'].includes(func)) && tokens[i + 1] === '(') { let argStart = i + 2; let argEnd = tokens.indexOf(')', argStart); if (argEnd === -1) { userOutput.value = "缺少闭合括号"; return; } // 先计算括号内的表达式(支持运算,比如sin(30+20)) let argTokens = tokens.slice(argStart, argEnd); // 处理乘除模 for (let j = 0; j < argTokens.length; j++) { if (['*', '/', '%'].includes(argTokens[j])) { let left = parseFloat(argTokens[j - 1]); let right = parseFloat(argTokens[j + 1]); let result = argTokens[j] === '*' ? left * right : argTokens[j] === '/' ? left / right : left % right; argTokens.splice(j - 1, 3, result.toString()); j--; } } // 处理加减 for (let j = 0; j < argTokens.length; j++) { if (['+', '-'].includes(argTokens[j])) { let left = parseFloat(argTokens[j - 1]); let right = parseFloat(argTokens[j + 1]); let result = argTokens[j] === '+' ? left + right : left - right; argTokens.splice(j - 1, 3, result.toString()); j--; } } let angle = parseFloat(argTokens[0]); let result; switch(func) { case 'sin': result = Math.sin(angle * (Math.PI / 180)); break; case 'cos': result = Math.cos(angle * (Math.PI / 180)); break; case 'tan': result = Math.tan(angle * (Math.PI / 180)); break; case 'sin-1': result = Math.asin(angle) * (180 / Math.PI); break; case 'cos-1': result = Math.acos(angle) * (180 / Math.PI); break; case 'tan-1': result = Math.atan(angle) * (180 / Math.PI); break; } tokens.splice(i, argEnd - i + 1, result.toString()); i--; } } // 处理乘除模 for (let i = 0; i < tokens.length; i++) { if (['*', '/', '%'].includes(tokens[i])) { let left = parseFloat(tokens[i - 1]); let right = parseFloat(tokens[i + 1]); let result = tokens[i] === '*' ? left * right : tokens[i] === '/' ? left / right : left % right; tokens.splice(i - 1, 3, result.toString()); i--; } } // 处理加减 for (let i = 0; i < tokens.length; i++) { if (['+', '-'].includes(tokens[i])) { let left = parseFloat(tokens[i - 1]); let right = parseFloat(tokens[i + 1]); let result = tokens[i] === '+' ? left + right : left - right; tokens.splice(i - 1, 3, result.toString()); i--; } } userOutput.value = tokens[0]; }); document.getElementById("cut").addEventListener('click', () => { userInput.value = userInput.value.slice(0, -1); }); clearbtn.addEventListener('click', () => { userInput.value = ''; userOutput.value = ''; }); } calc();
关键修改说明
- 统一格式匹配:点击科学按钮时将函数名转为小写,同时把
sin⁻¹转成sin-1,确保正则能准确识别。 - 修复事件绑定:将计算按钮的点击事件移到外层循环外,避免重复绑定导致多次执行。
- 支持复杂参数:新增括号内表达式计算逻辑,支持
sin(30+20)这类带运算的参数。 - 扩展反三角函数:新增
sin⁻¹、cos⁻¹、tan⁻¹的计算支持。
内容的提问来源于stack exchange,提问作者Adarsh tripathi
相关产品推荐
相关产品推荐

