React Native计算器括号功能实现:解决eval表达式报错问题
React Native计算器括号输入时
eval报错解决方案 我正在开发一款基于React Native的现代计算器,核心功能逻辑如下:
- 在
currentOperandView输入时触发onTextInput事件 - 用户输入
currentOperand时,实时计算结果并显示在previousOperand中 - 点击等于按钮后,
previousOperand置空,currentOperand更新为最终计算结果
添加开括号和闭括号按钮后出现问题:点击开括号后输入数字,会触发eval计算非法表达式(如eval('3(')),抛出"unexpected end of string"错误。尝试过在currentOperand包含括号时停止计算,但未解决问题,现寻求可行方案。
相关代码
App.js
import { useReducer, useState } from 'react'; import Clockcircleo from 'react-native-vector-icons/AntDesign'; import Backspace from 'react-native-vector-icons/Ionicons'; import { useFonts, Nunito_600SemiBold, Nunito_700Bold, } from '@expo-google-fonts/nunito'; import { View, StyleSheet, FlatList, TouchableOpacity, Text, Button, Alert, } from 'react-native'; import ButtonsData from './components/ButtonData'; import AppLoading from 'expo-app-loading'; const App = () => { let [fontsloaded] = useFonts({ Nunito_600SemiBold, Nunito_700Bold, }); const [green, setgreen] = useState(false); const [showOperand, setshowOperand] = useState(false); const operators = ['*', '/', '+', '-', '%', '(', ')']; function calculate(currentOperand) { let currentResult = currentOperand; if (currentResult == null && currentResult == undefined) { return null; } else { let lastChar = currentResult.charAt(currentResult.length - 1); if (operators.includes(lastChar)) { return null; } else { return eval(currentOperand); } } } let bracket = "*(" const reducer = (state, { type, payload }) => { switch (type) { case 'chooseDigit': setgreen(false); setshowOperand(false); if (state.overwrite) { return { ...state, currentOperand: payload, overwrite: false, }; } if(payload == "3" && state.currentOperand.includes(bracket)){ return setshowOperand(true) } if (payload == '0' && state.currentOperand == '0') { return state; } if (payload == '.' && state.currentOperand.includes('.')) { return state; } return { ...state, currentOperand: `${state.currentOperand || ''}${payload}`, }; case 'showCalculations': if (state.currentOperand == null && state.currentOperand == undefined) { return state; } if(state.currentOperand.includes("(")){ setshowOperand(true) } return { ...state, currentOperand: state.currentOperand, previousOperand: state.calulated || state.bracketHere ? state.currentOperand : calculate(state.currentOperand), calulated:false, }; case 'chooseOperation': if (payload == '×') { if (state.green) { return state; } else { return { ...state, currentOperand: `${state.currentOperand}*`, }; } } if(payload == "(" && state.currentOperand == null){ return { ...state, currentOperand:`${state.currentOperand || ""}${payload}`, calulated:true, bracketHere:true, } } if(payload == "(" && state.currentOperand % 10 !== null && !operators.includes(state.currentOperand % 10)){ return { ...state, currentOperand:`${state.currentOperand}*${payload}` } } if (payload == '÷') { if (state.green) { return state; } else { return { ...state, currentOperand: `${state.currentOperand}/`, }; } } if (state.currentoperand == null && state.previousOperand == null) { return state; } if (state.overwrite) { return { ...state, currentOperand: state.currentOperand, }; } if (operators.includes(state.currentOperand.slice(-1))) { return state; } return { ...state, currentOperand: `${state.currentOperand || ''}${payload}`, }; case 'backspace': if (state.currentOperand == null && state.previousOperand == null) { return state; } if (state.calulated) { return { ...state, currentOperand: state.currentOperand, }; } if (state.currentOperand.length == 1) { return { ...state, currentOperand: null, }; } if (state.overwrite) { return { ...state, overwrite: false, currentoperand: null, }; } return { ...state, currentOperand: state.currentOperand.slice(0, -1), previousOperand: null, }; case 'evaluation': setshowOperand(true); setgreen(true); if (state.currentOperand == null) { return state; } return { ...state, calulated: true, previousOperand: null, currentOperand: calculate(state.currentOperand), green: true, overwrite: true, }; case 'clear': return {}; default: return state; } }; const [{ currentOperand, previousOperand }, dispatch] = useReducer( reducer, {} ); const handleCalulations = (type, digit) => { dispatch({ type: type, payload: digit }); }; const renderButtons = ({ item }) => { return ( <View style={styles.buttnContainer}> <TouchableOpacity onPress={() => handleCalulations(item.type, item.digit)} style={[ styles.button, { backgroundColor: item.backgroundColor, }, ]}> <Text style={[ styles.buttonText, { color: item.color, }, ]}> {item.digit} <Text>{item.operation}</Text> </Text> </TouchableOpacity> </View> ); }; if (!fontsloaded) { return <AppLoading />; } return ( <View style={styles.container}> <View style={styles.screen}> <Text style={{ color: green ? '#427e04' : 'white', fontSize: 50, fontFamily: 'Nunito_600SemiBold', }} onTextLayout={ !showOperand ? () => dispatch({ type: 'showCalculations' }) : () => console.log('') }> {currentOperand} </Text> <Text style={{ color: 'grey', fontSize: 30, fontWeight: '100', fontFamily: 'Nunito_700Bold', }}> {previousOperand} </Text> <View style={{ display: 'flex', flexDirection: 'row', width: '100%', justifyContent: 'space-between', padding: 10, }}> <View style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around', marginTop: 15, width: 150, }}> <Clockcircleo name="clockcircleo" size={25} color="#fff" style={{ fontWeight: 5 }} /> <Clockcircleo name="calculator" size={25} color="#fff" style={{ fontWeight: 5 }} /> </View> <TouchableOpacity onPress={() => dispatch({ type: 'backspace' })} style={styles.backspace}> <Backspace name="backspace-outline" color="#427e04" style={{ fontSize: 20 }} /> </TouchableOpacity> </View> </View> <View style={{ paddingVertical: 10, paddingHorizontal: 5 }}> <FlatList numColumns={4} data={ButtonsData} key={(item) => item.digit} renderItem={renderButtons} ItemSeparatorComponent={() => ( <View style={{ marginRight: 3, width: 5, height: 5 }}></View> )} /> </View> </View> ); }; const styles = StyleSheet.create({ container: { backgroundColor: '#010101', height: '100%', }, screen: { height: '40%', paddingTop: 45, paddingBottom: 12, borderBottomColor: 'grey', borderWidth: 1, width: '90%', marginLeft: 14, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', alignItems: 'flex-end', paddingHorizontal: 10, }, buttnContainer: { width: 100, height: '240%', }, button: { width: 80, borderRadius: 50, height: undefined, margin: 4, aspectRatio: 1, borderWidth: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', }, buttonText: { fontSize: 35, }, backspace: { height: 40, width: 40, display: 'flex', justifyContent: 'center', alignItems: 'center', flexDirection: 'column', backgroundColor: '#8080806b', borderRadius: 50, }, }); export default App;
ButtonData.js
const ButtonsData = [ { digit: 'C', backgroundColor: '#171717', color: '#e97271', type:"clear" }, { digit: ".", backgroundColor: '#171717', color: '#427e04', type:"chooseDigit" }, { digit: '%', backgroundColor: '#171717', color: '#427e04', type:"chooseOperation" }, { digit: '÷', backgroundColor: '#171717', color: '#427e04', type:"chooseOperation" }, { digit: "1", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: "2", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: "3", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: '×', backgroundColor: '#171717', color: '#427e04', type:"chooseOperation" }, { digit: "4", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: "5", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: "6", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: '-', backgroundColor: '#171717', color: '#427e04', type:"chooseOperation" }, { digit: "7", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: "8", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: "9", backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: '+', backgroundColor: '#171717', color: '#427e04', type:"chooseOperation" }, { digit: '(', backgroundColor: '#171717', color: '#fff', type:"chooseOperation" }, { digit: ")", backgroundColor: '#171717', color: '#fff', type:"chooseOperation" }, { digit: '0', backgroundColor: '#171717', color: '#fff', type:"chooseDigit" }, { digit: '=', backgroundColor: '#427e04', color: '#fff', type:"evaluation" }, ]; export default ButtonsData;
解决方案
1. 重写calculate函数,增加合法性校验
function calculate(currentOperand) { if (!currentOperand) return null; // 统计括号数量,判断是否匹配 const openBrackets = (currentOperand.match(/\(/g) || []).length; const closeBrackets = (currentOperand.match(/\)/g) || []).length; if (openBrackets !== closeBrackets) return null; // 检查表达式结尾是否为非法字符 const lastChar = currentOperand.slice(-1); const invalidEndChars = ['*', '/', '+', '-', '%', '(']; if (invalidEndChars.includes(lastChar)) return null; try { // 用try-catch捕获所有语法错误 return eval(currentOperand); } catch (error) { return null; } }
2. 优化showCalculations动作逻辑
移除原有的showOperand强制判断,让计算逻辑自动决定是否显示结果:
case 'showCalculations': if (!state.currentOperand) return state; return { ...state, previousOperand: calculate(state.currentOperand), calulated: false, };
3. 清理冗余逻辑
删除无效的bracket变量和if(payload == "3" && state.currentOperand.includes(bracket))这段无意义的判断。
4. 修正括号输入的状态管理
在chooseOperation中处理括号时,移除calulated: true的设置,让计算逻辑自主判断:
if(payload == "(" && state.currentOperand == null){ return { ...state, currentOperand:`${state.currentOperand || ""}${payload}`, bracketHere: true, } }
核心改进点
- 括号匹配校验:只有当左右括号数量相等时才尝试计算
- 异常捕获:用
try-catch包裹eval,避免非法表达式直接抛出错误 - 结尾字符检查:排除以运算符或开括号结尾的无效表达式
- 冗余逻辑清理:移除无意义的判断,简化代码逻辑
内容的提问来源于stack exchange,提问作者Talha Shiekh
相关产品推荐
相关产品推荐

