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 客户端JavaScript - 图1

图 1-2 一个贷款计算器Web应用

在看代码(例1-1)之前应当先仔细阅读本段文字。你不需要理解所有内容,代码中有着完整的注释,至少你应该能正确运行这段代码得到如图1-2所示的界面。这里的例子展示了诸多JavaScript语言核心特性,同样展示了重要的客户端JavaScript技术:

·如何在文档中查找元素

·如何通过表单input元素来获取用户的输入数据

·如何通过文档元素来设置HTML内容

·如何将数据存储在浏览器中

·如何使用脚本发起HTTP请求

·如何利用<canvas>元素绘图

例1-1:基于JavaScript实现的贷款计算器

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>JavaScript Loan Calculator</title>
  5. <style>/*这是一个CSS样式表:定义了程序输出的样式*/
  6. .output{font-weight:bold;}/*计算结果定义为粗体*/
  7. #payment{text-decoration:underline;}/*定义id="payment"的元素样式*/
  8. #graph{border:solid black 1px;}/*图表有一个1像素的边框*/
  9. th,td{vertical-align:top;}/*表格单元格对其方式为顶端对齐*/
  10. </style>
  11. </head>
  12. <body>
  13. <!--
  14. 这是一个HTML表格,其中包含<input>元素可以用来输入数据。
  15. 程序将在<span>元素中显示计算结果,这些元素都具有类似"interset"和"years"的id
  16. 这些id将在表格下面的JavaScript代码中用到。我们注意到,有一些
  17. input元素定义了"onchange"或"onclick"的事件处理程序,以便用户在输入数据或者点击inputs时
  18. 执行指定的JavaScript代码段
  19. -->
  20. <table>
  21. <tr><th>Enter Loan Data:</th>
  22. <td></td>
  23. <th>Loan Balance,Cumulative Equity,and Interest Payments</th></tr>
  24. <tr><td>Amount of the loan($):</td>
  25. <td><input id="amount"onchange="calculate();"></td>
  26. <td rowspan=8>
  27. <canvas id="graph"width="400"height="250"></canvas></td></tr>
  28. <tr><td>Annual interest(%):</td>
  29. <td><input id="apr"onchange="calculate();"></td></tr>
  30. <tr><td>Repayment period(years):</td>
  31. <td><input id="years"onchange="calculate();"></td>
  32. <tr><td>Zipcode(to find lenders):</td>
  33. <td><input id="zipcode"onchange="calculate();"></td>
  34. <tr><th>Approximate Payments:</th>
  35. <td><button onclick="calculate();">Calculate</button></td></tr>
  36. <tr><td>Monthly payment:</td>
  37. <td>$<span class="output"id="payment"></span></td></tr>
  38. <tr><td>Total payment:</td>
  39. <td>$<span class="output"id="total"></span></td></tr>
  40. <tr><td>Total interest:</td>
  41. <td>$<span class="output"id="totalinterest"></span></td></tr>
  42. <tr><th>Sponsors:</th><td colspan=2>
  43. Apply for your loan with one of these fine lenders:
  44. <div id="lenders"></div></td></tr>
  45. </table>
  46. <!--随后是JavaScirpt代码,这些代码内嵌在了一个<script>标签里-->
  47. <!--通常情况下,这些脚本代码应当放在<head>标签中-->
  48. <!--将JavaScript代码放在HTML代码之后仅仅是为了便于理解-->
  49. <script>
  50. "use strict";//如果浏览器支持的话,则开启ECMAScript 5的严格模式/*
  51. *这里的脚本定义了caculate()函数,在HTML代码中绑定事件处理程序时会调用它
  52. *这个函数从<input>元素中读取数据,计算贷款赔付信息,并将结果显示在<span>元素中
  53. *同样,这里还保存了用户数据、展示了放贷人链接并绘制出了图表
  54. */
  55. function calculate(){//查找文档中用于输入输出的元素
  56. var amount=document.getElementById("amount");
  57. var apr=document.getElementById("apr");
  58. var years=document.getElementById("years");
  59. var zipcode=document.getElementById("zipcode");
  60. var payment=document.getElementById("payment");
  61. var total=document.getElementById("total");
  62. var totalinterest=document.getElementById("totalinterest");//假设所有的输入都是合法的,将从input元素中获取输入数据
  63. //将百分比格式转换为小数格式,并从年利率转换为月利率
  64. //将年度赔付转换为月度赔付
  65. var principal=parseFloat(amount.value);
  66. var interest=parseFloat(apr.value)/100/12;
  67. var payments=parseFloat(years.value)*12;//现在计算月度赔付的数据
  68. var x=Math.pow(1+interest,payments);//Math.pow()进行幂次运算
  69. var monthly=(principal*x*interest)/(x-1);//如果结果没有超过JavaScript能表示的数字范围,且用户的输入也正确
  70. //这里所展示的结果就是合法的
  71. if(isFinite(monthly)){//将数据填充至输出字段的位置,四舍五入到小数点后两位数字
  72. payment.innerHTML=monthly.toFixed(2);
  73. total.innerHTML=(monthly*payments).toFixed(2);
  74. totalinterest.innerHTML=((monthly*payments)-principal).toFixed(2);//将用户的输入数据保存下来,这样在下次访问时也能取到数据
  75. save(amount.value,apr.value,years.value,zipcode.value);//找到并展示本地放贷人,但忽略网络错误
  76. try{//捕获这段代码抛出的所有异常
  77. getLenders(amount.value,apr.value,years.value,zipcode.value);
  78. }
  79. catch(e){/*忽略这些异常*/}//最后,用图表展示贷款余额、利息和资产收益
  80. chart(principal,interest,monthly,payments);
  81. }
  82. else{//计算结果不是数字或者是无穷大,意味着输入数据是非法或不完整的
  83. //清空之前的输出数据
  84. payment.innerHTML="";//清空元素的文本内容
  85. total.innerHTML=""
  86. totalinterest.innerHTML="";
  87. chart();//不传参数的话就是清除图表
  88. }
  89. }
  90. //将用户的输入保存至localStorage对象的属性中
  91. //这些属性在再次访问时还会继续保持在原位置
  92. //如果你在浏览器中按照file://URL的方式直接打开本地文件,
  93. //则无法在某些浏览器中使用存储功能(比如FireFox)
  94. //而通过HTTP打开文件是可行的
  95. function save(amount,apr,years,zipcode){
  96. if(window.localStorage){//只有在浏览器支持的时候才运行这里的代码
  97. localStorage.loan_amount=amount;
  98. localStorage.loan_apr=apr;
  99. localStorage.loan_years=years;
  100. localStorage.loan_zipcode=zipcode;
  101. }
  102. }
  103. //在文档首次加载时,将会尝试还原输入字段
  104. window.onload=function(){//如果浏览器支持本地存储并且上次保存的值是存在的
  105. if(window.localStorage&&localStorage.loan_amount){
  106. document.getElementById("amount").value=localStorage.loan_amount;
  107. document.getElementById("apr").value=localStorage.loan_apr;
  108. document.getElementById("years").value=localStorage.loan_years;
  109. document.getElementById("zipcode").value=localStorage.loan_zipcode;
  110. }
  111. };//将用户的输入发送至服务器端脚本(理论上)将
  112. //返回一个本地放贷人的链接列表,在这个例子中并没有实现这种查找放贷人的服务
  113. //但如果该服务存在,该函数会使用它
  114. function getLenders(amount,apr,years,zipcode){//如果浏览器不支持XMLHttpRequest对象,则退出
  115. if(!window.XMLHttpRequest)return;//找到要显示放贷人列表的元素
  116. var ad=document.getElementById("lenders");
  117. if(!ad)return;//如果返回为空,则退出
  118. //将用户的输入数据进行URL编码,并作为查询参数附加在URL里
  119. var url="getLenders.php"+//处理数据的URL地址
  120. "?amt="+encodeURIComponent(amount)+//使用查询串中的数据
  121. "&apr="+encodeURIComponent(apr)+
  122. "&yrs="+encodeURIComponent(years)+
  123. "&zip="+encodeURIComponent(zipcode);//通过XMLHttpRequest对象来提取返回数据
  124. var req=new XMLHttpRequest();//发起一个新的请求
  125. req.open("GET",url);//通过URL发起一个HTTP GET请求
  126. req.send(null);//不带任何正文发送这个请求
  127. //在返回数据之前,注册了一个事件处理函数,这个处理函数
  128. //将会在服务器的响应返回至客户端的时候调用
  129. //这种异步编程模型在客户端JavaScript中是非常常见的
  130. req.onreadystatechange=function(){
  131. if(req.readyState==4&&req.status==200){//如果代码运行到这里,说明我们得到了一个合法且完整的HTTP响应
  132. var response=req.responseText;//HTTP响应是以字符串的形式呈现的
  133. var lenders=JSON.parse(response);//将其解析为JS数组
  134. //将数组中的放贷人对象转换为HTML字符串形式
  135. var list="";
  136. for(var i=0;i<lenders.length;i++){
  137. list+="<li><a href='"+lenders[i].url+"'>"+
  138. lenders[i].name+"</a>";
  139. }
  140. //将数据在HTML元素中呈现出来
  141. ad.innerHTML="<ul>"+list+"</ul>";
  142. }
  143. }
  144. }
  145. //在HTML<canvas>元素中用图表展示月度贷款余额、利息和资产收益
  146. //如果不传入参数的话,则清空之前的图表数据
  147. function chart(principal,interest,monthly,payments){
  148. var graph=document.getElementById("graph");//得到<canvas>标签
  149. graph.width=graph.width;//用一种巧妙的手法清除并重置画布
  150. //如果不传入参数,或者浏览器不支持画布,则直接返回
  151. if(arguments.length==0||!graph.getContext)return;//获得画布元素的"context"对象,这个对象定义了一组绘画API
  152. var g=graph.getContext("2d");//所有的绘画操作都将基于这个对象
  153. var width=graph.width,
  154. height=graph.height;//获得画布大小
  155. //这里的函数作用是将付款数字和美元数据转换为像素
  156. function paymentToX(n){
  157. return n*width/payments;}
  158. function amountToY(a){
  159. return height-(a*height/(monthly*payments*1.05));}//付款数据是一条从(0,0)到(payments,monthly*payments)的直线
  160. g.moveTo(paymentToX(0),amountToY(0));//从左下方开始
  161. g.lineTo(paymentToX(payments),//绘至右上方
  162. amountToY(monthly*payments));
  163. g.lineTo(paymentToX(payments),amountToY(0));//再至右下方
  164. g.closePath();//将结尾连接至开头
  165. g.fillStyle="#f88";//亮红色
  166. g.fill();//填充矩形
  167. g.font="bold 12px sans-serif";//定义一种字体
  168. g.fillText("Total Interest Payments",20,20);//将文字绘制到图例中
  169. //很多资产数据并不是线性的,很难将其反映至图表中
  170. var equity=0;
  171. g.beginPath();//开始绘制新图形
  172. g.moveTo(paymentToX(0),amountToY(0));//从左下方开始
  173. for(var p=1;p<=payments;p++){//计算出每一笔赔付的利息
  174. var thisMonthsInterest=(principal-equity)*interest;
  175. equity+=(monthly-thisMonthsInterest);//得到资产额
  176. g.lineTo(paymentToX(p),amountToY(equity));//将数据绘制到画布上
  177. }
  178. g.lineTo(paymentToX(payments),amountToY(0));//将数据线绘制至x轴
  179. g.closePath();//将线条结尾连接至线条开头
  180. g.fillStyle="green";//使用绿色绘制图形
  181. g.fill();//曲线之下的部分均填充
  182. g.fillText("Total Equity",20,35);//文本颜色设置为绿色
  183. //再次循环,余额数据显示为黑色粗线条
  184. var bal=principal;
  185. g.beginPath();
  186. g.moveTo(paymentToX(0),amountToY(bal));
  187. for(var p=1;p<=payments;p++){
  188. var thisMonthsInterest=bal*interest;
  189. bal-=(monthly-thisMonthsInterest);//得到资产额
  190. g.lineTo(paymentToX(p),amountToY(bal));//将直线连接至某点
  191. }
  192. g.lineWidth=3;//将直线宽度加粗
  193. g.stroke();//绘制余额的曲线
  194. g.fillStyle="black";//使用黑色字体
  195. g.fillText("Loan Balance",20,50);//图例文字
  196. //将年度数据在X轴做标记
  197. g.textAlign="center";//文字居中对齐
  198. var y=amountToY(0);//Y坐标设为0
  199. for(var year=1;year*12<=payments;year++){//遍历每年
  200. var x=paymentToX(year*12);//计算标记位置
  201. g.fillRect(x-0.5,y-3,1,3);//开始绘制标记
  202. if(year==1)g.fillText("Year",x,y-5);//在坐标轴做标记
  203. if(year%5==0&&year*12!==payments)//每5年的数据
  204. g.fillText(String(year),x,y-5);
  205. }
  206. //将赔付数额标记在右边界
  207. g.textAlign="right";//文字右对齐
  208. g.textBaseline="middle";//文字垂直居中
  209. var ticks=[monthly*payments,principal];//我们将要用到的两个点
  210. var rightEdge=paymentToX(payments);//设置X坐标
  211. for(var i=0;i<ticks.length;i++){//对每两个点做循环
  212. var y=amountToY(ticks[i]);//计算每个标记的Y坐标
  213. g.fillRect(rightEdge-3,y-0.5,3,1);//绘制标记
  214. g.fillText(String(ticks[i].toFixed(0)),//绘制文本
  215. rightEdge-5,y);
  216. }
  217. }
  218. </script>
  219. </body>
  220. </html>