1. 项目概述:用JavaScript函数模拟一个真实的ATM机
如果你正在学习JavaScript,或者想找个项目来巩固函数、对象和状态管理的概念,模拟一个ATM机取款机绝对是个绝佳的选择。这听起来像是个简单的练习,但真要把它做得像模像样,里面涉及到的编程思想可不少。从最基本的函数封装、条件判断,到更高级的状态管理、数据验证,甚至模拟异步操作,都能在这个项目里得到实践。我见过很多新手教程里的“ATM机”例子,大多只停留在输入密码、查询余额、取款这几步,逻辑简单到几乎脱离现实。今天,我想带你一起,用JavaScript构建一个功能更完整、逻辑更严谨、甚至带点“用户体验”考量的ATM机模拟程序。这不仅仅是写几个函数,而是学习如何用代码去建模一个真实的业务流程,思考每一步操作背后的边界条件和异常处理。无论你是刚学完JavaScript基础语法的初学者,还是想找个小项目练手的中级开发者,相信这个从零到一的构建过程,都能给你带来一些新的启发。
2. 核心需求与功能设计拆解
在动手写代码之前,我们得先想清楚,一个ATM机到底应该做什么。我们不能只满足于“能取钱”,而要模拟出用户从插卡到退卡的完整交互旅程。
2.1 ATM机核心业务流程分析
一个典型的ATM交易流程,远不止输入金额然后吐钞那么简单。我们需要拆解出以下几个核心阶段:
- 身份验证阶段:用户插入卡片(或输入卡号),然后输入密码。系统需要验证密码是否正确,并判断账户是否被锁定或存在异常。这是所有交易的安全大门。
- 主菜单交互阶段:验证通过后,ATM会展示一个功能菜单,例如:查询余额、取款、存款、修改密码、转账等。用户通过按键(在我们的程序里就是输入数字选项)来选择服务。
- 具体交易执行阶段:根据用户选择,进入相应的子流程。每个子流程都有其特定的逻辑和验证规则。
- 交易反馈与结束阶段:完成操作后,显示结果(如“取款成功”、“余额不足”),询问用户是否继续其他服务,最后模拟退卡过程。
我们的JavaScript程序需要清晰地模拟出这几个状态的变化。用户不可能在未登录时直接查询余额,也不可能在取款过程中突然跳转到转账。这就引出了状态管理的概念。我们可以用一个变量(比如currentState)来记录当前处于哪个阶段,从而控制哪些函数可以被调用,哪些菜单应该被显示。
2.2 模拟ATM需要实现的关键功能
基于业务流程,我们可以规划出以下必须实现的功能模块:
- 账户系统:这是数据的基石。我们需要一个或多个账户对象,用来存储卡号、密码、余额、当日取现额度等关键信息。通常,我们会用一个对象数组或一个以卡号为键的Map来模拟数据库。
- 密码验证:接收用户输入,与存储的密码进行比对。必须考虑错误次数限制,比如连续输错3次就临时锁定账户,防止暴力破解。
- 余额查询:最简单的功能,但也是展示账户状态的核心。
- 取款功能:这是最复杂的之一。它需要:
- 检查余额是否充足。
- 检查取款金额是否为ATM支持的面额(如100的整数倍)。
- 检查是否超过单笔取款限额和当日累计取款限额。
- 更新账户余额和当日已取款额度。
- 模拟点钞和吐钞的“过程感”(可以用
setTimeout模拟延迟)。
- 存款功能:相对简单,主要是更新余额。但严谨一点,也可以模拟一个“请放入钞票”的提示和“正在验钞”的延迟。
- 修改密码:需要验证旧密码,然后要求输入两次新密码以确保一致。
- 转账功能(进阶):需要输入对方账号,验证对方账户是否存在,检查自身余额,然后进行资金划转。这涉及到两个账户对象的联动更新。
- 菜单驱动界面:由于我们是在控制台(Console)或简单的网页输入框里模拟,所以需要一个清晰的文本菜单系统,根据不同的状态循环或递归地提示用户输入。
注意:在真实项目中,密码绝不会以明文形式存储在客户端JavaScript中。这里我们只是为了模拟逻辑而简化处理。在实际的Web应用中,密码验证和所有账户操作都必须在服务端进行,前端只负责交互和展示。
3. 账户数据建模与初始化
任何金融系统的核心都是数据。我们先抛开花哨的界面,把地基打牢——创建并初始化我们的模拟银行账户数据。
3.1 设计账户对象结构
一个银行账户需要哪些属性?我们用一个JavaScript对象来定义:
/** * 模拟银行账户对象结构 * @typedef {Object} BankAccount * @property {string} cardNumber - 银行卡号,作为唯一标识 * @property {string} pin - 密码(模拟场景下为明文,仅用于演示) * @property {number} balance - 账户余额(单位:元) * @property {number} dailyWithdrawalLimit - 当日取款总额度 * @property {number} dailyWithdrawnAmount - 当日已取款金额 * @property {boolean} isLocked - 账户是否因密码错误过多被锁定 * @property {number} failedAttempts - 连续密码错误次数 */为什么需要dailyWithdrawnAmount和isLocked?这就是模拟真实性的细节。ATM会限制你一天内能取多少钱,并且有安全机制。failedAttempts用来实现输错密码锁定账户的逻辑。
3.2 创建模拟账户数据库
有了结构,我们来创建一些初始账户。通常我们会用一个数组来存放,但为了查询效率(根据卡号找账户),使用Map或普通对象以卡号为键会更方便。
// 使用Map存储账户,键为卡号,值为账户对象 const accountsDatabase = new Map(); // 初始化几个模拟账户 accountsDatabase.set('1234567890123456', { cardNumber: '1234567890123456', pin: '123456', // 警告:仅为演示!真实情况绝不可明文存储密码。 balance: 5000.00, dailyWithdrawalLimit: 2000.00, dailyWithdrawnAmount: 0.00, isLocked: false, failedAttempts: 0 }); accountsDatabase.set('9876543210987654', { cardNumber: '9876543210987654', pin: '654321', balance: 12000.50, dailyWithdrawalLimit: 5000.00, dailyWithdrawnAmount: 300.00, // 模拟今天已经取过300元 isLocked: false, failedAttempts: 0 }); // 也可以用一个函数来查找账户,这样逻辑更清晰 function findAccount(cardNumber) { return accountsDatabase.get(cardNumber); }这里我用了Map,因为它维护键值对的插入顺序,并且键可以是任何类型(这里用字符串卡号)。findAccount函数封装了查找逻辑,如果后续想改成从数组里查找,只需要修改这个函数内部实现,其他代码不用动,这是一种简单的封装思想。
实操心得:在模拟项目初期,用硬编码的数据快速启动是没问题的。但如果想做得更“工程化”,可以考虑将账户数据单独放在一个accounts.js模块中,或者甚至模拟一个从“文件”或“网络”读取数据的过程,这能帮你理解前端与后端数据分离的概念。
4. 核心函数实现:从登录到交易
数据准备好了,现在我们来打造ATM的“大脑”——那些处理具体业务的函数。我们将采用自顶向下的方式,先搭建主流程框架,再填充具体函数。
4.1 状态管理与主流程控制函数
我们需要一个变量来追踪整个系统的状态,以及当前登录的用户。
// 全局状态变量 let currentState = 'IDLE'; // 可能的状态:'IDLE'(空闲), 'AUTHENTICATING'(验证中), 'LOGGED_IN'(已登录), 'PROCESSING'(处理中) let currentAccount = null; // 存储当前登录成功的账户对象 // 主启动函数 function startATM() { console.log('欢迎使用模拟ATM系统!'); currentState = 'AUTHENTICATING'; authenticateUser(); } // 用户认证函数 function authenticateUser() { // 这里模拟从界面获取卡号和密码,我们用prompt简单模拟 const cardNumber = prompt('请输入您的卡号:'); if (!cardNumber) { console.log('操作已取消。'); resetATM(); return; } const account = findAccount(cardNumber); if (!account) { console.log('卡号不存在,请检查。'); setTimeout(() => authenticateUser(), 100); // 稍后重试 return; } if (account.isLocked) { console.log('账户已被锁定,请联系银行客服。'); resetATM(); return; } const pin = prompt('请输入密码:'); if (pin === account.pin) { // 登录成功! account.failedAttempts = 0; // 重置错误计数 currentAccount = account; currentState = 'LOGGED_IN'; console.log(`登录成功!欢迎用户 ${account.cardNumber.slice(-4)}`); // 显示卡号后四位 showMainMenu(); } else { // 密码错误 account.failedAttempts++; console.log(`密码错误!您还有${3 - account.failedAttempts}次机会。`); if (account.failedAttempts >= 3) { account.isLocked = true; console.log('密码错误次数过多,账户已被临时锁定。'); resetATM(); } else { setTimeout(() => authenticateUser(), 100); // 重新认证 } } } // 显示主菜单 function showMainMenu() { console.log('\n========== 主菜单 =========='); console.log('1. 查询余额'); console.log('2. 取款'); console.log('3. 存款'); console.log('4. 修改密码'); console.log('5. 转账'); console.log('0. 退卡'); const choice = prompt('请选择您要办理的业务(输入数字):'); handleMenuChoice(choice); }currentState这个状态变量非常关键。它就像一个交通信号灯,控制着程序的执行流。比如,在IDLE状态,不能直接调用showMainMenu;在PROCESSING状态(比如正在取款计数),可能不能响应其他菜单输入。我们这个简单版本没有完全严格地用状态机驱动所有分支,但引入了这个概念。更复杂的实现可以使用有限状态机模式。
4.2 取款功能的深度实现
取款是核心交易,逻辑最复杂。我们把它单独拿出来详细实现。
function handleWithdrawal() { if (currentState !== 'LOGGED_IN' || !currentAccount) { console.log('错误:请先登录。'); return; } const amountInput = prompt('请输入取款金额(需为100的整数倍):'); const amount = parseFloat(amountInput); // 输入验证 if (isNaN(amount) || amount <= 0) { console.log('输入金额无效,请重新输入。'); setTimeout(() => handleWithdrawal(), 100); return; } if (amount % 100 !== 0) { console.log('取款金额必须为100元的整数倍。'); setTimeout(() => handleWithdrawal(), 100); return; } // 业务规则验证 if (amount > currentAccount.balance) { console.log('余额不足,无法完成取款。'); showMainMenu(); return; } const remainingDailyLimit = currentAccount.dailyWithdrawalLimit - currentAccount.dailyWithdrawnAmount; if (amount > remainingDailyLimit) { console.log(`超过当日取款限额。今日剩余可取额度:${remainingDailyLimit.toFixed(2)}元`); showMainMenu(); return; } // 模拟处理中状态和点钞延迟 currentState = 'PROCESSING'; console.log('正在处理您的请求,请稍候...'); setTimeout(() => { // 执行扣款 currentAccount.balance -= amount; currentAccount.dailyWithdrawnAmount += amount; console.log(`\n[交易成功]`); console.log(`取款金额:${amount.toFixed(2)}元`); console.log(`当前余额:${currentAccount.balance.toFixed(2)}元`); console.log(`今日已取款:${currentAccount.dailyWithdrawnAmount.toFixed(2)}元`); console.log('请收好您的现金和卡片。'); // 模拟吐钞动画或提示 for (let i = 0; i < 3; i++) { setTimeout(() => console.log(`...`), i * 300); } setTimeout(() => { currentState = 'LOGGED_IN'; const continueChoice = prompt('是否继续其他业务?(y/n): '); if (continueChoice && continueChoice.toLowerCase() === 'y') { showMainMenu(); } else { logout(); } }, 1200); }, 1500); // 模拟1.5秒的处理延迟 }这个函数体现了几个重要的编程实践:
- 防御性编程:一开始就检查状态和账户是否存在。
- 输入验证:对用户输入的金额进行了是否为数字、是否为正数、是否为100倍数的多重检查。
- 业务规则验证:余额检查和限额检查是金融业务的核心,必须放在执行实际扣款操作之前。
- 用户体验模拟:使用
setTimeout模拟网络延迟或机器处理时间,让程序感觉更“真实”。最后的“模拟吐钞”小动画虽然简单,但增加了趣味性。 - 状态恢复:在处理完成后,将状态从
PROCESSING改回LOGGED_IN,并引导用户进行下一步操作。
4.3 其他交易功能的实现要点
其他功能的实现模式类似,但各有侧重点:
查询余额:最简单,直接读取currentAccount.balance并显示即可。可以美化一下输出格式。
存款:
function handleDeposit() { const amountInput = prompt('请输入存款金额:'); const amount = parseFloat(amountInput); // 验证输入有效性(>0且是数字) if (isNaN(amount) || amount <= 0) { console.log('存款金额无效。'); return; } // 模拟点钞验钞延迟 console.log('请放入钞票...'); setTimeout(() => { currentAccount.balance += amount; console.log(`存款成功!存入${amount.toFixed(2)}元,当前余额:${currentAccount.balance.toFixed(2)}元`); showMainMenu(); }, 2000); }修改密码:
function changePin() { const oldPin = prompt('请输入旧密码:'); if (oldPin !== currentAccount.pin) { console.log('旧密码输入错误。'); return; } const newPin1 = prompt('请输入新密码:'); const newPin2 = prompt('请再次输入新密码:'); if (newPin1 !== newPin2) { console.log('两次输入的新密码不一致。'); return; } if (newPin1.length < 6) { console.log('密码长度不能少于6位。'); return; } currentAccount.pin = newPin1; console.log('密码修改成功!'); // 通常修改密码后需要重新登录,这里我们直接返回菜单 showMainMenu(); }这里增加了密码长度验证,这是常见的业务规则。
转账功能(进阶): 这是最复杂的,因为它涉及两个账户的原子性操作(要么都成功,要么都失败)。在模拟环境中,我们无法实现真正的数据库事务,但可以模拟这个逻辑:
- 输入对方卡号,验证对方账户存在且非自己。
- 输入转账金额,验证自己余额是否充足。
- 执行“转账”:从自己账户扣除金额,向对方账户增加金额。
- 在模拟环境中,如果第二步之后程序出错,状态可能不一致。更严谨的做法是,在执行修改前,先检查所有条件,然后一次性完成两个账户的更新。
4.4 菜单选择处理器与退出逻辑
最后,我们需要一个函数来分发主菜单的选择:
function handleMenuChoice(choice) { switch (choice) { case '1': checkBalance(); break; case '2': handleWithdrawal(); break; case '3': handleDeposit(); break; case '4': changePin(); break; case '5': handleTransfer(); break; case '0': logout(); break; default: console.log('无效的选择,请重新输入。'); setTimeout(showMainMenu, 100); } } function checkBalance() { console.log(`\n您的当前余额为:${currentAccount.balance.toFixed(2)}元`); setTimeout(showMainMenu, 500); } function logout() { console.log('\n感谢您的使用,请收好您的卡片。再见!'); currentAccount = null; currentState = 'IDLE'; // 在实际Web应用中,这里可能会跳转到登录页或首页 // 在控制台模拟中,可以重新启动或结束 setTimeout(startATM, 2000); // 2秒后重启ATM,模拟下一个用户 } function resetATM() { currentAccount = null; currentState = 'IDLE'; setTimeout(startATM, 1000); }switch...case是处理这种菜单选择的经典结构,清晰易懂。logout函数负责清理会话状态(将currentAccount置为null),这是非常重要的,否则下一个用户可能会直接访问上一个用户的账户。
5. 从控制台到网页:构建交互界面
到目前为止,我们一直用console.log和prompt在浏览器控制台里交互。这适合测试逻辑,但用户体验很差。让我们把它升级成一个简单的网页应用,这样更有“项目”的感觉。
5.1 设计简单的HTML界面
创建一个index.html文件:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>模拟ATM取款机</title> <style> body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; } #atm-output { background-color: #f4f4f4; border: 1px solid #ddd; min-height: 200px; padding: 15px; margin-bottom: 20px; border-radius: 5px; white-space: pre-wrap; /* 保留输出格式和换行 */ overflow-y: auto; max-height: 300px; } .input-area { margin-bottom: 15px; } input[type="text"], input[type="password"] { padding: 10px; width: 250px; margin-right: 10px; } button { padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:hover { background-color: #0056b3; } .menu button { margin: 5px; } </style> </head> <body> <h1>模拟ATM取款机系统</h1> <div id="atm-output">欢迎!请插入您的卡片(输入卡号开始)。</div> <div class="input-area" id="auth-section"> <input type="text" id="cardNumberInput" placeholder="请输入卡号"> <input type="password" id="pinInput" placeholder="请输入密码" style="display:none;"> <button id="submitBtn">确认</button> <button id="cancelBtn">取消/退卡</button> </div> <div class="menu" id="menu-section" style="display:none;"> <p>请选择业务:</p> <button>// atm.js const outputDiv = document.getElementById('atm-output'); const authSection = document.getElementById('auth-section'); const menuSection = document.getElementById('menu-section'); const amountSection = document.getElementById('amount-section'); const cardNumberInput = document.getElementById('cardNumberInput'); const pinInput = document.getElementById('pinInput'); const submitBtn = document.getElementById('submitBtn'); // ... 获取其他DOM元素 // 账户数据库(同上,略) const accountsDatabase = new Map(); // ... 初始化账户 let currentState = 'IDLE'; let currentAccount = null; let pendingAction = null; // 用于记录当前等待金额输入的操作,如 'WITHDRAW', 'DEPOSIT' // 输出函数,替代 console.log function atmOutput(message) { outputDiv.textContent += message + '\n'; outputDiv.scrollTop = outputDiv.scrollHeight; // 自动滚动到底部 } // 清除输出 function clearOutput() { outputDiv.textContent = ''; } // 初始化:显示登录界面 function initATM() { clearOutput(); atmOutput('欢迎使用模拟ATM系统!'); atmOutput('请输入卡号开始。'); authSection.style.display = 'block'; menuSection.style.display = 'none'; amountSection.style.display = 'none'; pinInput.style.display = 'none'; cardNumberInput.focus(); currentState = 'AUTHENTICATING'; } // 提交按钮事件处理 submitBtn.addEventListener('click', handleAuthSubmit); function handleAuthSubmit() { if (currentState === 'AUTHENTICATING') { const cardNumber = cardNumberInput.value.trim(); if (!cardNumber) { atmOutput('卡号不能为空。'); return; } const account = findAccount(cardNumber); if (!account) { atmOutput('卡号不存在。'); cardNumberInput.value = ''; cardNumberInput.focus(); return; } if (account.isLocked) { atmOutput('账户已被锁定,请联系银行。'); resetATM(); return; } // 卡号有效,进入密码输入阶段 currentAccount = account; // 临时保存,等待密码验证 atmOutput(`卡号 ${cardNumber.slice(-4)} 识别成功,请输入密码。`); cardNumberInput.style.display = 'none'; pinInput.style.display = 'inline'; pinInput.value = ''; pinInput.focus(); // 状态可以更细化,这里我们简化处理 } else if (currentState === 'LOGGED_IN' && pendingAction) { // 处理金额输入提交(例如取款、存款) handleAmountSubmit(); } } // 密码输入框的回车键支持 pinInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { handlePinSubmit(); } }); function handlePinSubmit() { const pin = pinInput.value; if (pin === currentAccount.pin) { currentAccount.failedAttempts = 0; atmOutput('登录成功!'); showMainMenu(); } else { currentAccount.failedAttempts++; atmOutput(`密码错误!剩余尝试次数:${3 - currentAccount.failedAttempts}`); pinInput.value = ''; pinInput.focus(); if (currentAccount.failedAttempts >= 3) { currentAccount.isLocked = true; atmOutput('错误次数过多,账户已锁定。'); resetATM(); } } } function showMainMenu() { currentState = 'LOGGED_IN'; pendingAction = null; authSection.style.display = 'none'; amountSection.style.display = 'none'; menuSection.style.display = 'block'; clearOutput(); atmOutput('========== 主菜单 =========='); atmOutput('1. 查询余额'); atmOutput('2. 取款'); atmOutput('3. 存款'); atmOutput('4. 修改密码'); atmOutput('5. 转账'); atmOutput('0. 退卡'); atmOutput('请点击上方按钮选择业务。'); } // 为菜单按钮绑定事件 document.querySelectorAll('#menu-section button[data-action]').forEach(button => { button.addEventListener('click', function() { const action = this.getAttribute('data-action'); switch (action) { case 'balance': checkBalance(); break; case 'withdraw': startWithdrawal(); break; case 'deposit': startDeposit(); break; case 'changePin': changePin(); break; case 'transfer': handleTransfer(); break; case 'logout': logout(); break; } }); }); function startWithdrawal() { pendingAction = 'WITHDRAW'; clearOutput(); atmOutput('请输入取款金额(需为100的整数倍):'); menuSection.style.display = 'none'; amountSection.style.display = 'block'; document.getElementById('amountInput').focus(); } // handleAmountSubmit 函数需要根据 pendingAction 来判断执行取款还是存款逻辑 function handleAmountSubmit() { const amountInput = document.getElementById('amountInput'); const amount = parseFloat(amountInput.value); if (isNaN(amount) || amount <= 0) { atmOutput('输入金额无效。'); amountInput.value = ''; amountInput.focus(); return; } if (pendingAction === 'WITHDRAW') { // 调用之前写的取款逻辑,但需要适配新的输出和界面 performWithdrawal(amount); } else if (pendingAction === 'DEPOSIT') { performDeposit(amount); } // 处理完后隐藏金额输入区域 amountSection.style.display = 'none'; amountInput.value = ''; } function performWithdrawal(amount) { // 这里整合之前 handleWithdrawal 函数的核心逻辑,但输出用 atmOutput if (amount % 100 !== 0) { atmOutput('取款金额必须为100元的整数倍。'); showMainMenu(); return; } if (amount > currentAccount.balance) { atmOutput('余额不足。'); showMainMenu(); return; } const remainingDailyLimit = currentAccount.dailyWithdrawalLimit - currentAccount.dailyWithdrawnAmount; if (amount > remainingDailyLimit) { atmOutput(`超过当日取款限额。今日剩余可取额度:${remainingDailyLimit.toFixed(2)}元`); showMainMenu(); return; } atmOutput('正在处理,请稍候...'); setTimeout(() => { currentAccount.balance -= amount; currentAccount.dailyWithdrawnAmount += amount; atmOutput(`\n[交易成功]`); atmOutput(`取款金额:${amount.toFixed(2)}元`); atmOutput(`当前余额:${currentAccount.balance.toFixed(2)}元`); atmOutput(`今日已取款:${currentAccount.dailyWithdrawnAmount.toFixed(2)}元`); atmOutput('请收好您的现金。'); // 询问是否继续 setTimeout(() => { // 这里可以弹出一个确认框,或者简单地返回菜单 showMainMenu(); }, 1500); }, 1500); } // 其他函数如 checkBalance, performDeposit, logout 等也需要用 atmOutput 和 DOM 操作重构 // ... // 页面加载完成后初始化ATM window.onload = initATM;这个重构过程将我们的纯逻辑代码与网页界面绑定了起来。关键点在于:
- 状态驱动UI:通过控制
display: none/block来显示和隐藏不同的界面区块(认证区、菜单区、金额输入区)。 - 事件监听:使用
addEventListener来响应用户的点击和键盘输入。 - 数据流:用户输入从
input元素中获取,输出显示在div中,完全替代了控制台的prompt和log。
6. 常见问题、调试技巧与项目扩展
即使是一个模拟项目,在开发过程中也会遇到各种问题。这里分享一些我踩过的坑和调试心得。
6.1 典型问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 点击按钮没反应 | 1. JavaScript 代码有语法错误,导致后续脚本未执行。 2. 事件监听器绑定时,DOM元素还未加载完成。 3. 函数名拼写错误或函数未定义。 | 1. 打开浏览器开发者工具(F12),查看“控制台(Console)”面板是否有红色报错信息。 2. 将初始化代码(如 initATM())放在window.onload事件中,或使用DOMContentLoaded事件。3. 仔细检查函数名,确保调用和定义一致。 |
| 账户状态混乱,A用户操作影响了B用户的数据 | 全局变量currentAccount在用户退卡后没有正确清空,或者在某个错误分支中未重置。 | 1. 确保在logout()和resetATM()函数中,将currentAccount设为null。2. 在每个需要登录状态的函数开头,都检查 if (!currentAccount) { ... return; }。3. 仔细梳理所有可能改变状态的代码路径。 |
| 取款后余额显示为很长的小数(如 4899.999999999999) | JavaScript 浮点数精度问题。计算机用二进制表示小数,有些十进制小数无法精确表示。 | 1. 在显示时使用toFixed(2)固定两位小数。2. 在内部计算时,可以考虑以“分”为单位存储金额(整数),避免浮点运算。例如,存储 500000代表5000.00元。 |
| 页面刷新后所有账户数据恢复初始状态 | 数据存储在内存变量中,页面刷新即丢失。 | 这是预期行为。如果想持久化,可以引入localStorage或sessionStorage来在浏览器端简单存储。注意:这仍然不安全,仅用于演示。真实应用数据必须保存在服务器。 |
| 输入金额时,用户输入了非数字字符导致程序崩溃 | parseFloat对非数字输入返回NaN,后续与数字比较时可能出错。 | 在调用parseFloat后,必须用isNaN()函数检查结果是否为有效数字,并进行处理。 |
实操心得:调试JavaScript,尤其是DOM操作,最强大的工具就是浏览器的开发者工具。多用console.log在关键节点输出变量值(在重构为网页版后,可以输出到控制台辅助调试),多用“元素(Elements)”面板检查DOM结构是否正确,多用“源代码(Sources)”面板设置断点进行单步调试。
6.2 项目扩展方向
这个基础版本已经实现了核心功能,但还有很多可以深化和扩展的地方,能让你的项目脱颖而出:
- 引入
localStorage持久化:将accountsDatabase保存到localStorage中,这样页面刷新后数据不会丢失。每次启动时从localStorage读取,每次更新账户后写回。这能让你练习JSON的序列化(JSON.stringify)与反序列化(JSON.parse)。 - 模拟网络延迟与加载状态:用
Promise和async/await模拟所有操作都有网络请求延迟。在请求发出时,在界面上显示“加载中...”的提示,增强真实感。 - 实现完整的转账功能:这是很好的综合练习。需要输入对方账号、姓名(可做简单验证)、金额,并同时更新两个账户。要特别注意并发问题(虽然模拟环境下很少发生),思考如果扣款成功但存款失败该怎么办(事务回滚的思想)。
- 添加交易记录:为每个账户增加一个
transactions数组属性,记录每笔交易的时间、类型、金额、对方账号等。并提供一个“查询交易明细”的菜单选项。 - 美化UI:使用CSS框架(如Bootstrap)或自己编写更精美的样式,让界面看起来更像一个真正的ATM触摸屏。
- 模块化重构:将代码拆分成多个模块文件,例如
account.js(账户数据与操作)、ui.js(界面控制)、main.js(主逻辑)。使用ES6模块的import/export来组织代码,这更接近现代前端工程的实践。
我个人在实现这个项目时,最大的体会是:边界条件处理是区分玩具项目和严谨项目的关键。用户可能会输入负数、输入字母、在密码框里粘贴一大段文字、连续快速点击按钮……你的代码能优雅地处理这些异常吗?多思考“如果……会怎样”,并为之编写防御性代码,你的编程能力会提升一个档次。
这个模拟ATM机的项目,就像是一个微型的“状态机”和“业务逻辑”的练习场。它不涉及复杂的算法,但对培养清晰的逻辑思维、严谨的代码风格和对用户体验的考量,非常有帮助。希望你能在实现它的过程中,享受到用代码构建一个完整系统的乐趣。