Javascript高级程序设计 笔记

with语句额作用是将代码的作用域设置到一个特定的对象中。语法:with(expression) statement;
定义with语句的目的主要是为了简化多次编写同一个对象的工作,
with(location){
var qs=search.substring(1);
var hostName=hostname;
var url=href;
}
使用with语句关联了location对象
严格模式下不允许使用with语句,否则将视为语法错误。
还有大量使用with语句会导致性能下降,同时也会增加调试困难,因此开发大型应用程序时,不建议使用with语句。
 
ECMAscript变量可能包含两种不同数据类型的值:基本类型值和引用类型值。基本类型指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象
基本类型:underfined,Null,Boolean,Number,String
我们不能给基本类型添加属性,即使不会报错;
所以,引用值类型和值类型的内存地址也是跟C#中的类型保存地址差不多,栈堆和托管堆
值类型传值,引用值类型传地址
 
DOM2级事件定义了两个方法,用于处理制定和删除事件处理程序的操作:addEventListener()和removeEventListener()。
使用DOM2级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。
通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这有意味着通过addEventListener()添加的匿名函数将无法移除;
 
在触发DOM上的某个事件时会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素,事件的类型以及其他与特定事件相关的信息。
 
在需要通过一个函数处理多个事件时,可以使用type属性。例如:
var btn=document.getElementById("myBtn");
var handler=function(event){
switch(event.type){
case "click":
  alert("clicked");
  break;
case "mouseover":
  event.target.style.backgroundColor="red";
  break;
case "mouseout":
  event.target.style.backgroundColor="";
  break;
}
};
btn.onclick=handler;
btn.onmouseover=handler;
btn.onmouseout=handler;
 
在DOM出现之前,开发人员经常使用Image对象在客户端预先加载图像。可以像使用<img>元素一样使用Image对象,只不过无法将其添加到DOM树中
 
