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中的变量是松散类型(即弱类型)的,能够用来保存任何类型的数据。php
typeof 能够用来检测给定变量的数据类型,可能的返回值:1. 'undefined' --- 这个值未定义;html
2. 'boolean' --- 这个值是布尔值;程序员
3. 'string' --- 这个值是字符串;web
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("You
’
re 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脚本就是采用流实现的浏览器中常见的形式。
$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);
}
}
单一的全局量的延伸即是命名空间的概念,由
YUI
(
Yahoo! 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语句块。
在须要一系列值,而每个值又与前一个值存在某种关系的状况下,能够使用生成器。