在JavaScript权威指南一书中,有贷款计算器一例。
书中有很多低级错误(我应该是盗版…),经过一些修改,它成功完成了贷款计算,我把源码放到这里,记录一下。
<html>
<head>
<meta charset="UTF-8">
<title>Javascript Loan Calculatortitle>
<style>
*{padding:0;margin:0;}
.output{font-weight:bold;}
#payment{text-decoration:underline;}
#graph{border:solid black 1px;}
th,td{vertical-align:top;}
style>
head>
<body>
<script src="js.js">script>
<table>
<tr>
<th>输入贷款数据:th>
<td>td>
<th>贷款余额,累积权益和利息支付th>
tr>
<tr>
<td>贷款金额($):td>
<td><input id="amount" onchange="calculator();">td>
<td rowspan=8>
<canvas id="graph" width="400" height="250">canvas>
td>
tr>
<tr>
<td>年利率(%):td>
<td><input id="apr" onchange="calculator();">td>
tr>
<tr>
<td>还款期(年):td>
<td><input id="years" onchange="calculator();">td>
<tr>
<td>Zipcode(寻找贷方):td>
<td><input id="zipcode" onchange="calculator();">td>
<tr>
<th>近似付款:th>
<td>
// 2019.6.19 Javascript Loan Calculator
// 如果浏览器支持则开启ECMAScript 5(ES5)严格模式
"use strict";
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/* 【函数功能】
A. 封装calculate()函数,在html代码中绑定事件处理程序时会调用它
B. 此函数从读取数据,计算贷款赔付信息,并将结果显示在
C. 它还保存了用户数据、展示了放贷人链接并绘制出了图标
*/
function calculator(){
// 查找文档中用于输入输出的元素
// 这些amount|apr|变量都是局部的,函数运行结束后立刻销毁
// amount|apr|years|zipcode => input
var amount = document.getElementById("amount");
var apr = document.getElementById("apr");
var years = document.getElementById("years");
var zipcode = document.getElementById("zipcode");
// payment|total|totalinterest => output
var payment = document.getElementById("payment");
var total = document.getElementById("total");
var totalinterest = document.getElementById("totalinterest");
// 假设所有输入都是合法的,将从input元素中获取输入数据
// 将百分比格式转换为小数格式,并从年利率转换为月利率
// 将年度赔付转换为月度赔付
var principal = parseFloat(amount.value);
var interest = parseFloat(apr.value)/100/12;
var payments = parseFloat(years.value)*12;
// 计算月度赔付的数据
var x = Math.pow(1+interest,payments); //进行幂次运算
var monthly = (principal * x * interest) / (x-1);
// 如果结果没有超过JavaScript能表示的数字范围,且用户的输入也正正确
// 这里所展示的结果就是合法有效的
if(isFinite(monthly)){ // isFinite检测monthly是否无穷大
// 将数据填充到并四舍五入到小数点后2位
payment.innerHTML = monthly.toFixed(2);
total.innerHTML = (monthly*payments).toFixed(2);
totalinterest.innerHTML = ((monthly*payments)-principal).toFixed(2);
// 将用户输入的数据保存下来(方便下次也能取到历史数据)
save(amount.value,apr.value,years.value,zipcode.value);
// 找到并展示本地放贷人(忽略网络错误)
try{ // 捕获这段代码抛出的所有异常
getLenders(amount.value,apr.value,years.value,zipcode.value);
}catch(e){
/* 忽略抛出这些异常 */
}
// 图表展示贷款余额、利息、资产收益
chart(principal,interest,monthly,payments);
}else{
// 计算结果不是数字或是无穷大(表示数据是非法或不完整)
// 清空之前的输出数据
payment.innerHTML = "";
total.innerHTML = "";
totalinterest.innerHTML = "";
chart(); // 如不传参则清空图表
}
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/* 【函数功能】
A. 将用户的输入保存至localStorage对象属性中
B. 这些属性在再次访问时还会继续保持在原位置
C. 如果在浏览器中使用file://URL方式打开则无法使用浏览器存储功能(fireFox)
D. 通过HTPP是可以实现的
*/
function save(amount,apr,years,zipcode){
// 只有浏览器支持时才执行(window.localStorage判断)
if(window.localStorage){
localStorage.loan_amount = amount;
localStorage.loan_apr = apr;
localStorage.loan_years = years;
localStorage.loan_zipcode = zipcode;
}
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/* 【全局onload函数】
A. 在文档首次加载时尝试还原输入字段
*/
window.onload = function(){
// 如果浏览器支持本地存储并且上次保存的值是存在的
if(window.localStorage && localStorage.loan_amount){
document.getElementById("amount").value = localStorage.loan_amount;
document.getElementById("apr").value = localStorage.loan_apr;
document.getElementById("years").value = localStorage.loan_years;
document.getElementById("zipcode").value = localStorage.loan_zipcode;
}
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/* 【函数功能】
A. 将用户的输入发送至服务器端脚本(理论上)
B. 将返回一个本地放贷人的链接列表(未实现)
C. 但如果该服务存在 则getLenders()有效
*/
function getLenders(amount,apr,years,zipcode){
// 如果浏览器不支持XMLHttpRequest对象(则退出)
if(!window.XMLHttpRequest) return;
// 找到要显示的放贷人列表的元素
var ad = document.getElementById("lenders");
if(!ad) return; // 没有则退出
// 将用户的输入数据进行URL编码 并作为查询参数附加在URL里
var url = "getLenders.php"+ // 处理数据的URL地址
"?amt=" + encodeURIComponent(amount)+ // 使用查询串中的数据
"&apr=" + encodeURIComponent(apr)+
"&yrs=" + encodeURIComponent(years)+
"&zip=" + encodeURIComponent(zipcode);
// 通过XMLHttpRequest对象来提取返回数据
var req = new XMLHttpRequest(); // 发起一个新的请求
req.open("GET",url); // 通过URL发起一个HTTP GET请求
req.send(null); // 不带任何正文发送这个请求
// 在返回数据之前 注册了一个事件处理函数
// 这个事件处理函数将会在服务器的响应返回至客户端的时候调用
req.onreadystatechange = function(){
if(req.readyState == 4 && req.status == 200){
// 如果代码运行到这里 说明得到了一个合法且完整的HTTP响应
var response = req.responseText; // HTTP响应是以字符串的形式呈现的
var lenders = JSON.parse(response); // 将其解析为JS数组
// 将数组中的放贷人对象转换为HTML字符串形式
var list = "";
for(var i=0;i<lenders.length;i++){
list += "" +lenders[i].name+"";
}
// 将数据在HTML元素中呈现出来
ad.innerHTML = ""
+ list + "";
}
}
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/* 【函数功能】
A. 在HTML
function chart(principal,interest,monthly,payments){
var graph = document.getElementById("graph"); // 得到
g.clearRect(0, 0, width, height); // 清除并重置画布
// 如果不传入参数|或者浏览器不支持画布则直接返回
if(arguments.length == 0 || lgraph.getContext) return;
// 获得画布元素的"context"对象 这个对象定义了一组绘画API
var g = graph.getContext("2d"); // 所有绘画操作都基于这个对象
var width = graph.width;
height = graph.height; // 获得画布的大小
// 这里的函数作用是将付款数字和美元数据转换为像素
function paymentToX(n){
return n * width / payments;
}
function amountToY(a){
return height - (a*height/(monthly*payments*1.05));
}
// 付款数据是一条从(0,0)到(payments,monthly*payments)的直线
g.moveTo(paymentToX(0),amountToY(0)); //从左下方开始
g.lineTo(paymentToX(payments),amountToY(monthly*payments)); //绘制右上方
g.lineTo(paymentToX(payments),amountToY(0)); //再至右下方
g.closePath(); //将结尾连接至开头
g.fillStyle = "#f88"; //亮红色
g.fill(); //填充矩形
g.font = "bold 12px sans-serif"; //定义一种字体
g.fillText("Total Interest Payments",20,20); //将文字绘制到图例中
// 很多资产数据并不是线性 很难反映到图表中
var equity = 0;
g.beginPath(); //开始绘制新图形
g.moveTo(paymentToX(0),amountToY(0)); //从左下方开始
for(var p=1;p<=payments;p++){
// 计算每一笔赔付利息
var thisMonthsInterest = (principal - equity) * interest;
equity += (monthly - thisMonthsInterest); //得到资产额
g.lineTo(paymentToX(p),amountToY(equity)); //将数据绘制到画布中
}
g.lineTo(paymentToX(payments),amountToY(0)); //将数据线绘制至X轴
g.closePath(); //将线条结尾连接至线条开头
g.fillStyle = "green"; //使用绿色绘制图形
g.fill(); //曲线之下的部分均填充
g.fillText("Total Equity",20,35); //文本颜色设置为绿色
// 再次循环 余额数据显示为黑色粗线条
var bal = principal;
g.beginPath();
g.moveTo(paymentToX(0),amountToY(bal));
for(var p=1;p<=payments;p++){
var thisMonthsInterest = bal * interest;
bal -= (monthly - thisMonthsInterest); //得到资产额
g.lineTo(paymentToX(p),amountToY(bal)); //将直线连接至某点
}
g.lineWidth = 3; //将直线宽度加粗
g.stroke(); //绘制余额的曲线
g.fillStyle = "black"; //使用黑色字体
g.fillText("Loan Balance",20,50); //图例文字
// 将年度数据在X轴做标记
g.textAlign = "center"; //文字居中对齐
var y = amountToY(0); //Y坐标设为0
for(var year=1;year*12 <= payments;year++){ //遍历每年
var x = paymentToX(year * 12); //计算标记位置
g.fillRect(x-0.5,y-3,1,3); //开始绘制标记
if(year == 1) g.fillText("Year",x,y-5); //在坐标轴做标记
if(year % 5 == 0 && year *12 !== payments) //每5年的数据
g.fillText(String(year),x,y-5);
}
// 将赔付数额标记在右边界
g.textAlign = "right"; //文字右对齐
g.textBaseline = "middle"; //文字垂直居中
var ticks = [monthly*payments,principal]; //将用到的两个点
var rightEdge = paymentToX(payments); //设置X坐标
for(var i=0;i<ticks.length;i++){ //对每两个点做循环
var y = amountToY(ticks[i]); //计算每个标记的Y坐标
g.fillRect(rightEdge-3,y-0.5,3,1); //绘制标记
g.fillText(String(ticks[i].toFixed(0)),rightEdge-5,y);
}
}
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++