hashchange事件,以便在URL的参数列表(及URL中“#”号后面的所有字符串)发生变化时通知开发人员。之所以新增这个事件,是因为在Ajax应用中,开发人员经常要利用URL参数列表来保存状态或导航信息。
 
在使用事件时,需要考虑如下一些内存与性能方面的问题:
有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏。
建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。
建议在浏览器卸载页面之前移除页面中的所有处理事件处理程序。
 
EventUtil对象,可以跨浏览器处理事件。并且调用方法;
 
对于提交表单的事件,最好使用submit事件去添加事件处理程序。事件触发后,代码取得了提交按钮并将其按钮的disabled的属性设置为true。注意,不能通过onclick事件处理程序来实现这个功能,原因是不同浏览器之间存在“时差”:有的浏览器会在触发表单的submit事件之间就触发click事件,而又的浏览器则相反。对于先触发click事件的浏览器,意味着会在提交发生之前禁用按钮,结果永远都不会提交表单。因此,最好是通过submi事件来禁用提交按钮。不过,这种方式不适合表单中不包含提交按钮的情况;如前所述,只有在包含提交按钮的情况下,才有可能触发表单submit;
 
文本框:
利用<input>元素的type特性设置为"text"。而通过设置size特性,来限制输入的字数
 
富文本编辑:(在iframe里面插入一个html页面,将这个页面设置为可编辑,然后就变成了富文本编辑)
又称为WYSIWYG(What You See Is What You Get,所见即所得)。
通过设置designMode属性,这个空白的HTML页面可以被编辑,而编辑对象则是该页面<body>元素的HTML代码。
通过设置使用名为contenteditable的特殊属性,这个也是可以编辑富文本内容的方式。
 
canvas:
var drawing=document.getElementById('mycanvas');
var context=drawing.getContext('2d');
2D上下文的两种基本绘图操作是填充和描边。
操作的结果取决于两个属性:fillStyle和strokeStyle
 
绘制矩形 矩形是唯一一种可以直接在2D上下文中绘制的形状,与矩形有关的方法包括fillRect(),strokeRect()和clearRect()。这三个方法都能接收4个参数:矩形的x坐标,矩形的y坐标,矩形宽度,矩形高度。这些参数单位都是像素;
 
toDataURL()是Canvas对象的方法,不是上下文对象的方法。
 
渐变:
在使用渐变的时候,为了让渐变覆盖整个矩形,而不是仅应用到矩形的一部分,矩形和渐变对象的坐标必须匹配才行,
var gradient=context.createLinearGradient(30,30,80,80);
gradient.addColorStop(0,"White");
gradient.addColorStop(1,"black");
context.fillStyle=gradient;
context.fillRect(30,30,50,50);
有时候可以考虑使用函数来确保坐标合适。
function createRectLinearGradient(context,x,y,width,height){
return context.createLinearGradient(x,y,x+width,y+height);
}
整合成下面的代码:
var gradient = createRectLinearGradient(context, 30, 30, 50, 50);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
//绘制渐变矩形
context.fi llStyle = gradient;
context.fillRect(30, 30, 50, 50);
 
创建径向渐变(或放射渐变):
var gradient=context.createRadialGradient(55,55,10,55,55,30);
gradient.addColorStop(0,"White");
gradient.addColorStop(1,"black");
 
context.fillStyle=gradient;
context.fillRect(30,30,50,50);
一般来说,让起点圆和终点圆保持为同心圆的情况比较多,这时候只要考虑给两个圆设置不同的半径就好了。
 
使用图像数据;
2D上下文的一个明显的长处就是,可以通过getImageData()取得原始图像数据。这个方法接收4个参数:要取得其数据的画面区域的x和y坐标以及该区域的像素宽度和高度。可以使用一下代码:
var imageData=context.getImageData(10,5,50,50);
这里返回的对象是ImageData的实例。每个ImageData对象都有三个属性:width,height和data。
 
HTML5的<canvas>元素提供了一组JavaScriptAPI,让我们可以动态地创建图形和图像。图形是在一个特定的上下文中创建的,而上下文对象目前有两种。
第一种是2D上下文,可以执行原始的绘图操作,比如:
1.设置填充,描边颜色和模式
2.绘制矩形
3.绘制路径
4.绘制文本
5.创建渐变和模式
第二种是3D上下文,即WebGL上下文。WebGL是从OpenGLES 2.0移植到浏览器的,而OpenGLES2.0是游戏开发人员在创建计算机图形图像时经常使用的一种语言。WebGL支持比2D上下文更丰富和更强大的图形图像处理能力,比如:
1.用GLSL(OpenGL Shading Language,OpenGL着色语言)编写的顶点和片段着色器
2.组成骨类型化数组,即能够将数组中的数据限定为某种特定的数值类型
3.创建和操作文理
 
HTML5脚本编程
跨文档消息传送(cross-document messaging),有时候简称为XDM,指的是在来自不同域的页面间传递消息.XDM把这种机制规范化,让我们能既稳妥又简单地实现跨文档通信。
XDM核心方法是postMessage()
 
Audio类型
<audio>元素还有一个原生的JavaScript构造函数Audio,可以再任何时候播放音频。从同为DOM元素的角度看,Audio与Image很相似,但Audio不用像Image那样必须插入到文档中。只要创建一个新势力,并传入音频源文件即可。
var audio = new Audio("sound.mp3");
EventUtil.addHandler(audio, "canplaythrough", function(event){
audio.play();
});
创建新的Audio实例即可开始下载指定的文件。下载完成后,调用play()就可以播放音频。
 
讨论了如下几个API:
1.跨文档消息传递API能够让我们在不降低同源策略安全性的前提下,在来自不同域的文档间传递消息。
2.原生拖放功能让我们可以方便地制定某个元素可拖动,并在操作系统要放置时做出响应。还可以创建自定义的可拖动元素及放置目标。
3.新的媒体元素<audio>和<video>拥有自己的音频和视频交互的API。并非所有浏览器支持所有的媒体格式,因此应该使用canPlayType()检查浏览器是否支持特定的格式。
4.历史状态管理让我们不必卸载当前页面即可修改浏览器的历史状态栈。有了这种机制,用户就可以通过“后退”和“前进“按钮在页面状态间切换,而这些状态完全由JavaScript进行控制。
 
错误处理与调试
try-catch语句
ECMA-262第3版引入了try-catch语句,作为JavaScript中处理异常的一种标准方式。
try{
//可能会导致错误的代码
}
catch(error){
//在错误发生时怎么处理
}
 
1.finally子句:
虽然在try-catch语句中式可选的,但finally子句一经使用,其代码无论如何都会执行。换句话说,try语句中的代码全部正常执行,finally子句会执行;如果因为出错而执行了catch语句块,finally子句照样还会执行。只要代码中包含finally子句,则无论try或catch语句块中包含什么代码--甚至return语句,都不会阻止finally子句的执行。
(只要代码中包含finally子句,那么无论try还是catch语句块中的return语句都将被忽略。因此,在使用finally子句之前,一定要非常清楚你想让代码怎么样)
 
2.错误类型
Error
EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
 
抛出错误
与try-catch语句相配的还有一个throw操作符,用于随时抛出自定义错误。抛出错误时,必须要给throw操作符制定一个值,这个值是什么类型,没有要求,下列代码都是有效的。
throw 1234;
throw "hello";
throw true;
throw {name:"JavaScript"};
 
function process(values){
if (!(values instanceof Array)){
throw new Error("process(): Argument must be an array.");
}
values.sort();
for (var i=0, len=values.length; i < len; i++){
if (values[i] > 100){
return values[i];
}
}
return -1;
}
 
常见的错误类型:
一般来说,需要关注三种错误:
1.类型转换错误:
类型转换错误发生在使用某个操作符,或者使用其他可能会自动转换值得数据类型的语言结构时。在使用相等(==)和不相等(!=)操作符,或者在if,for及while等流控制语句中使用非布尔值时,最常发生类型转换错误。
 
像if之类的语句在确定下一步操作之前,会自动把任何值转换成布尔值。
出错:
function concat(str1, str2, str3){
var result = str1 + str2;
if (str3){ //绝对不要这样!!!
result += str3;
}
return result;
}
正确:
function concat(str1, str2, str3){
var result = str1 + str2;
if (typeof str3 == "string"){ // 恰当的比较
result += str3;
}
return result;
}
 
数据类型错误
JavaScript是松散类型的,也就是说,在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确,为了保证不会发生数据类型错误,只能依靠开发人员编写适当的数据类型检测代码。
 
function getQueryString(url){
if (typeof url == "string"){ // 通过检查类型确保安全
var pos = url.indexOf("?");
if (pos > -1){
return url.substring(pos +1);
}
}
return "";
}
使用indexOf获取到?的下标位置;
然后substring对url?下标位置后的所有字符串取出来;
 
//安全,非数组值将被忽略
function reverseSort(values){
if (values instanceof Array){ // 问题解决了
values.sort();
values.reverse();
}
}
体来说,基本类型的值应该使用typeof来检测,而对象的值则应该使用instanceof来检测。根据使用函数的方式,有时候并不需要逐个检测所有参数的数据类型。但是,面向公众的API则必须无条件地执行类型检查。

JS中的变量是松散类型(即弱类型)的,可以用来保存任何类型的数据。

typeof 可以用来检测给定变量的数据类型,可能的返回值:1. 'undefined' --- 这个值未定义;

2. 'boolean'    --- 这个值是布尔值;

3. 'string'        --- 这个值是字符串;

4. 'number'     --- 这个值是数值;

5. 'object'       --- 这个值是对象或null;
6. 'function'    --- 这个值是函数。
 
instanceof 用于判断一个变量是否某个对象的实例,如
var a=new Array();
alert(a instanceof Array);
 
通信错误
随着Ajax编程的星期,Web应用程序在其生命周期内动态加载信息或功能,已经成为一件司空见惯的事。不过,JavaScript与服务器之间的任何一次通信,都有可能会产生错误。
 
下面是集中避免浏览器响应JavaScript错误的方法
1.在可能发生错误的地方使用try-catch语句,这样你还有机会以适当的方式对错误给出响应,而不必沿用浏览器处理错误的机制。
2.使用window.onerror事件处理程序,这种方式可以接受try-catch不能处理的所有错误(仅限IE,Firefox,Chrome).
另外,对任何Web应用程序都应该分析肯呢个的错误来源,并制定处理错误的方法。
3.首先,必须要明确什么是指明错误,什么是非致命错误。
4.其次,再分析代码,以判断最肯呢个发生的错误。JavaScript中发生错误的主要原因如下。
类型转换,未充分检测数据类型,发送给服务器或从服务器接收到的数据又错误
 
 
JSON
 
JSON是一个轻量级的数据格式,可以简化表示复杂数据结构的工作量。JSON使用JavaScript语法的子集表示对象,数组,字符串,数值,布尔值和null。即使XML也能表示同意复杂的数据结果,但JSON没有那么繁琐,而且在JavaScript中使用更便利。
ECMAScript 5定义了一个原生的JSON对象,可以用来将对象序列化为JSON字符串或者将JSON数据解析为JavaScript对象。JSON.stringify()和JSON.parse()方法分别用来实现上述两项功能。这两个方法都有一些选项,通过它们可以改变过滤方式,或者改变序列化的过程。
 
JSON的语法可以表示以下三种类型的值;
简单值:
使用与JavaScript相同的语法,可以再JSON中表示字符串,数值,布尔值和null。但JSON不支持JavaScript中的特殊值underfined;
JavaScript字符串与JSON字符串的最大区别在于,JSON字符必须使用双引号(单引号会导致语法错误)。
布尔值和null也是有效的JSON形式。但是,在实际应用中,JSON更多地用来表示更复杂的数据结构,而简单值只是整个数据结构中的一部分。
 
对象:
对象作为一种复杂数据类型,表示的是一组无序的键值对儿。而每个键值对儿中的值可以是简单值,也可以是复杂数据类型。
 
{
"name": "Nicholas",
"age": 29,
"school": {
"name": "Merrimack College",
"location": "North Andover, MA"
}
}
与JavaScript不同,JSON中对象的属性名任何时候都必须加双引号。手工编写JSON时,忘了给对象属性名加双引号或者把双引号写成单引号都是常见的错误。
 
数组:
数组也是一种复杂数据类型的值,表示一组有序的值得列表,可以通过数值索引来访问其中的值。数组的值也可以是任意类型-简单值,对象或数组。
JSON不支持变量,函数或对象实例,它就是一种表示结构化数据的格式,虽然与JavaScript中表示数据的某些语法相同,但它并不局限于JavaScript的范畴。
 
JSON中的第二种复杂数据类型是数组。JSON数组采用的就是JavaScript中的数组字面量形式。
下面是JavaScript中的数组字面量:
var values = [25, "hi", true];
JSON 中,可以采用同样的语法表示同一个数组:
[25, "hi", true]
 
[
{
"title": "Professional JavaScript",
"authors": [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
},
{
"title": "Professional JavaScript",
"authors": [
"Nicholas C. Zakas"
],
edition: 2,
year: 2009
},
{
"title": "Professional Ajax",
"authors": [
"Nicholas C. Zakas",
"Jeremy McPeak",
"Joe Fawcett"
],
edition: 2,
year: 2008
}
]
 
当然,这里是假设把解析 JSON 数据结构后得到的对象保存到了变量 books 中。再看看下面在DOM
结构中查找数据的代码:
doc.getElementsByTagName("book")[2].getAttribute("title")
 
JSON对象
早起的JSON解析器基本上就是使用JavaScript的eval()函数。由于JSON是JavaScript语法的子集,因此eval()函数可以解析,解释并返回JavaScript对象和数组。
对于不能原生支持JSON解析的浏览器,使用这个shim是最佳选择。
JSON对象有两个方法:stringify()和parse()。在最简单的情况下,这两个方法分辨用于把JavaScript对象序列化为JSON字符串和把JSON字符串解析为原生JavaScript值。例如
var book = {
title: "Professional JavaScript",
authors: [
"Nicholas C. Zakas"
],
edition: 3,
year: 2011
};
var jsonText = JSON.stringify(book);
 
{"title":"Professional JavaScript","authors":["Nicholas C. Zakas"],"edition":3,
"year":2011}
 
Ajax:
 
XHR的用法:
在使用XHR对象时,要调用的第一个方法是open(),它接受3个参数:要发送的请求的类型,请求的URL和表示是否异步发送请求的布尔值
function createXHR(){
if (typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined"){
if (typeof arguments.callee.activeXString != "string"){
var versions = [ "MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"],
i, len;
for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){
// 跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
var xhr=createXHR();
xhr.open("get","example.php",false);
xhr.send(null);
 
使用readyState在异步模式的情况下
var xhr = createXHR();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example.txt", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
 
setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值
 
下面这个函数可以辅助向现有URL的末尾添加查询字符串参数:
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
var url = "example.php";
//添加参数
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
//初始化请求
xhr.open("get", url, false);
在这里使用addURLParam()函数可以确保查询字符串的格式良好,并可靠地用于XHR对象;
 
FormData:
为序列化表单以及创建与表单格式相同的数据(用于通过XHR传输)提供便利;
var data=new FormData();
data.append("name","Nicholas");
 
var data=new FormData(document.forms[0]);
使用FormData的方便之处体现在不必明确地在XHR对象上设置请求头部。XHR对象能够识别传入的数据类型是FormData的实例,并配置适当的头部信息;
 
超时设定:
IE8------timeout属性,表示请求在等待响应多少毫秒之后就终止。在给timeout设置一个数值后,如果在规定的事件内浏览器还没有接受到响应,那么久会触发timeout事件,进而会调用ontimeout事件处理程序。(后被收入XMLRequestHttp2级规范)
xhr.timeout = 1000; // 将超时设置为 1 秒钟(仅适用于IE8+
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
 
重写XHR响应的MIME类型:
overrideMimeType()方法
能够重写服务器返回的MIME类型是很有用的。
调用overrideMimeType()必须在send()方法之前,才能保证重写响应的MIME类型。
 
Progress Events 进度事件:
 
总的有6个进度事件:
loadstart:在接收到响应数据的第一个字节时触发。
progress:在接收响应期间持续不断地触发
error:在请求发生错误时触发
abort:在因为调用abort()方法而终止连接时触发。
load:在接收到完整的响应数据时触发
loadend:在通信完成或者触发error,abort或load事件后触发
 
其中两个事件有一些细节需要注意
1,load事件 在接收到完整的响应数据时触发(就是为了替代readystatuschange事件。
只要浏览器接收到服务器的响应,不管其状态如何,都会触发load事件。而这意味着你必须要检查status属性,才能确定数据是否真的已经可用了。
xhr.onload = function(){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
2,progress事件 在接收响应期间持续不断地触发
xhr.onprogress = function(event){
var divStatus = document.getElementById("status");
if (event.lengthComputable){
divStatus.innerHTML = "Received " + event.position + " of " +
event.totalSize +" bytes";
}
};
每次触发progress事件,都会以新的状态信息更新HTML元素的内容。如果响应头部中包含content-Length字段,那么也可以利用此信息来计算从响应中已经接收到的数据的百分比。
 
跨源资源共享(CORS):
通过XHR实现Ajax通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR对象只能访问与包含它的页面属于同一域中的资源。这种安全策略 可以预防某些恶意行为。但是,实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。
CORS是W3C的一个工作草案,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。
CROS背后的基本思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应式应该层共,还是应该失败。
 
其他浏览器对CORS的实现:
由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对URL,在访问远程资源时再使用绝对URL。这样就能消除歧义,避免出现限制访问头部或本地cookie信息等问题;
 
Preflighted Reqeusts:
CORS通过一种叫做Prelighted Requests的透明服务器验证机制支持开发人员使用自定义的头部,GET或POST之外的方法,已经不同类型的主体内容。在使用下列高级选项来发送请求时,就会向服务器发送一个Preflight请求。
这种请求使用OPTIONS方法,发送下列头部:
Origin:与简单的请求相同。
Access-Control-Request-Method:请求自身使用的方法。
Access-Control-Request-Headers:(可选)自定义的头部信息,多个头部以逗号分隔。
 
其他跨域技术:
 
1)图像Ping :
一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。动态地创建图像,使用它们的onload和onerror事件处理程序来确定是否接收到了响应。
 
动态创建图像经常用于图像Ping。图像Ping是与服务器进行简单,单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以使任意内容,但通常是像素图或204响应。
var img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";
 
图像Ping最常用于跟踪用户点击页面或动态广告曝光次数。图像Ping有两个主要的缺点,一是只能发送GET请求,二是无法访问服务器的响应文本。因此,图像Ping只能用于浏览器与服务器间单向通信。
 
 
2)JSONP
JSONP是JSON with padding(填充式JSON或参数式JSON)的简称,是应用JSON的一种新方法,在后来的Web服务中非常流行。JSONP看起来与JSON差不多,只不过是被包含在函数调用中的JSON,就像这样:callback({"name":"Nicholas"})
JSONP由两部分组成:回调函数和数据。回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。而数据就是传入回调函数中的JSON数据。
http://freegeoip.net/json/?callback=handleResponse
JSONP是通过动态<script>元素来使用的,使用时可以为src属性指定一个跨域URL。这里的<script>元素与<img>元素类似,都有能力不受限制地从其他域加载资源。
function handleResponse(response){
alert("Youre at IP address " + response.ip + ", which is in " +
response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
 
优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。不过JSONP也有亮点不足。
首先JSONP是从其他域中夹杂代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃JSONP调用之外,没有办法追究。因此在使用㐊你自己运维的Web服务时,一定得保证它安全可靠
其次,要确定JSONP请求是否失败并不容易。虽然HTML5给<script>元素新增了一个onerror事件处理程序,但目前还没有得到任何浏览器支持。为此,开发人员不得不使用计时器检测指定时间内是否接收到了响应。
 
3)Comet
Comet是Alex Russell发明的一个词儿,指的是一种更高级的Ajax技术(经常也有人称为“服务器推送”)。Ajax是一种从页面向服务器请求数据的技术,而Comet则是一种服务器向页面推送数据的技术。Comet能够让信息近乎实时地推送到页面上,非常适合处理体育比赛的分数和股票的报价。
有两种实现Comet的方式:长轮询和流,长轮询是传统轮询(也称为段轮询)的一个翻版,即浏览器定时向服务器发送请求,看有没有更新的数据。
长轮询把短轮询颠倒了一下。页面 发起一个到服务器的请求,然后服务器一直保持连接打开,直到有数据可发送。发送完数据之后,浏览器关闭连接,随即又发送一个到服务器的新请求。这一过程在页面打开期间一直持续不断。
无论是短轮询还是长轮询,浏览器都要再接收数据之前,先发起对服务器的连接。两者最大的区别在于服务器如何发送数据。短轮询是服务器立即发送响应,无论数据是否有效,而长轮询是等待发送响应。轮询的优势是所有浏览器都支持,因为使用XHR对象和setTimeout()就能实现。而你要做的就是决定什么时候发送请求。
 
第二种流行的Comet实现HTTP流。流不同于上述两种轮询,因为它在页面的整个生命周期内只使用一个HTTP连接。具体来说,就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。比如下面这段PHP脚本就是采用流实现的浏览器中常见的形式。
 
<?php
$i = 0;
while(true){
//输出一些数据,然后立即刷新输出缓存
echo "Number is $i";
flush();
//等几秒钟
sleep(10);
$i++;
}
所有服务器端语言都支持打印到输出缓存然后刷新(将输出缓存中的内容一次性全部发送到客户端)。而这正是实现HTTP流的关键所在。
 
使用XHR对象实现HTTP流典型代码如下所示;
 
function createStreamingClient(url, progress, finished){
var xhr = new XMLHttpRequest(),
received = 0;
xhr.open("get", url, true);
xhr.onreadystatechange = function(){
var result;
if (xhr.readyState == 3){
//只取得最新数据并调整计数器
result = xhr.responseText.substring(received);
received += result.length;
//调用progress 回调函数
progress(result);
} else if (xhr.readyState == 4){
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}
var client = createStreamingClient("streaming.php", function(data){
alert("Received: " + data);
}, function(data){
alert("Done!");
});
这个 createStreamingClient()函数接收三个参数:要连接的 URL、在接收到数据时调用的函
数以及关闭连接时调用的函数
 
管理Comet的连接时很容易出错的,需要时间不短改进才能达到完美。
 
浏览器社区为Comet创建了两个新的接口:
 
服务器发送事件(SSE)是围绕只读Comet交互推出的API或者模式。
SSE API用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的MIME类型必须是text/event-stream,而且是浏览器中的JavaScript API能解析格式输出。SSE支持短轮询,长轮询,HTTP流,而且能再断开连接时自动确定何时重新连接。
1.SSE API   SSE的JavaScript API很相似。要预定新的事件流,首先要创建一个新的EventSource对象,并传进一个入口点:
var source=new EventSource("myevents.php");
 
 注意传入的URL必须与创建对象的页面同源(相同的URL模式,域及端口)。EventSource的实例有一个readyState属性,值为0表示正连接到服务器,值为1表示打开了连接,值为2表示关闭了连接。另外还有以下三个时间。open:在建立连接时触发。message:在从服务器接收到新时间时触发。error:在无法建立连接时触发。
默认情况下, EventSource 对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味着 SSE 适合长轮询和HTTP 流。如果想强制立即断开连接并且不再重新连接,可以调用 close()方法。
 
2.事件流
所谓的服务器事件会通过一个持久的HTTP响应发送,这个响应的MIME类型为text/event-stream。响应的格式是纯文本,最简单的情况是每个数据项都带有前缀data:
通过 id:前缀可以给特定的事件指定一个关联的 ID,这个ID 行位于data:行前面或后面皆可:
data: foo
id: 1
设置了 ID 后,EventSource 对象会跟踪上一次触发的事件。如果连接断开,会向服务器发送一个
包含名为 Last-Event-ID 的特殊HTTP 头部的请求,以便服务器知道下一次该触发哪个事件。在多次
连接的事件流中,这种机制可以确保浏览器以正确的顺序收到连接的数据段。
 
Web Sockets 的目标是在一个单独的持久连接上提供全双工,双向通信。在JavaScript中创建了Web Socket之后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议转换为Web Socket协议。也就是说,使用标准的HTTP服务器无法实现Web Sockets,只有支持这种协议的专门服务器才能正常工作。
 
由于Web sockets使用了自定义的协议,所以URL模式也略有不同。未加密的连接不再是http://,而是ws://;加密的连接也不是https://,而是wss://。在使用Web SocketURL时,必须带着这个模式,因为将来还有可能支持其他模式;
使用自定义协议而非HTTP协议的好处是,能够在客户端和服务器之间放非常少量的数据,而不必担心HTTP那样字节级的开销。由于传递的数据包很小,因此Web Sockets非常适合移动应用。
使用自定义协议的缺点在于,制定协议的事件比制定JAvaScript API的事件还要长。
 
1.web Sockets API 
要创建WeSocket,先实例一个websocket对象并传入要连接的URL:
var socket=new Websocket("ws://www.example.con/server.php");
注意,必须给Websocket构造函数传入绝对URL。同源策略对Web Socket不适用 ,因此可以通过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器(通过握手信息就可以知道请求来自何方。)
 
发送和接收数据
send()方法;向服务器发送数据(发送给服务器的数据要记得序列化)
socket.onmessage = function(event){
var data = event.data;
//处理数据
};
event.data中返回的数据也是字符串。如果要得到其他格式的数据,必须手工解析这些数据才行
 
websocket对象还有其他三个事件,在连接生命周期的不同阶段触发
open:在成功建立连接时触发
error:在发生错误时触发,连接不能持续
close:在连接关闭时触发
websocket对象不支持DOM2级事件侦听器,因此必须使用DOM0级语法分别定义每个事件处理程序。
 
SSE和Web Sockets
面对某个具体的用力,在考虑是使用SSE还是使用Web Sockets时,可以考虑如下几个因素。首先,你是否有自由度建立和维护Web Sockets服务器?因为Web Socket协议不同于HTTP,所以现有服务器不能用于Web Socket通信。SSE倒是通过常规HTTP通信,因此现有服务器就可以满足需求。
第二个要考虑的问题是到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩),那么SSE比较容易实现。如果用例必须双向通信(如聊天室),那么WebSockets显然更好。别忘了,在不能选择WebSockets的情况下,组合XHR和SSE也是能显示双向通信的。
 
高级技巧
 
高级函数 函数是JavaScript中最有趣的部分之一。
 
安全的类型检测:
instanceof操作符存在多个全局作用域(像一个页面包含多个frame)的情况下,也是问题多多。一个经典的例子 就是像下面这样将对象表示为数组
var  isArray=value instanceof Array;
以上代码要返回true,value必须是一个数组,而且还必须与Array构造函数在同个全局作用域中。别忘了,(Array是window的属性)。如果value是在另个frame中定义的数组,那么以上代码就会返回false。
 
在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式的字符串。每个类在内部都有一个[[Class]]属性,这个属性就指定了上述字符串中的构造函数名。
alert(Object.prototype.toString.call(value)); //"[object Array]"
由于原生数组的构造函数名与全局作用域无关,因此使用toString()就能保证返回一致的值。因此可以用这个来测试某个值是不是原生函数或正则表达式:
function isArray(value){
return Object.prototype.toString.call(value) == "[object Array]";
}
这一技巧也广泛应用于检测原生JSON对象。object的toString()方法不能检测非原生构造函数的构造函数名。因此,开发人员定义的任何构造函数都将返回[object object]。有些JavaScript库会包含于下面类似的代码。
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) ==
"[object JSON]";
 
作用域安全的构造函数:
使用new来调用该构造函数的情况。
问题出在当没有使用new操作符来调用该构造函数的情况上。由于该this对象是在运行时绑定的,所以直接调用Person(),this会映射到全局对象window上,导致错误对象属性的意外增加。例如:
var person = Person("Nicholas", 29, "Software Engineer");
alert(window.name); //"Nicholas"
alert(window.age); //29
alert(window.job); //"Software Engineer"
 
为了防止出现这种情况,可以使用一下解决方法,也就是在函数内部作一个判断:
 
function Person(name, age, job){
if (this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
} else {
return new Person(name, age, job);
}
}
var person1 = Person("Nicholas", 29, "Software Engineer");
alert(window.name); //""
alert(person1.name); //"Nicholas"
var person2 = new Person("Shelby", 34, "Ergonomist");
alert(person2.name); //"Shelby"
 
最后的结果是,调用Person构造函数时无论是否使用new操作符,都会返回一个Person的新实例,这就避免了在全局对象上的意外设置属性。
 
惰性载入函数:
减少if语句的使用,也就是说即使只有一个if语句的代码,也肯定要比没有if语句的慢,所以如果if语句不必每次执行,那么代码可以运行地更快一些。解决方案就是称之为惰性载入的技巧。
惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,第一种就是再函数被调用时再处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样就任何对原函数的调用都不用再经过执行的分支了。
 
载入函数的优点是指在执行分支代码时牺牲一点儿性能。至于哪种方式更海华丝,就要看你的具体需求而定了。不过这两种方式都能避免执行不必要的代码。
 
函数绑定:
函数绑定要创建一个函数,可以在特定的this环境中以指定参数调用另一个函数该技巧常常和回调函数与事件处理程序一起使用,以便在将函数作为变量传递的同时保留代码执行环境。看看例子:
var handle={
message:"Event handled",
handleClick:function(event){
alert(this.message);
}
};
var btn=document.getElementById("my-btn");
EventUtil.addHandler(btn,"click",handler.handleClick);
 
//EventUtil.addHandler(btn,"click",function(event){
handler.handleClick(event);
})
 
一个简单bind()函数接收一个函数和一个环境,并返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动传递过去。语法如下:
function bind(fn,context){
return function(){
return fn.apply(context,arguments);
};
}
 
//EventUtil.addHandler(btn,"click",bind(handler.handleClick,handler));  ?
 
ECMAScript 5为所有函数定义了一个原生的bind()方法,进一步简单了操作。换句话说,你不用再自己定义bind()函数了,而是可以直接在函数上调用这个方法。例如:
//EventUtil.addHandler(btn,"click",handler.handleClick.bind(handler));
 
原生的bind()方法与前面介绍的自定义bind()方法类似,都是要传入作为this值的对象。支持原生bind()方法的浏览器有IE9+,Frefox 4+,Chrome.
 
只要是将某个函数指针以值得形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。它们主要用于事件处理程序以及setTimeout()和s etInterval()。然而,被绑定函数与普通函数相比有更多的开销,它们需要更多内存,同时也因为多重函数调用稍微慢点,所以最好指在必要时使用。
 
函数柯里化:
它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数
例子:
function add(num1,num2){
return num1+num2;
}
function curriedAdd(num2){
return add(5,num2);
}
alert(add(2,3));         //5
alert(curriedAdd(3)); //8
从技术上来说curriedAdd()并非柯里化的函数,但它很好地展示了其概念。
 
柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式。
function curry(fn){
 var args=Array.prototype.slice.call(arguments,1);
 return function(){
     var innerArgs=Array.prototype.slice.call(arguments);
     var finalArgs=args.concat(innerArgs);
     return fn.apply(null,finalArgs);
     }
}
curry()函数的主要工作就是将被返回函数的参数进行排序。curry()的第一个参数是要进行柯里化的函数,其他参数是要传入的值。为了获取第一个参数之后的所有参数,在arguments对象上调用了slice()方法,并传入参数1表示被返回的数组包含从第二个参数开始的所有参数。然后args数组包含了来自外部函数的参数。在内部函数中,创建了innerArgs数组用来存放所有传入的参数(又一次用到了slice())。有了存放来自外部函数和内部函数的参数数组后,就可以使用concat()方法将它们组合为finalArgs,然后使用apply()将结果传递给该函数。注意这个函数并没有考虑到执行环境,所有调用apply()时第一个参数是null。curry()函数可以按一下方式应用。
function add(num1,num2){
return num1+num2;
}
var curriedAdd=curry(add,5); 
alert(curriedAdd(3));     //8
 
JavaScript中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销。
 
防篡改对象
JavaScript共享的本质一直是开发人员心头的痛。因为任何对象都可以被在同一环境中运行的代码修改。开发人员很可能会意外地修改别人的代码,甚至更糟糕地,用不兼容的功能重写原生对象。
 
ECMAScript5 也增加了几个方法,通过它们可以指定对象的行为。不过请注意:一旦把把对象定义为防篡改,就无法撤销了。
 
不可扩展对象:(不能添加属性和方法)
默认情况下,所有对象都是可以扩展的。也就是说,任何时候都可以向对象中添加属性和方法。例如,可以像下面这样先定义一个对象,后来再给它添加一个属性。
var person={name:"Nicolas"};
person.age=29;
 
现在,使用Object.preventExtensions()方法可以改变这个行为,让你不能再给对象添加属性和方法。
例如:
var person={name:"Nicholas"};
alert(object.isExtensible(person));     //true
Objext.preventExtensions(person);   
alert(object.isExtensible(person));     //false
 
在调用了object.preventExtensions()方法后,就不能给person对象添加新属性和方法了。在非严格模式下,给对象添加新成员会导致静默失败,因此person.age将是undefined。而在严格模式下,导致给不可扩展的对象添加新成员会导致抛出错误。
虽然不能给对象添加新成员,但已有的成员则丝毫不受影响。你仍然还可以修改和删除已有的成员。
 
密封的对象:(不能添加,也不能删除)
ECMAScript 5 为对象定义的第二个保护级别是密封对象(sealed object)。密封对象不可扩展,而且已有成员的[[Configurable]]特性将被设置为false。这就意味着不能删除属性和方法,因为不能使用Object.defineProperty()把数据属性修改为访问器属性,或者相反。属性值是可以修改的。
要密封对象,可以使用Object.seal()方法。
var person={name:"Nicholas"};
Object.seal(person);
 
person.age=29;
alert(person.age);    //undefined
 
delete person.name;
alert(person.name); //"Nicholas"
 
这个代码里面添加以及删除操作都被忽略掉了。这是在非严格模式下的行为。在严格模式下,尝试添加或删除对象成员都会导致抛出错误。
 
使用Object.isSealed()方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以用Object.isExtendible()检测密封的对象也会返回false.
var person = { name: "Nicholas" };
alert(Object.isExtensible(person)); //true
alert(Object.isSealed(person)); //false
Object.seal(person);
alert(Object.isExtensible(person)); //false
alert(Object.isSealed(person)); //true
 
冻结的对象:(增删改,全部禁止掉)
最严格的防篡改级别是冻结对象(frozen object)。冻结的对象既不可扩展,优势密封的,而且对象数据属性的[[Writable]]特性会被设置为false。如果定义[[Set]]函数,访问器舒心仍然是可写的。
ECMAScirpt5定义的object.freeze()方法可以用来冻结对象。
var person = { name: "Nicholas" };
Object.freeze(person);
person.age = 29;
alert(person.age); //undefined
delete person.name;
alert(person.name); //"Nicholas"
person.name = "Greg";
alert(person.name); //"Nicholas"
 
与密封和不允许扩展一样,对象冻结的对象执行非法操作在非严格模式下会被忽略,而在严格模式下会抛出错误。
 
当然,也有一个Object.isFrozen()方法用于检测冻结对象。因为冻结对象既是密封的又是不可扩展的,所以用Object.isExtensible()和Object.isSealed()检测冻结对象将分别返回false和true。
 
JavaScript 库的作者而言,冻结对象是很有用的。因为 JavaScript 库最怕有人意外(或有意)地修改了库中的核心对象。冻结(或密封)主要的库对象能够防止这些问题的发生。
 
高级定时器
setTimeout()和setInterval()创建的定时器可以用于实现有趣且有用的功能。虽然人们对JavaScript的定时器存在普遍的误解,认为它们是线程,其实JavaScript是运行于单线程的环境中的,而定时器仅仅只是计划代码在未来的某个时间执行。执行时机是不能保证的,因为在页面的生命周期中,不同时间可能有其他代码在控制JavaScript进程。在页面下载完后的代码运行,事件处理程序,Ajax回调函数都必须使用同样的的线程来执行。实际上上,浏览器负责进行排序,指派某段代码在某个时间点运行的优先级。
 
定时器对队列的工作方式是,当特定事件过去后将代码插入。注意,给队列添加代码并不意味着对它立刻执行,而只能表示它会尽快执行。设定一个150ms后执行的定时器不代表到了150ms代码就立刻执行,它表示代码会在150ms后被加入到队列中,如果在这个时间点上,队列中没有其他东西,那么这段代码就会被执行,表面上看上去好像代码就字精确制定的时间点上执行了。其他情况下,代码可能明显地等待更长时间才执行。
 
关于定时器要记住的最重要的事情是,指定的时间间隔表示何时将定时器的代码添加到队列,而不是何时实际执行代码。如果前面例子中的onclick事件处理程序执行了300ms,那么定时器的代码至少要在定时器设置之后的300ms后才会被执行。队列中所有的代码都要等到JavaScript进程空闲之后才能执行,而不管它们是如何添加到队列中的。
 
在Firefox中定时器的实现还能让你确定定时器过了多久才执行,这需传递一个实际执行的时间与指定的间隔的差值。
如下:
//仅仅在Firefox中
setTimeout(function(diff){
if(diff>0){
//晚调用
}else if(diff<0){
//早调用                                                                                                                                                           
}else{
//调用及时
}
},250);
 
执行完一套代码后,JavaScript进程返回一段很短的时间,这样页面上的其他处理就可以进行了。由于JavaScript进程会阻塞其他页面处理,所以必须有这些小间隔来防止用户界面被锁定(代码长时间运行中还有可能出现)。这样设置一个定时器,可以确保在定时器代码执行前至少有一个进程间隔。
 
重复定时器
使用setInterval()创建的定时器确保了定时器代码规则地插入队列中。这个方式的问题在于,定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。幸好,JavaScript引擎够聪明,能避免这个问题。当使用setInterval()时,仅当没有该定时器的任何掐代码实例时,才将定时器代码添加到队列。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。
 
这种重复定时器的规则有两个问题:1)某些间隔会被跳过;2)多个定时器的代码之间的间隔可能会比预期的小。
 
为了避免setInterval()的重复定时器的这2个缺点,你可以用如下模式使用链式setTimeout()调用。
setTimeout(function(){
//处理中
setTimeout(arguments.callee,interval);
},interval);
 
这个模式链式调用了setTimeout(),每次函数执行的时候都会创建一个新的定时器。第二个setTimeout()调用使用了arguments.callee来获取对当前执行的函数的引用,并为其设置另外一个定时器。这样作的好处是,在前一个定时器代码执行完之前,不会向队列插入新的定时器代码,确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续的运行。
这个模式主要用于重复定时器,如下例所示:
setTimeout(function(){
var div = document.getElementById("myDiv");
left = parseInt(div.style.left) + 5;
div.style.left = left + "px";
if (left < 200){
setTimeout(arguments.callee, 50);
}
}, 50);
 
每个浏览器窗口、标签页、或者 frame 都有其各自的代码执行队列。这意味着,进行跨 frame 或者跨窗口的定时调用,当代码同时执行的时候可能会导致竞争条件。无论何时需要使用这种通信类型,最好是在接收 frame 或者窗口中创建一个定时器来执行代码。
 
Yielding Process
运行在浏览器中的JavaScript都被分配了一个确定数量的资源。不同于桌面应用往往能够随意控制他们要的内存大小和处理器事件,JavaScript被严格限制了,以防止恶意的Web程序员把用户的计算机搞挂了。其中一个限制是长时间运行脚本的制约,如果代码运行是超过特定的时间或者特定语句数量就不让它继续执行。如果代码达到了这个限制,会弹出一个浏览器错误的对话框,告诉用户某个脚本会用过长的事件执行,询问是允许其继续执行还是停止它。所有JavaScript开发人员的目标就是,确保用户永远不会在浏览器中看到这个令人费解的对话框。定时器是绕开此限制的方法之一。
 
脚本长时间运行的问题通常是由于两个原因之一造成的:过长的,过深嵌套的函数调用或者时进行大量处理的循环。
 
数组分块的技术,小块小块地处理数组,通常每次一小块。基本的思路是为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。基本模式如下:
setTimeout(function(){
//取出下一个条目并处理
var item = array.shift();
process(item);
//若还有条目,再设置另一个定时器
if(array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
 
在数组分块模式中,array变量本质上就是一个“待办事宜”列表,它包含了要处理的项目。使用shift()方法可以获取队列中下一个要处理的项目,然后将其传递给某个函数。如果在队列中还有其他项目,则设置另一个定时器,并通过arguments.callee调用同一个匿名函数。
 
要实现数组分块非常简单,可以使用一下函数。
function chunk(array, process, context){
setTimeout(function(){
var item = array.shift();
process.call(context, item);
if (array.length > 0){
setTimeout(arguments.callee, 100);
}
}, 100);
}
 
函数节流
浏览器中某而写计算和处理要比其他的昂贵很多。例如,DOM操作比非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在IE中使用onresize事件处理程序的时候容易发生,当调整浏览器大小的时候,该事件会连续触发。在inresize事件处理程序内部如果尝试进行DOM操作,其高频率的更改可能会让浏览器崩溃、为了绕开这个问题,你可以使用定时器对该函数进行节流。
 
函数节流背后的基本思想是指,某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在制定的时间间隔之后运行代码。当第二次调用该函数时,它会清楚前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止一段时间之后才执行。
 
自定义事件:
事件是一种叫观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该度一项生命周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。
事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中---通过实现自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:
 
拖放:
点击某个对象,并按住鼠标按钮不放,将鼠标移动到另一个区域,然后释放鼠标按钮将对象“放”在这里。
这个技术源自一种叫做“鼠标拖尾”的经典网页技巧。鼠标拖尾是一个或者多个图片在页面上跟着鼠标指针移动。单元素鼠标拖尾的基本代码需要为文档设置一个onmousemove事件处理程序,它总是将指定元素移动到鼠标指针位置
 
 
 
避免一个以上全局变量;
可以创建一个包含两者的对象:
var MyApplication={
name:"Nicholas",
sayName:function(){
alert(this.name);
}
}
 
单一的全局量的延伸便是命名空间的概念,由 YUIYahoo! User Interface )库普及。命名空间包括
创建一个用于放置功能的对象。在 YUI 2.x 版本中,有若干用于追加功能的命名空间。比如:
q YAHOO.util.Dom —— 处理 DOM 的方法;
q YAHOO.util.Event —— 与事件交互的方法;
q YAHOO.lang —— 用于底层语言特性的方法。
对于 YUI,单一的全局对象YAHOO 作为一个容器,其中定义了其他对象。用这种方式将功能组合
在一起的对象,叫做命名空间。整个 YUI 库便是构建在这个概念上的,让它能够在同一个页面上与其他
JavaScript 库共存。
 
//创建全局对象
var Wrox = {};
// Professional JavaScript 创建命名空间
Wrox.ProJS = {};
//将书中用到的对象附加上去
Wrox.ProJS.EventUtil = { ... };
Wrox.ProJS.CookieUtil = { ... };
 
避免与null进行比较:
JavaScript不做任何自动的类型检查,所以它就成了开发人员的责任
 
使用常量:
尽管JavaScript没有常量的正是概念,但它还是很有用的。这种将数据从应用逻辑分离出来的思想,可以在不冒引入错误的风险的同时,就改变数据。
function validate(value){
if (!value){
alert("Invalid value!");
location.href = "/errors/invalid.php";
}
}
险在这个函数中有两段数据:要显示给用户的信息以及URL。显示在用户界面上的字符串应该以允许进行语言国际化的方式抽取出来。URL也应被抽取出来,因为它们有随着应用成长而改变的倾向。基本上,有着可能由于这样那样原因会变化的这些数据,那么都会需要找到函数并在其中修改代码。而每次修改应用逻辑的代码,都可能会引入错误。可以通过将数据抽取出来变成单独定义的常量的方式,将应用逻辑与数据修改隔离来。
栗子;
var Constants = {
INVALID_VALUE_MSG: "Invalid value!",
INVALID_VALUE_URL: "/errors/invalid.php"
};
function validate(value){
if (!value){
alert(Constants.INVALID_VALUE_MSG);
location.href = Constants.INVALID_VALUE_URL;
}
}
在这段重写的代码中,消息和URL都被定义于Constans对象中,然后函数引用这些值。这些设置允许数据在无须接触使用它的函数的情况下进行变量。
关键在于将数据和使用它的逻辑进行分离。要注意的值得类型如下所示。
重复值--任何在多处用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了CSS类名。
用户界面字符串--任何用于显示给用户的字符串,都应被抽取出来以方便国际化。
URLs--在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL。
任意可能会更改的值--每当你在用到字面量值得时候,你都要问一下自己在这个值在未来是不是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量。
 
对于企业级的JavaScript开发而言,使用常量是非常重要的技巧,因为它能让代码更容易维护,并且在数据更改的同时保护代码。
 
性能
注意作用域:
随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的事件也在增加。访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能。
1.避免全局查找
可能优化脚本性能最重要的就是注意全局查找。使用全局变量和函数肯定要比局部的开销更大,因为要涉及作用域链上的查找。
function updateUI(){
var imgs = document.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = document.title + " image " + i;
}
var msg = document.getElementById("msg");
msg.innerHTML = "Update complete.";
}
该函数可能看上去完全正常,但是它包含了三个对于全局document对象的引用。如果在页面上有多个图片,那么for循环中的document引用就会被执行多次甚至上百次,每次都会要进行作用域查找。通过创建一个纸箱document对象的局部变量,就可以通过限制一次全局查找来改进这个函数的性能:
function updateUI(){
var doc = document;
var imgs = doc.getElementsByTagName("img");
for (var i=0, len=imgs.length; i < len; i++){
imgs[i].title = doc.title + " image " + i;
}
var msg = doc.getElementById("msg");
msg.innerHTML = "Update complete.";
}
 
2.避免with语句(  with 语句可以方便地用来引用某个特定对象中已有的属性,但是不能用来给对象添加属性。)
在性能非常重要的地方必须避免使用with语句。和函数类似,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度。用于额外的作用域链查找,在with语句中执行的代码肯定会比外面执行的代码要慢。
必须使用with语句的情况很少,因为它主要用于消除额外的字符。在大多数情况下,可以用局部变量完成相同的事情而不引入新的作用域。下面是一个例子:
function updateBody(){
with(document.body){
alert(tagName);
innerHTML="Hello world!";
}
}
这段代码中的with语句让document.body变得更容易使用。其实可以使用局部变量达到相同的效果,如下所示:
function updateBody(){
var body=document.body;
alert(body.tagName);
body.innerHTML="Hello world!";
}
虽然
代码稍微长了点,但是阅读起来比with语句版本更好,它确保让你知道tagName和innerHTML是属于哪个对象的。同时,这段代码通过将document.body存储在局部变量中省去了额外的全局查找。
 
选择正确方法
和其他语言一样,性能问题的一部分是和用于解决问题的算法或者方法有关的。老练的开发人员根据经验可以得知哪种方法可能获得更好的性能。恩多应用字其他编程语言中的技术和方法也可以在JavaScript中使用。
 
优化循环:
减值迭代--大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。
简化终止条件--由于每次循环过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或其他O(n)的操作。
简化循环体--循环体是执行最多的,所以要确保其被最大限度地优化。确保没有某些可以被很容易移除循环的密集计算。
使用后测试循环--最常用for循环和while循环都是前测试循环。而如do-while这种后测试循环,可以避免最终终止条件的计算,因此运行更快。
 
展开循环:
当循环次数是确定的,消除循环并使用多次函数调用往往更快。例如:如果数组的长度是一样的,对每个元素都调用pocess()可能更优,如以下代码所示:
//消除循环
process(values[0]);
process(values[1]);
process(values[2]);
这个例子假设values数组里面只有3个元素,直接对每个元素调用process()。这样展开循环可以消除建立循环和处理终止条件的额外开销,使代码运行得更快;
如果循环中的迭代次数不能事先确定,那么可以考虑使用一种叫做Duff装置的技术。这个技术是以其创建者Tom Duff命名的,他最早在C语言中使用这项技术。正是Jeff Greenberg用JavaScript实现了Duff装置。Duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。
请看以下代码:
//credit: Jeff Greenberg for JS implementation of Duff’s Device
//假设 values.length > 0
var iterations = Math.ceil(values.length / 8);
var startAt = values.length % 8;
var i = 0;
do {
switch(startAt){
case 0: process(values[i++]);
case 7: process(values[i++]);
case 6: process(values[i++]);
case 5: process(values[i++]);
case 4: process(values[i++]);
case 3: process(values[i++]);
case 2: process(values[i++]);
case 1: process(values[i++]);
}
startAt = 0;
} while (--iterations > 0);
Duff装置的实现是通过将values数组中元素个数除以8来计算出循环需要进行多少次迭代的。然后使用取整的上限函数确保结果是整数。如果完全根据除8来进行迭代,可能会有一些不能被处理到的元素,这个数量保存在startAt变量中。首次执行该循环时,会检查StartAt变量看有需要多少额外调用。例如,如果数组中有10个值,startAt则等于2,那么最开始的时候process()则只会被调用2次。在接下来的循环中,startAt被重置为0,这样之后的每次循环都会调用8次process()。展开循环可以提升大数据集的处理速度。
 
由Andrew B.King 所著的Speed Up your Site提出了一个更快的Duff装置技术,将do-while循环分成2个单独的循环。以下是例子:
//credit: Speed Up Your Site (New Riders, 2003)
var iterations = Math.floor(values.length / 8);
var leftover = values.length % 8;
var i = 0;
if (leftover > 0){
do {
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);
 
在这个实现中,剩余的计算部分不会再实际循环中处理,而是在一个初始化循环中进行除以8的操作。当处理掉了额外的元素,继续执行每次调用8次process()的主循环。这个方法几乎比原始的Duff装置实现快上40%。
针对大数据集使用展开循环可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。它是要花更多的代码来完成同样的任务,如果处理的不是大数据集,一般来说并不值得。
 
避免双重解释:
当JavaScript代码想解析JavaScript的时候就会存在双重解释惩罚。当使用eval()函数或者是Function构造函数以及使用setTimeout()传一个字符串参数时都会发生这种情况。下面有一些例子:
//某些代码求值——避免!!
eval("alert('Hello world!')");
//创建新函数——避免!!
var sayHi = new Function("alert('Hello world!')");
//设置超时——避免!!
setTimeout("alert('Hello world!')", 500);
 
在以上这些例子中,都要解析包含了JavaScript代码的字符串。这个操作是不能在初始的解析过程中完成的,因为代码是包含在字符串中的,也就是说在JavaScript代码运行的同时必须新启动一个解析器来解析新的代码。实例化一个新的解析器有不容忽视的开销,所以这种代码要直接解析慢得多。
对于这几个例子都有另外的办法。只有极少的情况下eval()是绝对必须的,所以尽可能避免使用。在这个例子中,代码其实可以直接内嵌在原代码中。对于Function构造函数,完全可以直接写成一般的函数,调用setTimeout()可以传入函数作为第一参数。以下是一些例子:
//已修正
alert('Hello world!');
//创建新函数——已修正
var sayHi = function(){
alert('Hello world!');
};
//设置一个超时——已修正
setTimeout(function(){
alert('Hello world!');
}, 500);
如果要提高代码性能,尽可能避免出现需要按照JavaScript解释的字符串。
 
性能的其他注意事项:
当评估脚本性能的时候,还有其他一些可以考虑的东西。下面并非主要的问题,不过如果使用得当也会有相当大的提升。
q 原生方法较快——只要有可能,使用原生方法而不是自己用JavaScript 重写一个。原生方法是用
诸如C/C++之类的编译型语言写出来的,所以要比JavaScript 的快很多很多。JavaScript 中最容
易被忘记的就是可以在 Math 对象中找到的复杂的数学运算;这些方法要比任何用JavaScript 写
的同样方法如正弦、余弦快的多。
q Switch 语句较快 —— 如果有一系列复杂的 if-else 语句,可以转换成单个switch 语句则可
以得到更快的代码。还可以通过将 case 语句按照最可能的到最不可能的顺序进行组织,来进一
步优化switch 语句。
q 位运算符较快 —— 当进行数学运算的时候,位运算操作要比任何布尔运算或者算数运算快。选
择性地用位运算替换算数运算可以极大提升复杂计算的性能。诸如取模,逻辑与和逻辑或都可
以考虑用位运算来替换
 
最小化语句数:
1.多个变量声明:
有个地方很多开发人员都容易创建很多语句,那就是多个变量的声明,很容易看到代码中由多个var语句来声明多个变量,如下所示:
//4 个语句——很浪费
var count = 5;
var color = "blue";
var values = [1,2,3];
var now = new Date();
在强类型语言中,不同的数据类型的变量必须在不同的语句中声明。然而,在JavaScript中所有的变量都可以使用单个var语句来声明。前面的代码可以如下重写:
//一个语句
var count = 5,
color = "blue",
values = [1,2,3],
now = new Date();
此处,变量声明只用了一个var语句,之间由逗号隔开。在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明快很多。
2.插入迭代值:
当使用迭代值(也就是在不同位置进行增加或减少的值)的时候,尽可能合并语句。请看以下代码:
var name=values[i];
i++;
前面这2句语句各只有一个目的:第一个从values数组中获取值,然后存储在name中;第二个给变量i增加1。这两句可以通过迭代值插入第一个语句组合成一个语句,如下所示:
var name=values[i++];
这一个语句可以完成和前面两个语句一样的事情。因为自增操作符是后缀操作符,i的值只有在语句其他部分结束之后才会增加。一旦出现类似情况,都要尝试将迭代值插入到最后使用它的语句中去。
3.使用数组和对象字面量:
使用构造函数或者是使用字面量。使用构造函数总是要用到更多的语句来插入元素或者定义属性,而字面量可以将这些操作在一个语句中完成。请看以下例子:
//4 个语句创建和初始化数组——浪费
var values = new Array();
values[0] = 123;
values[1] = 456;
values[2] = 789;
//4 个语句创建和初始化对象——浪费
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.sayName = function(){
alert(this.name);
};
这段代码中,只创建和初始化了一个数组和一个对象。各用了4个语句:一个调用构造函数,其他3个分配数据。其实可以很容易地转换成使用字面量的形式,如下所示:
//只用一条语句创建和初始化数组
var values = [123, 456, 789];
//只用一条语句创建和初始化对象
var person = {
name : "Nicholas",
age : 29,
sayName : function(){
alert(this.name);
}
};
重写后的代码只包含两条语句,一条是创建和初始化数组,另一条创建和初始化对象。之前用了八条语句的的东西现在只用了两条,减少了75%的语句量。在包含成千上万行JavaScript的代码库中,这些优化的价值更大。只要有可能,尽量使用数据和对象的字面量表达式来消除不必要的语句。
 
优化DOM交互:
1.最小化现场更新
一旦你需要访问的DOM部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新。每一个更改,不管是插入单个字符,还是移除整个片段,都会有一个性能惩罚,因为浏览器要重新计算无数尺寸以进行更新。现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。
2.使用innerHTML
有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改而言,两种方法效率都差不多。然而,对于大的DOM更改,使用innerHTML要比使用标准DOM方法创建同样的DOM结构快得多。
当把innerHTML设置为某个值时,后台会创建一个HTML解析器,然后使用内部的DOM调用来创建DOM结构,而非基于JavaScript的DOM调用。由于内部方法是编译好的而非解释执行的。所以执行快得多。
在使用innerHTML时候,可以使用字符串连接后再调用innerHTML比较好。
3.使用事件代理
大多数Web应用在用户交互上大量用到事件处理程序。页面上的事件处理程序的数量和页面相应用户交互的速度之间有个负相关。为了减轻这种惩罚,最好使用事件代理。
事件代理,如第13章所讨论的那样,用到了事件冒泡。任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理。使用这个知识,就可以将事件处理程序附加到更高层的地方负责多个目标的事件处理。如果可能,在文档级别附加事件处理程序,这样可以处理整个页面的事件。
4.注意HTMLCollection
HTMLCollection对象的陷阱,因为它们对于Web应用的性能而言是巨大的损害。记住,任何时候要访问HTMLCollection,不管它是一个属性还是一个方法,都是在文档删进行一个查询,这个查询开销很昂贵。最小化访问HTMLCollection的次数可以极大地改进脚本的性能。
也许优化HTMLCollection访问最重要的地方就是循环了。前面提到过将长度计算移入for循环的初始化部分。现在看一下这个例子:
var images = document.getElementsByTagName("img"),
i, len;
for (i=0, len=images.length; i < len; i++){
//处理
}
这里的关键在于长度length存入了len变量,而不是每次都去访问HTMLCollection的length属性。当在循环中使用HTMLCollection的时候,下一步应该是获取要使用的项目的引用,如下所示,以便避免在循环体内多次调用HTMLCollection。
var images = document.getElementsByTagName("img"),
image,
i, len;
for (i=0, len=images.length; i < len; i++){
image = images[i];
//处理
}
这段代码添加了image变量,保存了当前的图像。这之后,在循环内就没有理由再访问images的HTMLCollection了。
编写JavaScript的时候,一定要知道何时返回HTMLCollection对象,这样你就可以最小化对他们的访问。发生以下情况时会返回HTMLCollection对象:
q 进行了对 getElementsByTagName() 的调用;
q 获取了元素的 childNodes 属性;
q 获取了元素的 attributes 属性;
q 访问了特殊的集合,如document.forms document.images 等。
要了解当使用HTMLCollection对象时,合理使用会极大提升代码执行速度。
 
部署
构建过程:
完备JavaScript代码可以用于部署的一件很重要的事情,就是给它开发某些类型的的构建过程。软件开发的典型模式是写代码-编译-测试,即首先书写好代码,将其编译通过,然后运行并确保其正常工作。由于JavaScript并非一个编译型语言,模式变成了写代码-测试,这里你写的代码就是你要在浏览器中测试的代码。这个方法的问题在于他不是最优的,你写的代码不应该原封不动地放入浏览器中,理由如下所示。
 
q 知识产权问题 —— 如果把带有完整注释的代码放到线上,那别人就更容易知道你的意图,对它
再利用,并且可能找到安全漏洞。
q 文件大小 —— 书写代码要保证容易阅读,才能更好地维护,但是这对于性能是不利的。浏览器
并不能从额外的空白字符或者是冗长的函数名和变量名中获得什么好处。
q 代码组织 —— 组织代码要考虑到可维护性并不一定是传送给浏览器的最好方式。
基于这些原因,最好给JavaScript 文件定义一个构建过程。
 
构建过程始于在源控制中定义用于存储文件的逻辑结构。最好避免使用一个文件存放所有的JavaScript,遵循以下面向对象语言中的典型模式:将每个对象或自定义类型分别放入其单独的文件中。这样可以确保每个文件包含最少量的代码,使其在不引入错误的情况下更容易修改。另外,在使用像CVUS或Subversion这类并发源控制系统的时候,这样做也减少了在合并操作中产生冲突的风险。
 
记住将代码分离成多个文件只是为了提高可维护性,并非为了部署。要进行部署的时候,需要将这些源代码合并为一个或几个归并文件。推荐Web应用中尽可能使用最少的JavaScript文件。推荐Web应用中尽可能使用最少的JavaScript文件,是因为HTTP请求时Web中的主要性能瓶颈之一。记住通过<script>标记引用JavaScript文件是一个阻塞操作,当代码下载并运行的时候会停止其他所有的下载。因此,尽量从逻辑上将JavaScript代码分组成部署文件。
 
新兴的API:
requestAniamationFrame():
很长时间以来,计时器和循环间隔一直都是JavaSctipt动画的最核心技术。虽然CSS变换及动画为Web开发人员提供了实现动画的简单手段,但JavaScript动画开发领域的状况这些年来并没有大的变化。Firefox 4最早为JavaScript动画添加了一个新API,即mozRequestAnimationFrame()。这个方法会告诉浏览器:有一个动画开始了。进而 浏览器就可以确定重绘的最佳方式。
 
webkitRequestAnimationFrame与msRequestAnimationFrame:
Chrome和IE10+也都给出了自己的实现。这两个版本与Mozilla的版本有两个方面的微小差异。首先,不会给回调函数传递时间码,因此你无法知道下一次重绘将发生在什么时间。其次,Chrome又增加了第二个可选的参数,即将要发生辩护的DOM元素。知道了重绘将发生在页面中哪个特定元素的区域内,就可以将重绘限定在该区域内。
 
Page Visibility API:
不知道用户是不是在与页面交互,这是困扰广大Web开发人员的一个主要问题。如果页面最小化或者隐藏在了其他标签页后面,那么有些功能是可以停下来的,比如轮询服务器或者某些动画效果。而Page Visibility API(页面可见性API)就是为了让开发人员指导页面是否对用户可见而推出的。
这个API本身非常简单,由以下三部分组成。
q document.hidden:表示页面是否隐藏的布尔值。页面隐藏包括页面在后台标签页中或者浏览
器最小化。
q document.visibilityState:表示下列4 个可能状态的值。
n 页面在后台标签页中或浏览器最小化。
n 页面在前台标签页中。
n 实际的页面已经隐藏,但用户可以看到页面的预览(就像在Windows 7 中,用户把鼠标移动到
任务栏的图标上,就可以显示浏览器中当前页面的预览)。
n 页面在屏幕外执行预渲染处理。
q visibilitychange 事件:当文档从可见变为不可见或从不可见变为可见时,触发该事件
 
 
Geolocation API:
地理定位(geolocation)是最令人兴奋,而且得到了广泛支持的一个新API。通过这套API,JavaScript代码能够访问到用户的当前位置信息。当然,访问之前必须得到用户的明确许可,即统一在页面中共享其位置信息。如果页面尝试访问地理定位信息,浏览器就会显示一个对话框,请求用户许可共享其位置信息。
 
File API:
不能直接访问用户计算机中的文件,一直都是Web应用开发中的一大障碍。2000年以前,处理文件的唯一方式就是再表单中加入<input type="file">字段,仅此而已。File API的宗旨是为Web开发人员提供一种安全的方式,以便在客户端访问用户计算机中的文件,并更好地对这些文件执行操作。
File API在表单中的文件输入字段的基础上,又添加了一些直接访问文件信息的接口。HTML5在DOM中位文件输入元素添加一个files集合。咋通过文件输入字段选择了一或多个文件时,files集合中将包含一组File对象,每个File对象对应着一个文件。
 
Web计时:
页面性能一直都是Web开发人员最关注的领域。但直到最近,度量页面性能指标的唯一方式,就是提高代码复杂程度和巧妙地使用浏览器内部的度量结果,通过直接读取这些信息可以做任何想做的分析。与本章介绍过的其他API不同,Web Timming API实际上已经成为了W3C的建议标准,只不过目前支持它的浏览器还不够多。
Web 计时机制的核心是windo.performance对象。对页面的所有度量信息,包括那些规范中已经定义的和将来才能确定的,都包含在这个对象里面。Web Timing规范一开始就为performance对象定义了两个属性。
 
Web Workers:
随着Web应用复杂性的与日俱增,越来越复杂的计算在所难免。长时间运行的JavaScript进程会导致浏览器冻结用户界面,让人感觉屏幕”冻结“了。Web Workers规范通过让JavaScript在后台运行解决了这个问题。浏览器实现WebWorkers规范的方式有很多种,可以使用线程,后台进程或运行在其他处理器核心上的进程,等等。具体的实现细节其实没有那么重要,重要的是开发人员现在可以放心地运行JavaScript,而不必担心会影响用户体验了。
 
使用Worker:
实例化Worker对象并传入要执行的JavaScript文件名就可以创建一个新的Web Worker。例如:
var worker=new Worker("stufftodo.js");
这行代码会导致浏览器下载stufftodo.js,但只有Worker接收到消息才会实际执行文件中的代码。要给Worker传递消息,可以使用postMessage()方法:
worker.postMessage("start!");
消息内容可以是任何能够被序列化的值,不过与XDM不同的是,在所有支持的浏览器中,postMessage()都能接收对象参数。因此可以随便传递任何形式的对象数据;
 
ECMAScript Harmony:
一般性变化:
Harmny为ECMAScript引入了一些基本的变化。对这门语言来说,这些虽然不算是大的变化,但的确也弥补了它功能上的而一些缺憾。
 
常量:
没有正式的常量是JavaScript的一个明显缺陷。为了弥补这个缺陷,标准制定者为Harmony增加了用Const关键字声明常量的语言。使用方式与var类似,但const声明的变量在初始赋值后,就不能再重新赋值了。来看一个例子。
const MAX_SIZE=25;
可以像声明变量一样在任何地方声明常量。但在同一作用域中,常量名不能与其他变量或函数名重名,因此下列声明会导致错误:
const FLAG=true;
var FLAG=false; //错误!
除了值不能修改之外,可以像使用任何变量一样使用常量。修改常量的值,不会有任何效果,如下所示:
const FLAG=true;
FLAG=false;
alert(FLAG);//true
 
块级作用域及其他作用域:
本书时不时就会提醒读者一句:JavaScript没有块级作用域。换句话说,在语句块中定义的变量与在包含函数中定义的变量共享相同的作用域。Harmony新增了定义块级作用域的语法:使用let关键字。
与const和var类似,可以使用let在任何地方定义变量并为变量赋值。区别在于,使用let定义的变量在定义它的代码之外没有定义。比如说吧,下面是非常常见的代码块:
for(var i=0;i<10;i++){
//执行某些操作
}
alert(i);//10
在上面的代码块中,变量i是作为代码块所在函数的局部变量来声明的,也就是说,在for循环执行完毕后,仍然能够读取i的值。如果在这里使用let代替var,则循环之后,变量i将不复存在。
看下面的例子:
for(let i=0;i<10;i++){
//执行某些操作
}
alert(i); //错误! 变量i没有定义
以上代码执行到最后一行的时候,就会出现错误,因为for循环已结束,变量i就已经没有定义了。因此不能对没有定义的变量执行操作,所以发生错误是自然的。
还有另外一种使用let的方式,即创建let语句,在其中定义只能在后续代码块中使用的变量,像下面的例子这样:
var num=5;
let(num=10,multiplier=2){
alert(num*multiplier); //20
}
alert(num); //5
这是因为let语句创建了自己的作用域,这个作用域里的变量与外面的变量无关。
 
函数:
大多数代码都是以函数方式编写的,因此Harmony从几个方面改进了函数,使其更便于使用。与Harmony中其他部分类似,对函数的改进也集中在开发人员和实现人员共同面临的难题上。
 
剩余参数和分布参数:
Harmony中不再有arguments对象,因此也就无法通过它来读取到未声明的参数。不过,使用剩余参数语法,也能表示你期待给函数传入可变数量的参数。剩余参数的语法形式是三个点后跟一个标识符。使用这种语法可以定义可能会传进来的更多参数,然后把它们收集到一个数组中。
来看例子;
function num(sum,sum2,...sums){
var result=sum+sum2;
for(let i=0,len=nums.length;i<len;i++){
result+=nums[i];
}
return result;
}
 
var result=sum(1,2,3,4,5,6);
 以上代码定义了一个num()函数,接收至少两个参数。这个函数还能接收更多参数,而其余参数都将保存在sunms数组中,与原来的arguments对象不同,剩余参数都保存在Array的一个实例中,因此可以使用任何数组方法来操作它们。另外,即使并没有多余的参数传入函数,剩余参数对象也是Array的实例。
与剩余参数紧密相关的另一种参数语法是分布参数。通过分布参数,可以向函数中传入一个数组,然后数组中的元素会映射到函数的每个参数上。分布参数的语法形式与剩余参数的语法相同,就是再值得前面加三个点。唯一的区别是分布参数在调用参数的时候使用,而剩余参数在定义函数的时候使用。比如,我们可以不诶num函数一个一个地传入参数,而是传入分布参数:
var result=num(...[1,2,3,4,5]);
在这里,我们将一个数组作为分布参数传给了sum()函数。以上代码在功能上与下面这行代码等价:
var result=num.apply(this,[1,2,3,4,5,6]);
 
默认参数值:
ECMAScript函数中的所有参数都是可选的,因为实现不会检查传入的参数数量。不过,除了手工检查传入了哪个参数之外,你还可以为参数制定默认值。如果调用函数时没有传入该参数,那么该参数就会使用默认值。
要为参数制定默认值,可以在参数名后面直接加上等于号和默认值,就像下面这样:
function sum(num1,num2=0){
return num1+num2;
}
var result1=sum(5);
var result2=sum(5,5);
这个sum()函数接收两个参数,但第二个参数是可选的,因为它的默认值为0.使用可选参数的好处是开发人员不用再去检查是否给某个参数传入了值,如果没有的话就使用某个特定的值。默认参数值帮你解除了这个困扰。
 
生成器:
所谓生成器,其实就是一个对象,它每次都能生成一系列值中的一个。对Harmony而言,要创建生成器,可以让函数通过yield操作符返回某个特殊的值。对于使用yield操作符返回值的函数,调用它时就会创建并返回一个新的Generator实例。然后,在这个实例上调用next()方法就能取得生成器的第一个值。此时,执行的是原来的函数,但执行刘到yield语句就会停止,值返回特定的值。从这个角度看,yield与return很相似。如果再次调用next()方法,原来函数中位于yield语句后的代码会继续执行,直到再次遇见yield语句时停止执行,此时再返回一个新值。
function myNumbers(){
for (var i=0; i < 10; i++){
yield i * 2;
}
}
var generator = myNumbers();
try {
while(true){
document.write(generator.next() + "<br />");
}
} catch(ex){
//有意没有写代码
} finally {
generator.close();
}
function myNumbers(){
for (var i=0; i < 10; i++){
yield i * 2;
}
}
var generator = myNumbers();
try {
while(true){
document.write(generator.next() + "<br />");
}
} catch(ex){
//有意没有写代码
} finally {
generator.close();
}
 
调用MyNumbers()函数后,会得到一个生成器。myNumbers()函数本身非常简单,包含一个每次循环都产生一个值的for循环。每次调用next()方法都会执行一次for循环,然后返回下一个值。第一个值是0,第二个值是2,第三个值是4,以此类推,在myNumbers()函数完成推出而没有执行yield语句时(最后一次循环判断i不小于10的时候),生成器会抛出StopIteration错误。因此,为了输出生成器能产生的所有数值,这里用了一个try-catch结构包装了一个while循环,以避免出错时中断代码执行。
如果不再需要某个生成器,最好是调用它的close()方法。这样会执行原始函数的其他部分,包括try-catch相关的finally语句块。
在需要一系列值,而每一个值又与前一个值存在某种关系的情况下,可以使用生成器。
 
posted @ 2016-02-24 15:54  Jeff_lzf  阅读(196)  评论(0)    收藏  举报