1.2 客户端JavaScript
JavaScript语言核心部分的内容中的知识点交叉引用比较多,且知识点的层次感并不分明。而在客户端JavaScript部分的内容编排方式有了较大改变。依照本书给定的知识点顺序进行学习,完全可以学会如何在Web浏览器中使用JavaScript。但如果你想通过阅读本书来学习客户端JavaScript的话,不能只将眼光落在第二部分,所以本节会对于客户端编程技术做一个快速概览,随后会给出一个有深度的示例。
第13章是第二部分的第一章,该章介绍如何让JavaScript在Web浏览器中运行起来。从该章学到的最重要的内容是,JavaScript代码可以通过<scirpt>标签来嵌入到HTML文件中:
<html>
<head>
<script src="library.js"></script><!—引入一个JavaScript库—>
</head>
<body>
<p>This is a paragraph of HTML</p>
<script>//在这里编写嵌入到HTML文件中的JavaScript代码
</script>
<p>Here is more HTML</P>
</body>
</html>
第14章讲解Web浏览器端脚本技术,并涵盖客户端JavaScript中的一些重要全局函数,例如:
<script>
function moveon(){//通过弹出一个对话框来询问用户一个问题
var answer=confirm("准备好了吗?");//单击"确定"按钮,浏览器会加载一个新页面
if(answer)window.location="http://taobao.com";
}
//在1分钟(6万毫秒)后执行定义的这个函数
setTimeout(moveon,60000);
</script>
我们注意到,本节展示的客户端示例代码要比前面的示例代码要长很多。这里的示例代码并不是用来在Firebug(或者其他调试工具)控制台窗口中直接输入的,而是作为一个单独的HTML文件,并在Web浏览器中直接打开运行的。比如,上述代码段就是一个HTML文件的完整内容。
第15章的内容更加务实——通过脚本来操纵HTML文档内容。它将展示如何选取特定的HTML元素、如何给HTML元素设置属性、如何修改元素内容,以及如何给文档添加新节点。这里的示例函数展示了如何查找和修改基本文档的内容:
//在document中的一个指定的区域输出调试消息
//如果document不存在这样一个区域,则创建一个
function debug(msg){//通过查看HTML元素id属性来查找文档的调试部分
var log=document.getElementById("debuglog");//如果这个元素不存在,则创建一个
if(!log){
log=document.createElement("div");//创建一个新的<div>元素
log.id="debuglog";//给这个元素的HTML id赋值
log.innerHTML="<h1>Debug Log</h1>";//定义初始内容
document.body.appendChild(log);//将其添加到文档的末尾
}
//将消息包装在<pre>中,并添加至log中
var pre=document.createElement("pre");//创建<pre>标签
var text=document.createTextNode(msg);//将msg包装在一个文本节点中
pre.appendChild(text);//将文本添加至<pre>
log.appendChild(pre);//将<pre>添加至log
}
第15章讲述JavaScript如何操纵HTML中定义Web内容的元素。第16章讲述如何使用JavaScript来进行CSS样式操作,CSS样式定义了内容的展示方式。这通常会使用到HTML元素的style和class属性:
function hide(e,reflow){//通过JavaScript操纵样式来隐藏元素e
if(reflow){//如果第二个参数是true
e.style.display="none"//隐藏这个元素,其所占的空间也随之消失
}
else{//否则
e.style.visibility="hidden";//将e隐藏,但是保留其所占的空间
}
}
function highlight(e){//通过设置CSS类来高亮显示e
//简单地定义或追加HTML类属性
//这里假设CSS样式表中已经有"hilite"类的定义
if(!e.className)e.className="hilite";
else e.className+="hilite";
}
可以通过JavaScript来操控Web浏览器中的HTML内容和文档的CSS样式,同样,也可以通过事件处理程序(event handler)来定义文档的行为。事件处理程序是一个在浏览器中注册的JavaScript函数,当特定类型的事件发生时浏览器便调用这个函数。通常我们关心的事件类型是鼠标点击事件和键盘按键事件(在智能手机中则是各种触碰事件)。或者说,当浏览器完成了文档的加载,当用户改变窗口大小或当用户向HTML表单元素中输入数据时便会触发一个事件。第17章详细描述如何定义、注册事件处理程序,以及在事件发生时浏览器是如何调用它们的。
定义事件处理程序最简单的方法是,给HTML的以"on"为前缀的属性绑定一个回调。当写一些简单的测试程序时,最实用的方法就是给"onclick"处理程序绑定回调。假定已经将上文中的debug()和hide()两个函数保存至名为debug.js和hide.js的文件中,那么就可以写一个简单的HTML测试文件,来给<button>元素的onclick属性指定一个事件处理程序:
<script src="debug.js"></script>
<script src="hide.js"></script>
Hello
<button onclick="hide(this,true);debug('hide button 1');">Hide1</button>
<button onclick="hide(this);debug('hide button 2');">Hide2</button>
World
下面这些客户端JavaScript代码用到了事件,它给一个很重要的事件——"load"事件注册了一个事件处理程序。同时,也展示了注册"click"事件处理函数更高级的一种方法:
//"load"事件只有在文档加载完成后才会触发
//通常需要等待load事件发生后才开始执行JavaScript代码
window.onload=function(){//当文档加载完成时执行这里的代码
//找到文档中所有的<img>标签
var images=document.getElementsByTagName("img");//遍历images,给每个节点的"click"事件添加事件处理程序
//在点击图片的时候将图片隐藏
for(var i=0;i<images.length;i++){
var image=images[i];
if(image.addEventListener)//注册事件处理程序的另一种方法
image.addEventListener("click",hide,false);
else//兼容IE8及以前的版本
image.attachEvent("onclick",hide);
}
//这便是上面注册的事件处理函数
function hide(event){event.target.style.visibility="hidden";}
};
第15~17章讲述了如何使用JavaScript来操控网页的内容(HTML)、样式(CSS)以及行为(事件处理)。这些章所讨论的API多少有些复杂,且至今仍具有糟糕的浏览器兼容性。也正是由于这个原因,很多客户端JavaScript程序员选择使用“库”或“框架”来简化他们的编码工作。最流行的库非jQuery莫属。第19章将会详细介绍jQuery库。jQuery定义了一套灵巧易用的API,用来操控文档内容、样式和行为。jQuery经过了完整的测试,在所有现代主流浏览器,甚至在IE6这种早期浏览器中都可以照常运行。
jQuery代码非常易于识别,因为它充分利用了一个名为$()的函数。这里用jQuery重写了上文中提到的debug()函数:
function debug(msg){
var log=$("#debuglog");//找到要显示msg的元素.
if(log.length==0){//如果不存在则创建之
log=$("<div id='debuglog'><h1>Debug Log</h1></div>");
log.appendTo(document.body);//并将其追加到body里
}
log.append($("<pre/>").text(msg));//将msg包装在<pre>中,再追加到log里
}
目前我们所提到的第二部分的4章都是围绕网页展开讨论的。后续的4章将着眼点转向Web应用。这几章的内容并不是讨论如何通过编写操控内容、样式和行为的脚本使用Web浏览器来渲染文档;而是讲解如何将Web浏览器当做应用平台,并描述了用以支持更复杂精细的客户端Web应用的现代浏览器API。第18章讲解如何使用JavaScript来发起HTTP请求。第20章描述数据存储的机制以及客户端应用中的会话状态的保持。第21章涵盖基于HTML的<vanvas>标签的客户端API,用来进行任意形状图形的绘制。最后,第22章讲解HTML5所提供的新一代Web应用API。网络、存储、图形:这些都是Web浏览器提供的操作系统级的服务,它们定义了全新的跨平台的应用环境。如果你正在进行基于那些支持这些新API的浏览器的开发,这将是你作为客户端JavaScript程序员最激动人心的时刻。最后4章并没有太多示例代码,但下面的例子使用了这些新的API。
示例:一个JavaScript贷款计算器
本章最后展示一个例子,这个例子集中使用了诸多技术,展示了真实环境下的客户端JavaScript(包括HTML和CSS)编程。例1-1给出了一个简单的贷款计算器应用的代码,如图1-2所示。
图 1-2 一个贷款计算器Web应用
在看代码(例1-1)之前应当先仔细阅读本段文字。你不需要理解所有内容,代码中有着完整的注释,至少你应该能正确运行这段代码得到如图1-2所示的界面。这里的例子展示了诸多JavaScript语言核心特性,同样展示了重要的客户端JavaScript技术:
·如何在文档中查找元素
·如何通过表单input元素来获取用户的输入数据
·如何通过文档元素来设置HTML内容
·如何将数据存储在浏览器中
·如何使用脚本发起HTTP请求
·如何利用<canvas>元素绘图
例1-1:基于JavaScript实现的贷款计算器
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Loan Calculator</title>
<style>/*这是一个CSS样式表:定义了程序输出的样式*/
.output{font-weight:bold;}/*计算结果定义为粗体*/
#payment{text-decoration:underline;}/*定义id="payment"的元素样式*/
#graph{border:solid black 1px;}/*图表有一个1像素的边框*/
th,td{vertical-align:top;}/*表格单元格对其方式为顶端对齐*/
</style>
</head>
<body>
<!--
这是一个HTML表格,其中包含<input>元素可以用来输入数据。
程序将在<span>元素中显示计算结果,这些元素都具有类似"interset"和"years"的id
这些id将在表格下面的JavaScript代码中用到。我们注意到,有一些
input元素定义了"onchange"或"onclick"的事件处理程序,以便用户在输入数据或者点击inputs时
执行指定的JavaScript代码段
-->
<table>
<tr><th>Enter Loan Data:</th>
<td></td>
<th>Loan Balance,Cumulative Equity,and Interest Payments</th></tr>
<tr><td>Amount of the loan($):</td>
<td><input id="amount"onchange="calculate();"></td>
<td rowspan=8>
<canvas id="graph"width="400"height="250"></canvas></td></tr>
<tr><td>Annual interest(%):</td>
<td><input id="apr"onchange="calculate();"></td></tr>
<tr><td>Repayment period(years):</td>
<td><input id="years"onchange="calculate();"></td>
<tr><td>Zipcode(to find lenders):</td>
<td><input id="zipcode"onchange="calculate();"></td>
<tr><th>Approximate Payments:</th>
<td><button onclick="calculate();">Calculate</button></td></tr>
<tr><td>Monthly payment:</td>
<td>$<span class="output"id="payment"></span></td></tr>
<tr><td>Total payment:</td>
<td>$<span class="output"id="total"></span></td></tr>
<tr><td>Total interest:</td>
<td>$<span class="output"id="totalinterest"></span></td></tr>
<tr><th>Sponsors:</th><td colspan=2>
Apply for your loan with one of these fine lenders:
<div id="lenders"></div></td></tr>
</table>
<!--随后是JavaScirpt代码,这些代码内嵌在了一个<script>标签里-->
<!--通常情况下,这些脚本代码应当放在<head>标签中-->
<!--将JavaScript代码放在HTML代码之后仅仅是为了便于理解-->
<script>
"use strict";//如果浏览器支持的话,则开启ECMAScript 5的严格模式/*
*这里的脚本定义了caculate()函数,在HTML代码中绑定事件处理程序时会调用它
*这个函数从<input>元素中读取数据,计算贷款赔付信息,并将结果显示在<span>元素中
*同样,这里还保存了用户数据、展示了放贷人链接并绘制出了图表
*/
function calculate(){//查找文档中用于输入输出的元素
var amount=document.getElementById("amount");
var apr=document.getElementById("apr");
var years=document.getElementById("years");
var zipcode=document.getElementById("zipcode");
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);//Math.pow()进行幂次运算
var monthly=(principal*x*interest)/(x-1);//如果结果没有超过JavaScript能表示的数字范围,且用户的输入也正确
//这里所展示的结果就是合法的
if(isFinite(monthly)){//将数据填充至输出字段的位置,四舍五入到小数点后两位数字
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();//不传参数的话就是清除图表
}
}
//将用户的输入保存至localStorage对象的属性中
//这些属性在再次访问时还会继续保持在原位置
//如果你在浏览器中按照file://URL的方式直接打开本地文件,
//则无法在某些浏览器中使用存储功能(比如FireFox)
//而通过HTTP打开文件是可行的
function save(amount,apr,years,zipcode){
if(window.localStorage){//只有在浏览器支持的时候才运行这里的代码
localStorage.loan_amount=amount;
localStorage.loan_apr=apr;
localStorage.loan_years=years;
localStorage.loan_zipcode=zipcode;
}
}
//在文档首次加载时,将会尝试还原输入字段
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;
}
};//将用户的输入发送至服务器端脚本(理论上)将
//返回一个本地放贷人的链接列表,在这个例子中并没有实现这种查找放贷人的服务
//但如果该服务存在,该函数会使用它
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);//不带任何正文发送这个请求
//在返回数据之前,注册了一个事件处理函数,这个处理函数
//将会在服务器的响应返回至客户端的时候调用
//这种异步编程模型在客户端JavaScript中是非常常见的
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+="<li><a href='"+lenders[i].url+"'>"+
lenders[i].name+"</a>";
}
//将数据在HTML元素中呈现出来
ad.innerHTML="<ul>"+list+"</ul>";
}
}
}
//在HTML<canvas>元素中用图表展示月度贷款余额、利息和资产收益
//如果不传入参数的话,则清空之前的图表数据
function chart(principal,interest,monthly,payments){
var graph=document.getElementById("graph");//得到<canvas>标签
graph.width=graph.width;//用一种巧妙的手法清除并重置画布
//如果不传入参数,或者浏览器不支持画布,则直接返回
if(arguments.length==0||!graph.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);
}
}
</script>
</body>
</html>