You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 08:35:24