资讯动态

用eval实现前端计算器:核心逻辑、安全过滤与边界处理详解

发布时间:2026/10/16 16:09:59 来源:尧图企业网站定制
做前端或者写工具脚本的朋友应该都遇过这种需求用户输入一串1234*(56-8)这样的算式界面直接返回结果。我第一次接到类似任务时脑子里闪过的第一个方案就是 JavaScript 的 eval 函数return eval(expr)一行代码就能计算开发速度确实快得离谱。但这函数在社区里的名声又比较微妙懂行的人一听到 eval 就皱眉觉得它又慢又不安全。这篇我就把用 eval 做计算器的完整思路拆开聊为什么大家会优先想到它、核心实现怎么写、怎么规避它自带的坑以及我踩过的那些真实问题。这个内容适合两类人看一类是刚接触前端、想做一个带交互的计算器练习项目的同学看完能直接复现另一类是已经写过类似功能、但想搞清楚 eval 边界和替代方案的开发者后面关于过滤、上下文隔离、异常捕获的部分应该能给你一些新角度。1. 需求拆解与方案选型1.1 计算器场景的真实需求先说计算器本身。你仔细想想用户期望的计算器其实不只是“能算加减乘除”这么简单它有一套隐含要求支持混合运算也就是四则运算同时出现并且遵循数学上的优先级规则支持括号用户会输入(23)*4这类带优先级的表达式能处理小数、连续运算比如3.5*21.8/2输入错误时给出清晰提示而不是直接白屏或给出一个匪夷所思的值在某些扩展场景里还需要支持取余、幂运算、百分数换算等如果从头实现表达式解析你要先做词法分析、再转后缀表达式、再求值一套流程做下来至少两三百行代码。而 eval 方案几乎把这一步全部吞掉了JavaScript 引擎本身就是最专业的表达式解析器你把字符串丢给它它自己会按运算符优先级、括号规则、小数规则去算。这里想说的核心是用 eval 做计算器不是偷懒而是把“表达式求值”这个通用能力交给引擎去处理我们只需要解决“输入进来的是什么”、“计算完之后怎么处理”这两件事。1.2 为什么很多人第一个想到 eval我复盘过自己当初的选择也问过几个写前端的同事大家不约而同选 eval 的理由其实很朴素第一代码量真的少。不用写解析器不用处理运算符栈核心计算逻辑一行搞定。对于一个小计算器工具来说省下来的几十行代码意味着更少的出错点和更容易维护的逻辑。第二表达式语法天然正确。JavaScript 引擎对运算符优先级的理解是经过千万级场景验证的23*4它就会先算乘除再算加减(23)*4它会先处理括号这些规则不用你重新实现也不容易出现边界 bug。第三扩展能力极强。今天你的计算器只做加减乘除明天用户说想要Math.sqrt、想要幂运算Math.poweval 方案只需要把输入字符串按规则拼接函数调用自然就能被引擎解析执行你不需要为了一个新功能去改整个求值内核。但这里必须说清楚eval 的计算能力是把双刃剑。它之所以能计算Math.pow(2, 10)是因为它把字符串当成真正的 JavaScript 代码来执行这意味着如果输入内容不加控制它也能执行任何 JavaScript 语句。所以用 eval 做计算器真正的难点从来不是计算而是怎么把求值能力关在笼子里。1.3 eval 之外的备选方案对比写这段不是为了否定 eval而是想给你一个完整的选择视角。我在做第二个计算器版本时确实认真评估过替代方案结论是各有优劣取决于使用场景。方案工作量安全性表达式支持范围适合场景eval 直接求值极小低需严格过滤几乎无限但危险本地可信输入、练习项目、内部工具Function 构造器小较低同样需过滤几乎无限但隔离相对好与 eval 类似可避免直接作用域污染第三方表达式库中较高取决于库通常覆盖四则运算函数对安全有要求、需要长期维护的项目手写解析器大最高完全可控教育项目、定制化语法、高安全场景我的个人建议是如果你只是做一个练习用计算器、或者内网工具中的一个计算面板输入源是可控的eval 完全够用。如果你做的是公网页面、面向任意用户输入那至少要套上完整的过滤和加固方案或者直接用成熟的表达式求值库。手写解析器虽然安全但对大多数人来说投入产出比太低属于杀鸡用牛刀。2. eval 函数核心机制拆解2.1 eval 到底做了什么你每天都在用的浏览器它的 JavaScript 引擎在运行时遇到 eval 函数会把这个函数接收的字符串参数当作真实的 JavaScript 源代码走一遍词法分析、语法分析、代码生成、执行的全流程。这么说可能有点抽象我用一个生活类比解释一下你在代码里写let a 1引擎看到的是静态源码直接编译执行而 eval 接收的字符串相当于引擎没有把它当普通字符串而是把它当成“写好的代码片段”临时丢进运行环境里启动执行。这个能力本身是中性的就像一台打印机你给它文档它印文档给它图片它印图片危险与否取决于你往里面塞了什么。在实际运行中eval 会拿到字符串、解析表达式、计算出结果然后把结果返回给调用方。举一个最简单的例子const result eval(2 3 * 4); console.log(result); // 14这段代码中 eval 返回的是数字 14因为是3*4先算再与 2 相加。引擎处理括号、小数、负数、函数调用时全部按照 JavaScript 语法规则来这也是它能作为计算器核心的原因你已经拥有一个无条件递归下降解析器和表达式求值器只是平时没用上它。2.2 eval 的执行上下文与作用域问题用 eval 做计算器时有个很容易忽略的细节eval 在哪个作用域执行它就能访问到哪个作用域的变量。看这段代码function calculate(expr, extraNumber) { let hiddenValue extraNumber * 2; return eval(expr); } calculate(hiddenValue 1, 5); // 11如果调用方传入的 expr 是hiddenValue 1eval 直接就能访问到函数内部的局部变量 hiddenValue。这个特性在日常计算器场景下似乎没什么问题但埋着一个隐患如果你在计算器函数内部定义了一些临时变量这些变量全都在表达式求值的可见范围里。恶意用户虽然是通过页面输入框操作但如果有人往输入框里输入精心构造的内容可能会导致内部状态被读取或篡改。我自己在实际项目中处理这个问题时通常不希望计算表达式能访问任何外部变量也就是让表达式在“真空环境”里求值。常用的思路有两种一是把表达式处理成“自包含”的纯数字和运算符字符串不包含任何变量名二是对输入做严格白名单校验只允许出现数字、四则运算符、括号、小数点、百分号这些字符从源头把变量访问、函数调用、方法访问全部截断。2.3 用 eval 解析表达式的基本形态要让 eval 按计算器逻辑工作需要把用户的表达式先做格式整理。我在实际开发中总结了一套标准流程去掉字符串首尾的空格统一处理换行符和冗余空白字符避免解析歧义把用户习惯的运算符号转成 JavaScript 引擎能识别的写法比如乘法×转成*、除法÷转成/处理百分号用户输入的50%在多数计算器软件中意味着0.5但 JavaScript 的%是取余运算直接传给 eval 会得到奇怪结果必须预先转换处理连续运算符比如3-5用户可能习惯写但引擎不一定按你预期解析需要做规范化这样整理后传给 eval 的字符串已经是标准的 JavaScript 表达式。举个例子输入: (4.5 × 2 6 ÷ 3) % 处理: 去空格 → (4.5*26/3)% 百分号处理 → (4.5*26/3)/100 eval 结果 → 0.12所以后续所有核心工作的关键词就变成两个转换规则、边界处理。3. 计算器功能实操实现3.1 基础架构设计做计算器之前我习惯先画一个最小功能边界避免写着写着需求膨胀。一个典型的 eval 计算器项目模块可以拆成下面这几块输入区一个文本框或者由按钮拼接的表达式显示区表达式预处理器负责清洗、替换、格式化原始输入安全过滤器负责校验输入是否为合法算式拦截危险代码核心计算引擎调用 eval 并返回结果异常处理与结果显示如果你用前端框架写组件结构可以把预处理、过滤、计算分别拆成独立函数方便单元测试。如果是纯原生 JavaScript也用同样的思路至少按函数拆不要全揉在一个 onelick 事件里。我见过不少初学者把下面这种逻辑直接写在按钮点击事件里btn.onclick () { result.innerText eval(input.value); };这样写确实能跑但问题很大。没有预处理、没有过滤、没有异常捕获任何输入都会直接进 eval这相当于把引擎的无限制执行能力完全暴露给用户。我建议哪怕是最简单的练习项目也要把“输入清洗”和“错误提示”这两部分加上从一开始培养好的安全习惯。3.2 表达式校验与过滤逻辑这是整个 eval 计算器最核心的一环我会在这一小节讲得很细因为它直接决定你的计算器是“工具”还是“漏洞集合”。先定义一个允许字符集合。对于标准四则运算计算器允许的字符包括数字0-9小数点.四则运算符 - * /括号( )百分号%需要预转换空白字符对应的正则表达式可以写成const allowedPattern /^[0-9\-*/().%\s]$/;这个正则只允许数字、四则运算符、括号、小数点、百分号和空格。任何匹配不上的字符比如字母、引号、花括号、分号、方括号都直接拒绝。这一条就能拦截掉绝大多数危险输入因为恶意代码大多依赖标识符和特殊语法。但光有这个还不够。正则只是保证“字符集合合法”并不能保证表达式本身是合法的数学公式。我们还要做结构校验比如括号必须左右匹配运算符不能连续出现除了和-用于正负号的情况表达式不能以运算符开头或结尾正负号开头除外不能出现类似2..3或者2.3.4这样的非法小数我在实际项目里会写一个轻量的校验函数一边扫描一边记录状态也可以用正则分段校验。核心逻辑类似这样function validateExpression(expr) { if (!allowedPattern.test(expr)) { return { ok: false, reason: 包含非法字符 }; } let balance 0; for (let i 0; i expr.length; i) { const ch expr[i]; if (ch () balance; if (ch )) balance--; if (balance 0) return { ok: false, reason: 括号不匹配 }; } if (balance ! 0) return { ok: false, reason: 括号未闭合 }; if (/[\-*/]{2,}/.test(expr.replace(/\s/g, ))) { return { ok: false, reason: 存在连续运算符 }; } return { ok: true }; }这段代码里有一个细节连续的--和-在某些语言中有特殊含义比如3--2被引擎理解为3 - -2也就是 3 加 2但为了把边界情况控制住我建议普通计算器直接禁止连续运算符。如果你确实想支持3-5这种写法那要做更精细的上下文判断看运算符前后是否紧邻数字、括号和小数点。还有一点要注意过滤表达式时不要只过滤完再 eval而应该在校验通过之后再 eval。我的习惯是“先拦截再计算”只要校验有一点不确定就直接拒绝计算并返回友好提示而不是抱着侥幸心理把它丢给 eval。3.3 完整代码实现与解析下面给出一段可以直接复用的计算器核心模块。它包含预处理、校验、eval 调用、结果格式化四个部分整体复杂度控制在可维护范围内。/** * 计算器核心模块 */ class SafeCalculator { constructor() { // 运算符与特殊符号映射 this.replacements { ×: *, ÷: /, x: *, X: *, : ,, }; } preprocess(input) { let expr String(input).trim(); for (const [from, to] of Object.entries(this.replacements)) { expr expr.split(from).join(to); } // 百分号转换50% - 0.5独立% - /100 expr expr.replace(/(\d(?:\.\d)?)%/g, ($1/100)); expr expr.replace(/%/g, ); // 删除所有空白 expr expr.replace(/\s/g, ); return expr; } validate(expr) { const allowedPattern /^[0-9\-*/().,]$/; if (!allowedPattern.test(expr)) { return { ok: false, reason: 表达式包含非法内容 }; } let balance 0; for (const ch of expr) { if (ch () balance; if (ch )) balance--; if (balance 0) return { ok: false, reason: 括号配对错误 }; } if (balance ! 0) return { ok: false, reason: 括号未闭合 }; if (/[\-*/.]{2,}/.test(expr)) { return { ok: false, reason: 存在连续运算符或连续小数点 }; } if (/^[\-*/.]/.test(expr.replace(/^[\-]/, ))) { return { ok: false, reason: 表达式格式不正确 }; } return { ok: true }; } calculate(input) { let expr this.preprocess(input); const valid this.validate(expr); if (!valid.ok) { throw new Error(valid.reason); } try { const result eval(expr); if (typeof result ! number || !isFinite(result)) { throw new Error(计算结果无效); } return this.formatResult(result); } catch (e) { throw new Error(e.message || 计算过程出错); } } formatResult(num) { // 解决浮点精度问题 const rounded parseFloat(num.toPrecision(12)); return rounded; } }这里有个我特意加入的细节toPrecision(12)。因为 JavaScript 浮点运算存在精度问题比如0.10.2实际结果是0.30000000000000004直接展示很尴尬。用toPrecision(12)把多余精度丢掉再转回数字大多数情况下能得到符合直觉的结果。还有一处需要注意验证连续运算符时我把小数点也纳入了连续检查。原因是1..2、1.2.3这类输入在 JavaScript 引擎里可能解析出意想不到的结果直接标记为非法更稳妥。3.4 界面交互实现思路核心模块写好之后界面上可以有两种交互形态。一种是传统的计算器网格按钮另一种是类似输入框加实时计算的形态。我做过两种体验差异挺大。计算器网格按钮适合移动端和纯工具场景每个按钮往表达式的末尾追加字符用户不容易输入非法内容因为你能在按钮层面就控制住哪些字符可以按。比如按钮上没有%之外的符号用户就没机会输入字母。但这种方案无法处理“用户想粘贴一段表达式”的需求。输入框加实时计算适合嵌在管理后台或工具集里。用户可以直接在输入框里写完整公式点击计算或者监听输入事件实时出结果。这种形态对输入校验的要求就高了必须把过滤逻辑做扎实。我自己常用的交互是输入框实时监听放在防抖函数里停止输入 300 毫秒后自动计算并显示结果同时把错误提示显示在输入框下方。还有一个实用的细节在输入框的键盘事件里拦截回车键回车时强制触发计算。用户使用习惯上按回车意味着“我输入完了给我结果”这个体验计算器类产品要支持。4. 安全加固与边界处理4.1 eval 的典型风险场景用 eval 做计算器必须正视它的安全风险。我拆解成几个典型场景来说明第一个是直接注入。如果用户输入2; document.body.innerHTMLeval 会先计算结果或执行分号后的语句。当你没有做任何过滤直接调用 eval 时这行代码完全可能被执行。在单页面应用里这可能导致整个页面被破坏如果恶意代码更复杂还可能读取本地存储、发送网络请求等。第二个是访问内部变量和函数。eval 在调用它的函数作用域内运行可以直接访问该作用域内的所有变量。如果计算器模块里碰巧有一个名字敏感的变量比如 token、key用户构造的表达式可能把它取出来。虽然在计算器场景里这个概率不大但设计原则是不要给这个可能性留下任何机会。第三个是性能损耗。JavaScript 引擎无法对 eval 中的代码做静态优化每次调用都要重新解析和编译。对单个表达式计算来说这个损耗可以忽略但如果你在循环里或者高频实时触发场景里频繁调用 eval性能问题就会暴露出来。这也是我在实时输入的方案里加防抖的原因之一。第四个是调试困难。eval 的调用栈和上下文是动态的一旦表达式出错错误信息往往比较晦涩。如果你不包一层异常处理用户会看到一串看不懂的错误堆栈体验非常糟糕。这里要补充一句eval 被诟病最多的场景是“处理不可信输入”这是事实。但我们做计算器时目标不是“完全不能用 eval”而是“只允许算式被求值不允许其它任何能力被触发”。只要把这一点想清楚安全策略就有方向了。4.2 白名单校验与字符过滤上一节讲的 allowedPattern 已经做了一层白名单过滤这一层我称为“字符级白名单”。除了它之外我建议再加一层更严格的逻辑简单来说就是“只认结构不认内容”。一个可靠的做法是对表达式的每个字符做类型标记然后检查是否符合“数字-运算符-括号-小数点-数字”的交替模式。这种写法不依赖正则的复杂匹配直接用状态机思路逐个字符扫描function validateExpression(expr) { const validChars new Set(0123456789.-*/(),%); for (const ch of expr) { if (!validChars.has(ch)) return false; } return true; }这个函数比正则版本更清晰也更方便扩展。比如你希望后续增加对sin(、cos(这些函数的支持只需在 validChars 里允许字母再配合关键字白名单即可。但如果你的计算器不对函数开放我建议连字母出现的可能性都不要留。除了字符过滤还要做符号上下文校验。规则包括、-可以出现在表达式开头表示正负数否则必须是数字或括号在前*、/前面必须是数字或右括号括号前如果是数字需要补上乘法运算符比如2(34)在数学里合法但在 JavaScript 引擎里是函数调用直接拒绝或自动补2*(34)二选一我实际选择的是自动补乘法符号因为用户输入2(34)的意图大概率是 2 乘以括号内容。虽然这种输入严格来说不规范但把用户体验放在优先位置自动转换成2*(34)再求值效果更好。转换逻辑可用正则实现expr expr.replace(/(\d)\(/g, $1*();这里需要注意只处理“数字后跟左括号”的情况其他组合不做转换避免改变表达式语义。4.3 异常捕获与错误提示eval 的执行异常有三种来源。第一种是语法错误比如表达式是23引擎解析失败抛出一个 SyntaxError。第二种是运行时错误比如访问了不存在的对象在计算器场景下比较少见但恶意输入时可能出现。第三种是我主动抛出的业务错误比如校验失败、结果不是数字。捕获异常的最佳位置是在 calculate 函数内部包一层 try-catch并在 catch 里重新包装错误信息让界面层拿到的永远是用户能理解的提示。try { const result eval(expr); // ... } catch (e) { if (e instanceof SyntaxError) { throw new Error(表达式格式不正确请检查输入); } throw new Error(计算失败 e.message); }错误提示的措辞也有讲究。计算器用户的常见错误就是多打了一个运算符、括号没闭合、或者输错字符。提示要具体、直接不要抛出引擎的原始错误信息。我通常会把错误信息分档包含非法字符 → “只能输入数字、括号和四则运算符”括号不合法 → “括号匹配错误”连续运算符 → “运算符之间有缺失数字”结果不是数字 → “无法计算该表达式”结果超出安全范围 → “结果过大或无法精确表示”这层提示系统看着琐碎实际体验提升非常明显。用户看不到SyntaxError: Unexpected token只能看到“括号未闭合”心里立刻知道自己哪里错了。5. 常见问题与排查技巧实录5.1 连续运算符处理这是初学者最容易踩的坑。用户输入5-3数学上等价于5(-3)很多人预期结果是 2。引擎解析时-会被理解成正负符号还是运算符规则比较隐晦。我建议分情况处理表达式以-开头比如-53这是合法的负数模式直接保留表达式中间出现-、--、*-等组合要看前后符号。最简单的策略是统一用正则把(-转换成(-或者直接拒绝我自己的策略是支持-作为负号但不支持--连续两个减号。处理时先做整体校验再决定是否转换。如果校验通过但 eval 结果不符合预期可以打印归一化后的表达式排查。5.2 括号不匹配括号不匹配最常见的场景是用户漏掉右括号比如输入(23*4。你在校验逻辑里用计数器扫描整个表达式遇到左括号加一右括号减一过程中计数为负说明右括号多了最终不为零说明有未闭合括号。这个方法能覆盖绝大多数情况。还有一种特殊场景嵌套括号((23)*4)只要计数器逻辑正确嵌套层级不影响判断。但要注意括号内是空的情况比如()、( )这类输入传给 eval 会报错校验时需要额外检测括号内是否有数字或运算符。5.3 百分号语义冲突百分号是我遇到最有意思的一个分歧点。传统计算器上50%通常代表0.5但在某些行业计算里10%5%又可能被期望等同于0.10.05。JavaScript 引擎的%在表达式中是取余运算符直接传50%进去引擎会尝试把它解析成取余运算结果往往是 NaN 或者根本不合法。我的处理思路是预先把百分号转换成等价值表达式。对于50%转换为(50/100)对于(23)%转换为((23)/100)。这样可以保持用户输入习惯语义也统一为“占整体的百分比”。如果你需要不同语义的百分号要在配置层增加一个开关而不是贸然假定一种规则。5.4 浮点精度与结果显示你输入0.1 0.7eval 返回的可能是0.7999999999999999这不是 bug是 IEEE 754 浮点数表示的限制。计算器这种面向普通用户的工具直接把这个数显示出来显得很不专业。我给的方案是结果格式化function formatNumber(num) { if (!isFinite(num)) return 数值无效; const rounded Math.round(num * 1e10) / 1e10; return String(rounded); }这里用乘 1e10 再取整再除的方式固定精度对于日常计算足够。如果你的计算器涉及科学计数法或者非常小的小数可以考虑用 toPrecision 或配置最大小数位。另外要留意Infinity和NaN这两种特殊值。除数为零时引擎会返回Infinity而不是直接报错你在结果判断里要主动拦截提示用户“不能除以零”而不是显示一个诡异的Infinity。NaN 通常出现在解析失败或非法运算里同样需要拦截。5.5 浏览器兼容问题eval 的兼容性本身不用太担心现代浏览器都支持甚至老到 IE 时代都能跑。真正需要关注的是你在预处理器里用的那些 ES6 语法特性比如 Set、箭头函数、模板字符串。如果你想做一个老旧环境也能运行的计算器代码里尽量避免这些特性或者使用构建工具做语法降级。还有一点部分严格模式环境如启用了 CSP 的页面会阻止 eval 执行。如果在受限环境里使用控制台会报一个Refused to evaluate a string as JavaScript的错误。这种场景下你需要调整策略改用接受表达式字符串并解析的库或者把计算过程放到 Web Worker 里但 CSP 限制同样可能影响 Worker又或者退回手写解析器。结尾我前后做过三个不同场景的 eval 计算器版本第一个是练习项目直接把 eval 用在按钮点击事件里代码短但后续几乎没法扩展第二个做成了带预处理器和过滤器的小工具维护起来舒服很多第三个是在受限环境里重写的改成了手写解析器。一个很清晰的体会是eval 本身不邪恶但使用 eval 的人必须清楚它的边界在哪里。对一个计算器来说真正的价值不是“能算出结果”而是“能稳定、安全、清晰地把结果算出来”。建议你不管做什么项目哪怕只是自己写着玩也要把输入校验、异常捕获、结果格式化这三件小事做完整这比代码短三行重要得多。最后再分享一个实用小技巧调试这种计算器时不要直接看 eval 的结果先把预处理后的表达式打出来。我遇到过的绝大多数诡异计算问题都是预处理阶段出的幺蛾子不是 eval 本身的问题。每次调试先确认这一步的输入输出对不对能帮你省下一大把时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