<h2 id="9.1">Promise</h2>
Promise是JavaScript异步操作解决方案。介绍Promise之前,先对异步操作做一个详细介绍。
## JavaScript的异步执行
### 概述
Javascript语言的执行环境是"单线程"(single thread)。所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
JavaScript语言本身并不慢,慢的是读写外部数据,比如等待Ajax请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。"同步模式"就是传统做法,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的。这往往用于一些简单的、快速的、不涉及读写的操作。
"异步模式"则完全不同,每一个任务分成两段,第一段代码包含对外部数据的请求,第二段代码被写成一个回调函数,包含了对外部数据的处理。第一段代码执行完,不是立刻执行第二段代码,而是将程序的执行权交给第二个任务。等到外部数据返回了,再由系统通知执行第二段代码。所以,程序的执行顺序与任务的排列顺序是不一致的、异步的。
以下总结了"异步模式"编程的几种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的JavaScript程序。
### 回调函数
回调函数是异步编程最基本的方法。
假定有两个函数f1和f2,后者等待前者的执行结果。
```javascript
f1();
f2();
```
如果`f1`是一个很耗时的任务,可以考虑改写`f1`,把`f2`写成`f1`的回调函数。
```javascript
function f1(callback){
setTimeout(function () {
// f1的任务代码
callback();
}, 1000);
}
```
执行代码就变成下面这样:
```javascript
f1(f2);
```
采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度[耦合](http://en.wikipedia.org/wiki/Coupling_(computer_programming))(Coupling),使得程序结构混乱、流程难以追踪(尤其是回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
### 事件监听
另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的[写法](http://api.jquery.com/on/))。
```javascript
f1.on('done', f2);
```
上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:
```javascript
function f1(){
setTimeout(function () {
// f1的任务代码
f1.trigger('done');
}, 1000);
}
```
上面代码中,`f1.trigger('done')`表示,执行完成后,立即触发`done`事件,从而开始执行`f2`。
这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"[去耦合](http://en.wikipedia.org/wiki/Decoupling)"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。
### 发布/订阅
"事件"完全可以理解成"信号",如果存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"[发布/订阅模式](http://en.wikipedia.org/wiki/Publish-subscribe_pattern)"(publish-subscribe pattern),又称"[观察者模式](http://en.wikipedia.org/wiki/Observer_pattern)"(observer pattern)。
这个模式有多种[实现](http://msdn.microsoft.com/en-us/magazine/hh201955.aspx),下面采用的是Ben Alman的[Tiny Pub/Sub](https://gist.github.com/661855),这是jQuery的一个插件。
首先,f2向"信号中心"jQuery订阅"done"信号。
```javascript
jQuery.subscribe("done", f2);
```
然后,f1进行如下改写:
```javascript
function f1(){
setTimeout(function () {
// f1的任务代码
jQuery.publish("done");
}, 1000);
}
```
jQuery.publish("done")的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
f2完成执行后,也可以取消订阅(unsubscribe)。
```javascript
jQuery.unsubscribe("done", f2);
```
这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
## 异步操作的流程控制
如果有多个异步操作,就存在一个流程控制的问题:确定操作执行的顺序,以后如何保证遵守这种顺序。
```javascript
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function() { callback(arg * 2); }, 1000);
}
```
上面代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数。
如果有6个这样的异步任务,需要全部完成后,才能执行下一步的final函数。
```javascript
function final(value) {
console.log('完成: ', value);
}
```
请问应该如何安排操作流程?
```javascript
async(1, function(value){
async(value, function(value){
async(value, function(value){
async(value, function(value){
async(value, function(value){
async(value, final);
});
});
});
});
});
```
上面代码采用6个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护。
### 串行执行
我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。
```javascript
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function series(item) {
if(item) {
async( item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results);
}
}
series(items.shift());
```
上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。
### 并行执行
流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。
```javascript
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length == items.length) {
final(results);
}
})
});
```
上面代码中,forEach方法会同时发起6个异步任务,等到它们全部完成以后,才会执行final函数。
并行执行的好处是效率较高,比起串行执行一次只能执行一个任务,较为节约时间。但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。
### 并行与串行的结合
所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务。这样就避免了过分占用系统资源。
```javascript
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;
function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final();
}
});
running++;
}
}
launcher();
```
上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。
## Promise对象
### 简介
Promise对象是CommonJS工作组提出的一种规范,目的是为异步操作提供[统一接口](http://wiki.commonjs.org/wiki/Promises/A)。
那么,什么是Promises?
首先,它是一个对象,也就是说与其他JavaScript对象的用法,没有什么两样;其次,它起到代理作用(proxy),充当异步操作与回调函数之间的中介。它使得异步操作具备同步操作的接口,使得程序具备正常的同步运行的流程,回调函数不必再一层层嵌套。
简单说,它的思想是,每一个异步任务立刻返回一个Promise对象,由于是立刻返回,所以可以采用同步操作的流程。这个Promises对象有一个then方法,允许指定回调函数,在异步任务完成后调用。
比如,异步操作`f1`返回一个Promise对象,它的回调函数`f2`写法如下。
```javascript
(new Promise(f1)).then(f2);
```
这种写法对于多层嵌套的回调函数尤其方便。
```javascript
// 传统写法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promises的写法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
```
从上面代码可以看到,采用Promises接口以后,程序流程变得非常清楚,十分易读。
注意,为了便于理解,上面代码的Promise对象的生成格式,做了简化,真正的语法请参照下文。
总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promises规范就是为了解决这个问题而提出的,目标是使用正常的程序流程(同步),来处理异步操作。它先返回一个Promise对象,后面的操作以同步的方式,寄存在这个对象上面。等到异步操作有了结果,再执行前期寄放在它上面的其他操作。
Promises原本只是社区提出的一个构想,一些外部函数库率先实现了这个功能。ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象。
### Promise接口
前面说过,Promise接口的基本思想是,异步任务返回一个Promise对象。
Promise对象只有三种状态。
- 异步操作“未完成”(pending)
- 异步操作“已完成”(resolved,又称fulfilled)
- 异步操作“失败”(rejected)
这三种的状态的变化途径只有两种。
- 异步操作从“未完成”到“已完成”
- 异步操作从“未完成”到“失败”。
这种变化只能发生一次,一旦当前状态变为“已完成”或“失败”,就意味着不会再有新的状态变化了。因此,Promise对象的最终结果只有两种。
- 异步操作成功,Promise对象传回一个值,状态变为`resolved`。
- 异步操作失败,Promise对象抛出一个错误,状态变为`rejected`。
Promise对象使用`then`方法添加回调函数。`then`方法可以接受两个回调函数,第一个是异步操作成功时(变为`resolved`状态)时的回调函数,第二个是异步操作失败(变为`rejected`)时的回调函数(可以省略)。一旦状态改变,就调用相应的回调函数。
```javascript
// po是一个Promise对象
po.then(
console.log,
console.error
);
```
上面代码中,Promise对象`po`使用`then`方法绑定两个回调函数:操作成功时的回调函数`console.log`,操作失败时的回调函数`console.error`(可以省略)。这两个函数都接受异步操作传回的值作为参数。
`then`方法可以链式使用。
```javascript
po
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
```
上面代码中,`po`的状态一旦变为`resolved`,就依次调用后面每一个`then`指定的回调函数,每一步都必须等到前一步完成,才会执行。最后一个`then`方法的回调函数`console.log`和`console.error`,用法上有一点重要的区别。`console.log`只显示回调函数`step3`的返回值,而`console.error`可以显示`step1`、`step2`、`step3`之中任意一个发生的错误。也就是说,假定`step1`操作失败,抛出一个错误,这时`step2`和`step3`都不会再执行了(因为它们是操作成功的回调函数,而不是操作失败的回调函数)。Promises对象开始寻找,接下来第一个操作失败时的回调函数,在上面代码中是`console.error`。这就是说,Promises对象的错误有传递性。
从同步的角度看,上面的代码大致等同于下面的形式。
```javascript
try {
var v1 = step1(po);
var v2 = step2(v1);
var v3 = step3(v2);
console.log(v3);
} catch (error) {
console.error(error);
}
```
### Promise对象的生成
ES6提供了原生的Promise构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。
```javascript
var promise = new Promise(function(resolve, reject) {
// 异步操作的代码
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
```
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject`。它们是两个函数,由JavaScript引擎提供,不用自己部署。
`resolve`函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从`Pending`变为`Resolved`),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;`reject`函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从`Pending`变为`Rejected`),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用`then`方法分别指定`Resolved`状态和`Reject`状态的回调函数。
```javascript
po.then(function(value) {
// success
}, function(value) {
// failure
});
```
### 用法辨析
Promise的用法,简单说就是一句话:使用`then`方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法,它们的差别在哪里?
```javascript
// 写法一
doSomething().then(function () {
return doSomethingElse();
});
// 写法二
doSomething().then(function () {
doSomethingElse();
});
// 写法三
doSomething().then(doSomethingElse());
// 写法四
doSomething().then(doSomethingElse);
```
为了便于讲解,这四种写法都再用`then`方法接一个回调函数`finalHandler`。写法一的`finalHandler`回调函数的参数,是`doSomethingElse`函数的运行结果。
```javascript
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
```
写法二的`finalHandler`回调函数的参数是`undefined`。
```javascript
doSomething().then(function () {
doSomethingElse();
return;
}).then(finalHandler);
```
写法三的`finalHandler`回调函数的参数,是`doSomethingElse`函数返回的回调函数的运行结果。
```javascript
doSomething().then(doSomethingElse())
.then(finalHandler);
```
写法四与写法一只有一个差别,那就是`doSomethingElse`会接收到`doSomething()`返回的结果。
```javascript
doSomething().then(doSomethingElse)
.then(finalHandler);
```
## Promise的应用
### 加载图片
我们可以把图片的加载写成一个`Promise`对象。
```javascript
var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
```
### Ajax操作
Ajax操作是典型的异步操作,传统上往往写成下面这样。
```javascript
function search(term, onload, onerror) {
var xhr, results, url;
url = 'http://example.com/search?q=' + term;
xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
if (this.status === 200) {
results = JSON.parse(this.responseText);
onload(results);
}
};
xhr.onerror = function (e) {
onerror(e);
};
xhr.send();
}
search("Hello World", console.log, console.error);
```
如果使用Promise对象,就可以写成下面这样。
```javascript
function search(term) {
var url = 'http://example.com/search?q=' + term;
var xhr = new XMLHttpRequest();
var result;
var p = new Promise(function (resolve, reject) {
xhr.open('GET', url, true);
xhr.onload = function (e) {
if (this.status === 200) {
result = JSON.parse(this.responseText);
resolve(result);
}
};
xhr.onerror = function (e) {
reject(e);
};
xhr.send();
});
return p;
}
search("Hello World").then(console.log, console.error);
```
加载图片的例子,也可以用Ajax操作完成。
```javascript
function imgLoad(url) {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'blob';
request.onload = function() {
if (request.status === 200) {
resolve(request.response);
} else {
reject(new Error('图片加载失败:' + request.statusText));
}
};
request.onerror = function() {
reject(new Error('发生网络错误'));
};
request.send();
});
}
```
### 小结
Promise对象的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它的一整套接口,可以实现许多强大的功能,比如为多个异步操作部署一个回调函数、为多个回调函数中抛出的错误统一指定处理方法等等。
而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是,编写和理解都相对比较难。
<h2 id="9.2">JavaScript与有限状态机</h2>
## 概述
有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。
简单说,它有三个特征:
- 状态总数(state)是有限的。
- 任一时刻,只处在一种状态之中。
- 某种条件下,会从一种状态转变(transition)到另一种状态。
它对JavaScript的意义在于,很多对象可以写成有限状态机。
举例来说,网页上有一个菜单元素。鼠标点击,菜单显示;鼠标再次点击,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。
代码可以写成下面这样:
```javascript
var menu = {
// 当前状态
currentState: 'hide',
// 绑定事件
initialize: function() {
var self = this;
self.on("click", self.transition);
},
// 状态转换
transition: function(event){
switch(this.currentState) {
case "hide":
this.currentState = 'show';
doSomething();
break;
case "show":
this.currentState = 'hide';
doSomething();
break;
default:
console.log('Invalid State!');
break;
}
}
};
```
可以看到,有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。
另外,JavaScript语言是一种异步操作特别多的语言,常用的解决方法是指定回调函数,但这样会造成代码结构混乱、难以测试和除错等问题。有限状态机提供了更好的办法:把异步操作与对象的状态改变挂钩,当异步操作结束的时候,发生相应的状态改变,由此再触发其他操作。这要比回调函数、事件监听、发布/订阅等解决方案,在逻辑上更合理,更易于降低代码的复杂度。
## Javascript Finite State Machine函数库
下面介绍一个有限状态机的函数库[Javascript Finite State Machine](https://github.com/jakesgordon/javascript-state-machine)。这个库非常好懂,可以帮助我们加深理解,而且功能一点都不弱。
该库提供一个全局对象StateMachine,使用该对象的create方法,可以生成有限状态机的实例。
```javascript
var fsm = StateMachine.create();
```
生成的时候,需要提供一个参数对象,用来描述实例的性质。比如,交通信号灯(红绿灯)可以这样描述:
```javascript
var fsm = StateMachine.create({
initial: 'green',
events: [
{ name: 'warn', from: 'green', to: 'yellow' },
{ name: 'stop', from: 'yellow', to: 'red' },
{ name: 'ready', from: 'red', to: 'yellow' },
{ name: 'go', from: 'yellow', to: 'green' }
]
});
```
交通信号灯的初始状态(initial)为green,events属性是触发状态改变的各种事件,比如warn事件使得green状态变成yellow状态,stop事件使得yellow状态变成red状态等等。
生成实例以后,就可以随时查询当前状态。
- fsm.current :返回当前状态。
- fsm.is(s) :返回一个布尔值,表示状态s是否为当前状态。
- fsm.can(e) :返回一个布尔值,表示事件e是否能在当前状态触发。
- fsm.cannot(e) :返回一个布尔值,表示事件e是否不能在当前状态触发。
Javascript Finite State Machine允许为每个事件指定两个回调函数,以warn事件为例:
- onbefore**warn**:在warn事件发生之前触发。
- onafter**warn**(可简写成onwarn) :在warn事件发生之后触发。
同时,它也允许为每个状态指定两个回调函数,以green状态为例:
- onleave**green** :在离开green状态时触发。
- onenter**green**(可简写成ongreen) :在进入green状态时触发。
假定warn事件使得状态从green变为yellow,上面四类回调函数的发生顺序如下:onbefore**warn** → onleave**green** → onenter**yellow** → onafter**warn**。
除了为每个事件和状态单独指定回调函数,还可以为所有的事件和状态指定通用的回调函数。
- onbeforeevent :任一事件发生之前触发。
- onleavestate :离开任一状态时触发。
- onenterstate :进入任一状态时触发。
- onafterevent :任一事件结束后触发。
如果事件的回调函数里面有异步操作(比如与服务器进行Ajax通信),这时我们可能希望等到异步操作结束,再发生状态改变。这就要用到transition方法。
```javascript
fsm.onleavegreen = function(){
light.fadeOut('slow', function() {
fsm.transition();
});
return StateMachine.ASYNC;
};
```
上面代码的回调函数里面,有一个异步操作(light.fadeOut)。如果不希望状态立即改变,就要让回调函数返回StateMachine.ASYNC,表示状态暂时不改变;等到异步操作结束,再调用transition方法,使得状态发生改变。
Javascript Finite State Machine还允许指定错误处理函数,当发生了当前状态不可能发生的事件时自动触发。
```javascript
var fsm = StateMachine.create({
// ...
error: function(eventName, from, to, args, errorCode, errorMessage) {
return 'event ' + eventName + ': ' + errorMessage;
},
// ...
});
```
比如,当前状态是green,理论上这时只可能发生warn事件。要是这时发生了stop事件,就会触发上面的错误处理函数。
Javascript Finite State Machine的基本用法就是上面这些,更详细的介绍可以参见它的[主页](https://github.com/jakesgordon/javascript-state-machine)。
<h2 id="9.3">MVC框架与Backbone.js</h2>
## MVC框架
随着JavaScript程序变得越来越复杂,往往需要一个团队协作开发,这时代码的模块化和组织规范就变得异常重要了。MVC模式就是代码组织的经典模式。
(……MVC介绍。)
**(1)Model**
Model表示数据层,也就是程序需要的数据源,通常使用JSON格式表示。
**(2)View**
View表示表现层,也就是用户界面,对于网页来说,就是用户看到的网页HTML代码。
**(3)Controller**
Controller表示控制层,用来对原始数据(Model)进行加工,传送到View。
由于网页编程不同于客户端编程,在MVC的基础上,JavaScript社区产生了各种变体框架MVP(Model-View-Presenter)、MVVM(Model-View-ViewModel)等等,有人就把所有这一类框架的各种模式统称为MV*。
框架的优点在于合理组织代码、便于团队合作和未来的维护,缺点在于有一定的学习成本,且限制你只能采取它的写法。
## 零框架解决方案
MVC框架(尤其是大型框架)有一个严重的缺点,就是会产生用户的重度依赖。一旦框架本身出现问题或者停止更新,用户的处境就会很困难,维护和更新成本极高。
ES6的到来,使得JavaScript语言有了原生的模块解决方案。于是,开发者有了另一种选择,就是不使用MVC框架,只使用各种单一用途的模块库,组合完成一个项目。下面是可供选择的各种用途的模块列表。
辅助功能库(Helper Libraries)
- [moment.js](http://momentjs.com/):日期和时间的标准化
- [underscore.js](http://underscorejs.org/) / [Lo-Dash](https://lodash.com/):一系列函数式编程的功能函数
路由库(Routing)
- [router.js](https://github.com/tildeio/router.js/):Ember.js使用的路由库
- [route-recognizer](https://github.com/tildeio/route-recognizer):功能全面的路由库
- [page.js](https://github.com/visionmedia/page.js):类似Express路由的库
- [director](https://github.com/flatiron/director):同时支持服务器和浏览器的路由库
Promise库
- [RSVP.js](https://github.com/tildeio/rsvp.js):ES6兼容的Promise库
- [ES6-Promise](https://github.com/jakearchibald/es6-promise):RSVP.js的子集,但是全面兼容ES6
- [q](https://github.com/kriskowal/q):最常用的Promise库之一,AngularJS用了它的精简版
- [native-promise-only](https://github.com/getify/native-promise-only):严格符合ES6的Promise标准,同时兼容老式浏览器
客户端与服务器的通信库
- [fetch](https://github.com/github/fetch):实现window.fetch功能
- [qwest](https://github.com/pyrsmk/qwest):支持XHR2和Promise的Ajax库
- [jQuery](https://github.com/jquery/jquery):jQuery 2.0支持按模块打包,因此可以创建一个纯Ajax功能库
动画库(Animation)
- [cssanimevent](https://github.com/magnetikonline/cssanimevent):兼容老式浏览器的CSS3动画库
- [Velocity.js](http://julian.com/research/velocity/):性能优秀的动画库
辅助开发库(Development Assistance)
- [LogJS](https://github.com/bfattori/LogJS):轻量级的logging功能库
- [UserTiming.js](https://github.com/nicjansma/usertiming.js):支持老式浏览器的高精度时间戳库
流程控制和架构(Flow Control/Architecture)
- [ondomready](https://github.com/tubalmartin/ondomready):类似jQuery的ready()方法,符合AMD规范
- [script.js](https://github.com/ded/script.js]):异步的脚本加载和依赖关系管理库
- [async](https://github.com/caolan/async):浏览器和node.js的异步管理工具库
- [Virtual DOM](https://github.com/Matt-Esch/virtual-dom):react.js的一个替代方案,参见[Virtual DOM and diffing algorithm](https://gist.github.com/Raynos/8414846)
数据绑定(Data-binding)
- Object.observe():Chrome已经支持该方法,可以轻易实现双向数据绑定
模板库(Templating)
- [Mustache](http://mustache.github.io/):大概是目前使用最广的不含逻辑的模板系统
微框架(Micro-Framework)
某些情况下,可以使用微型框架,作为项目开发的起点。
- [bottlejs](https://github.com/young-steveo/bottlejs):提供惰性加载、中间件钩子、装饰器等功能
- [Stapes.js](http://hay.github.io/stapes/#top):微型MVC框架
- [soma.js](http://somajs.github.io/somajs/site/):提供一个松耦合、易测试的架构
- [knockout](http://knockoutjs.com/):最流行的微框架之一,主要关注UI
## Backbone的加载
```html
<script src="/javascripts/lib/jquery.js"></script>
<script src="/javascripts/lib/underscore.js"></script>
<script src="/javascripts/lib/backbone.js"></script>
<script src="/javascripts/jst.js"></script>
<script src="/javascripts/router.js"></script>
<script src="/javascripts/init.js"></script>
```
## Backbone的用法
Backbone是最早的JavaScript MVC框架,也是最简化的一个框架。它的设计思想是,只提供最基本的功能,给用户提供最大的自由。这意味着,好的一面是它没有一整套规则,强制你接受,坏的一面是很多功能你必须自己实现。Backbone的体积相当小,最小化后只有30多KB。
定义一个对象,表示Web应用。
```javascript
var AppName = {
Models :{},
Views :{},
Collections :{},
Controllers :{}
};
```
上面代码表示,应用由四部分组成:Model、Collection、Controller和View。
定义Model,表示数据的一个基本单位。
```javascript
AppName.Models.Person = Backbone.Model.extend({
urlRoot: "/persons"
});
```
定义Collection,表示Model的集合。
```javascript
AppName.Collections.Library = Backbone.Collection.extend({
model: AppName.Models.Book
});
```
上面代码表示,Collection对象必须有model属性,指明由哪一个model构成。
定义一个View。
```javascript
AppName.Views.Modals.AcceptDecline = Backbone.View.Extend({
el: ".modal-accept",
events: {
"ajax:success .link-accept" :"acceptSuccess",
"ajax:error .link-accept" :"acceptError"
},
acceptSuccess :function(evt, response) {
this.$el.modal("hide");
alert('Cool! Thanks');
},
acceptError :function(evt, response) {
var $modalContent = this.$el.find('.panel-modal');
$modalContent.append("Something was wrong!");
}
});
```
View对象必须有el属性,指明当前View绑定的DOM节点,events属性指明事件和对应的方法。
定义一个Controller。
```javascript
AppName.Controllers.Person = {};
AppName.Controllers.Person.show = function(id) {
var aMa = new AppName.Models.Person({id: id});
aMa.updateAge(25);
aMa.fetch().done(function(){
var view = new AppName.Views.Show({model: aMa});
});
};
```
最后,定义路由,启动应用程序。
```javascript
var Workspace = Backbone.Router.extend({
routes: {
"*" :"wholeApp",
"users/:id" :"usersShow",
"users/:id/orders/" :"ordersIndex"
},
wholeApp :AppName.Controller.Application.default,
usersShow :AppName.Controller.Users.show,
ordersIndex :AppName.Controller.Orders.index
});
new Workspace();
Backbone.history.start({pushState: true});
```
## Backbone.View
### 基本用法
Backbone.View方法用于定义视图类。
```javascrip
var AppView = Backbone.View.extend({
render: function(){
$('main').append('<h1>一级标题</h1>');
}
});
```
上面代码通过Backbone.View的extend方法,定义了一个视图类AppView。该类内部有一个render方法,用于将视图放置在网页上。
使用的时候,需要先新建视图类的实例,然后通过实例,调用render方法,从而让视图在网页上显示。
```javascript
var appView = new AppView();
appView.render();
```
上面代码新建视图类AppView的实例appView,然后调用appView.render,网页上就会显示指定的内容。
新建视图实例时,通常需要指定Model。
```javascript
var document = new Document({
model: doc
});
```
### initialize方法
视图还可以定义initialize方法,生成实例的时候,会自动调用该方法对实例初始化。
```javascript
var AppView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
$('main').append('<h1>一级标题</h1>');
}
});
var appView = new AppView();
```
上面代码定义了initialize方法之后,就省去了生成实例后,手动调用appView.render()的步骤。
### el属性,$el属性
除了直接在render方法中,指定“视图”所绑定的网页元素,还可以用视图的el属性指定网页元素。
```javascript
var AppView = Backbone.View.extend({
el: $('main'),
render: function(){
this.$el.append('<h1>一级标题</h1>');
}
});
```
上面的代码与render方法直接绑定网页元素,效果完全一样。上面代码中,除了el属性,还是$el属性,前者代表指定的DOM元素,后者则表示该DOM元素对应的jQuery对象。
### tagName属性,className属性
如果不指定el属性,也可以通过tagName属性和className属性指定。
```javascript
var Document = Backbone.View.extend({
tagName: "li",
className: "document",
render: function() {
// ...
}
});
```
### template方法
视图的template属性用来指定网页模板。
```javascript
var AppView = Backbone.View.extend({
template: _.template("<h3>Hello <%= who %><h3>"),
});
```
上面代码中,underscore函数库的template函数,接受一个模板字符串作为参数,返回对应的模板函数。有了这个模板函数,只要提供具体的值,就能生成网页代码。
```javascript
var AppView = Backbone.View.extend({
el: $('#container'),
template: _.template("<h3>Hello <%= who %><h3>"),
initialize: function(){
this.render();
},
render: function(){
this.$el.html(this.template({who: 'world!'}));
}
});
```
上面代码的render就调用了template方法,从而生成具体的网页代码。
实际应用中,一般将模板放在script标签中,为了防止浏览器按照JavaScript代码解析,type属性设为text/template。
```html
<script type="text/template" data-name="templateName">
<!-- template contents goes here -->
</script>
```
可以使用下面的代码编译模板。
```javascript
window.templates = {};
var $sources = $('script[type="text/template"]');
$sources.each(function(index, el) {
var $el = $(el);
templates[$el.data('name')] = _.template($el.html());
});
```
### events属性
events属性用于指定视图的事件及其对应的处理函数。
```javascript
var Document = Backbone.View.extend({
events: {
"click .icon": "open",
"click .button.edit": "openEditDialog",
"click .button.delete": "destroy"
}
});
```
上面代码中一个指定了三个CSS选择器的单击事件,及其对应的三个处理函数。
### listento方法
listento方法用于为特定事件指定回调函数。
```javascript
var Document = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render);
}
});
```
上面代码为model的change事件,指定了回调函数为render。
### remove方法
remove方法用于移除一个视图。
```javascript
updateView: function() {
view.remove();
view.render();
};
```
### 子视图(subview)
在父视图中可以调用子视图。下面就是一种写法。
```javascript
render : function (){
this.$el.html(this.template());
this.child = new Child();
this.child.appendTo($.('.container-placeholder').render();
}
```
## Backbone.Events
`Backbone.Events`是一个事件对象。任何继承了这个对象的对象,都具备了`Backbone.Events`的事件接口,可以调用on和trigger方法,发布和订阅消息。
```javascript
var EventChannel = _.extend({}, Backbone.Events);
```
下面是一些例子。
```javascript
var channel = $.extend( {}, Backbone.Events );
channel.on('remove-node', function(msg) {
// code to remove the node
});
channel.trigger( 'remove-node', msg );
// 'msg' can be everything: String, number, object and so forth
// also we can pass more than one message like the example below
channel.on('add-node', function(node, callback) {
// code to add a new node
callback();
} );
channel.trigger('add-node', {
label: 'I am a new node',
color: 'black'
}, function() {
console.log( 'I am a callback' );
});
```
## Backbone.Router
Router是Backbone提供的路由对象,用来将用户请求的网址与后端的处理函数一一对应。
首先,新定义一个Router类。
```javascript
Router = Backbone.Router.extend({
routes: {
}
});
```
## routes属性
Backbone.Router对象中,最重要的就是routes属性。它用来设置路径的处理方法。
routes属性是一个对象,它的每个成员就代表一个路径处理规则,键名为路径规则,键值为处理方法。
如果键名为空字符串,就代表根路径。
```javascript
routes: {
'': 'phonesIndex',
},
phonesIndex: function () {
new PhonesIndexView({ el: 'section#main' });
}
```
星号代表任意路径,可以设置路径参数,捕获具体的路径值。
```javascript
var AppRouter = Backbone.Router.extend({
routes: {
"*actions": "defaultRoute"
}
});
var app_router = new AppRouter;
app_router.on('route:defaultRoute', function(actions) {
console.log(actions);
})
```
上面代码中,根路径后面的参数,都会被捕获,传入回调函数。
路径规则的写法。
```javascript
var myrouter = Backbone.Router.extend({
routes: {
"help": "help",
"search/:query": "search"
},
help: function() {
...
},
search: function(query) {
...
}
});
routes: {
"help/:page": "help",
"download/*path": "download",
"folder/:name": "openFolder",
"folder/:name-:mode": "openFolder"
}
router.on("route:help", function(page) {
...
});
```
## Backbone.history
设置了router以后,就可以启动应用程序。Backbone.history对象用来监控url的变化。
```javascript
App = new Router();
$(document).ready(function () {
Backbone.history.start({ pushState: true });
});
```
打开pushState方法。如果应用程序不在根目录,就需要指定根目录。
```javascript
Backbone.history.start({pushState: true, root: "/public/search/"})
```
## Backbone.Model
Model代表单个的对象实体。
```javascript
var User = Backbone.Model.extend({
defaults: {
name: '',
email: ''
}
});
var user = new User();
```
上面代码使用extend方法,生成了一个User类,它代表model的模板。然后,使用new命令,生成一个Model的实例。defaults属性用来设置默认属性,上面代码表示user对象默认有name和email两个属性,它们的值都等于空字符串。
生成实例时,可以提供各个属性的具体值。
```javascript
var user = new User ({
id: 1,
name: 'name',
email: 'name@email.com'
});
```
上面代码在生成实例时,提供了各个属性的具体值。
### idAttribute属性
Model实例必须有一个属性,作为区分其他实例的主键。这个属性的名称,由idAttribute属性设定,一般是设为id。
```javascript
var Music = Backbone.Model.extend({
idAttribute: 'id'
});
```
### get方法
get方法用于返回Model实例的某个属性的值。
```javascript
var user = new User({ name: "name", age: 24});
var age = user.get("age"); // 24
var name = user.get("name"); // "name"
```
### set方法
set方法用于设置Model实例的某个属性的值。
```javascript
var User = Backbone.Model.extend({
buy: function(newCarsName){
this.set({car: newCarsName });
}
});
var user = new User({name: 'BMW',model:'i8',type:'car'});
user.buy('Porsche');
var car = user.get("car"); // ‘Porsche’
```
### on方法
on方法用于监听对象的变化。
```javascript
var user = new User({name: 'BMW',model:'i8'});
user.on("change:name", function(model){
var name = model.get("name"); // "Porsche"
console.log("Changed my car’s name to " + name);
});
user.set({name: 'Porsche'});
// Changed my car’s name to Porsche
```
上面代码中的on方法用于监听事件,“change:name”表示name属性发生变化。
### urlroot属性
该属性用于指定服务器端对model进行操作的路径。
```javascript
var User = Backbone.Model.extend({
urlRoot: '/user'
});
```
上面代码指定,服务器对应该Model的路径为/user。
### fetch事件
fetch事件用于从服务器取出Model。
```javascript
var user = new User ({id: 1});
user.fetch({
success: function (user){
console.log(user.toJSON());
}
})
```
上面代码中,user实例含有id属性(值为1),fetch方法使用HTTP动词GET,向网址“/user/1”发出请求,从服务器取出该实例。
### save方法
save方法用于通知服务器新建或更新Model。
如果一个Model实例不含有id属性,则save方法将使用POST方法新建该实例。
```javascript
var User = Backbone.Model.extend({
urlRoot: '/user'
});
var user = new User ();
var userDetails = {
name: 'name',
email: 'name@email.com'
};
user.save(userDetails, {
success: function (user) {
console.log(user.toJSON());
}
})
```
上面代码先在类中指定Model对应的网址是/user,然后新建一个实例,最后调用save方法。它有两个参数,第一个是实例对象的具体属性,第二个参数是一个回调函数对象,设定success事件(保存成功)的回调函数。具体来说,save方法会向/user发出一个POST请求,并将{name: 'name', email: 'name@email.com'}作为数据提供。
如果一个Model实例含有id属性,则save方法将使用PUT方法更新该实例。
```javascript
var user = new User ({
id: 1,
name: '张三',
email: 'name@email.com'
});
user.save({name: '李四'}, {
success: function (model) {
console.log(user.toJSON());
}
});
```
上面代码中,对象实例含有id属性(值为1),save将使用PUT方法向网址“/user/1”发出请求,从而更新该实例。
### destroy方法
destroy方法用于在服务器上删除该实例。
```javascript
var user = new User ({
id: 1,
name: 'name',
email: 'name@email.com'
});
user.destroy({
success: function () {
console.log('Destroyed');
}
});
```
上面代码的destroy方法,将使用HTTP动词DELETE,向网址“/user/1”发出请求,删除对应的Model实例。
## Backbone.Collection
Collection是同一类Model的集合,比如Model是动物,Collection就是动物园;Model是单个的人,Collection就是一家公司。
```javascript
var Song = Backbone.Model.extend({});
var Album = Backbone.Collection.extend({
model: Song
});
```
上面代码中,Song是Model,Album是Collection,而且Album有一个model属性等于Song,因此表明Album是Song的集合。
### add方法,remove方法
Model的实例可以直接放入Collection的实例,也可以用add方法添加。
```javascript
var song1 = new Song({ id: 1 ,name: "歌名1", artist: "张三" });
var song2 = new Music ({id: 2,name: "歌名2", artist: "李四" });
var myAlbum = new Album([song1, song2]);
var song3 = new Music({ id: 3, name: "歌名3",artist:"赵五" });
myAlbum.add(song3);
```
remove方法用于从Collection实例中移除一个Model实例。
```javascript
myAlbum.remove(1);
```
上面代码表明,remove方法的参数是model实例的id属性。
### get方法,set方法
get方法用于从Collection中获取指定id的Model实例。
```javascript
myAlbum.get(2))
```
### fetch方法
fetch方法用于从服务器取出Collection数据。
```javascript
var songs = new Backbone.Collection;
songs.url = '/songs';
songs.fetch();
```
## Backbone.events
```javascript
var obj = {};
_.extend(obj, Backbone.Events);
obj.on("show-message", function(msg) {
$('#display').text(msg);
});
obj.trigger("show-message", "Hello World");
```
<h2 id="9.4">严格模式</h2>
## 概述
### 设计目的
除了正常运行模式,ECMAScript 5添加了第二种运行模式:“严格模式”(strict mode)。顾名思义,这种模式使得JavaScript在更严格的条件下运行。
设立”严格模式“的目的,主要有以下几个:
- 消除JavaScript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的JavaScript做好铺垫。
“严格模式”体现了JavaScript更合理、更安全、更严谨的发展方向。
同样的代码,在”正常模式“和”严格模式“中,可能会有不一样的运行结果。一些在"正常模式"下可以运行的语句,在"严格模式"下将不能运行。掌握这些内容,有助于更细致深入地理解JavaScript,让你变成一个更好的程序员。
### 启用方法
进入“严格模式”的标志,是一行字符串`use strict`。
```javascript
'use strict';
```
老版本的浏览器会把它当作一行普通字符串,加以忽略。新版本的浏览器就会进入严格模式。
“严格模式”可以用于整个脚本,也可以只用于单个函数。
**(1) 针对整个脚本文件**
将`use strict`放在脚本文件的第一行,则整个脚本都将以“严格模式”运行。如果这行语句不在第一行就无效,整个脚本会以“正常模式”运行。(严格地说,只要前面不是产生实际运行结果的语句,`use strict`可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面。)
```html
<script>
'use strict';
console.log('这是严格模式');
</script>
<script>
console.log('这是正常模式');
</script>
```
上面的代码表示,一个网页文件中依次有两段JavaScript代码。前一个`<script>`标签是严格模式,后一个不是。
如果字符串`use strict`出现在代码中间,则不起作用,即严格模式必须从代码一开始就生效。
两个不同模式的脚本合并成一个文件,如果严格模式的脚本在前,则合并后的脚本都是”严格模式“;如果正常模式的脚本在前,则合并后的脚本都是”正常模式“。总之,这两种情况下,合并后的结果都是不正确的。因此,建议在多个脚本需要合并的场合,”严格模式“只在函数中打开,不针对整个脚本打开。
**(2)针对单个函数**
将“use strict”放在函数体的第一行,则整个函数以“严格模式”运行。
```javascript
function strict() {
'use strict';
return '这是严格模式';
}
function notStrict() {
return '这是正常模式';
}
```
**(3)脚本文件的变通写法**
因为在脚本文件第一行放置`use strict`不利于文件合并,所以更好的做法是,借用第二种方法,将整个脚本文件放在一个立即执行的匿名函数之中。
```javascript
(function () {
"use strict";
// some code here
})();
```
## 显式报错
严格模式使得JavaScript的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错。
### 字符串的length属性不可写
严格模式下,设置字符串的`length`属性,会报错。
```javascript
'use strict';
'abc'.length = 5;
```
实际上,严格模式下,对只读属性赋值,或者删除不可配置(nonconfigurable)属性都会报错。
### eval、arguments不可用作函数名
使用`eval`,或者在函数内部使用`arguments`,作为标识名,将会报错。
下面的语句都会报错。
```javascript
'use strict';
eval = 17;
arguments++;
++eval;
var obj = { set p(arguments) { } };
var eval;
try { } catch (arguments) { }
function x(eval) { }
function arguments() { }
var y = function eval() { };
var f = new Function("arguments", "'use strict'; return 17;");
```
### 只读属性不可写
正常模式下,对一个对象的只读属性进行赋值,不会报错,只会默默地失败。严格模式下,将报错。
```javascript
'use strict';
var o = {};
Object.defineProperty(o, 'v', { value: 1, writable: false });
o.v = 2; // 报错
```
### 只设置了赋值器的属性不可写
严格模式下,对一个只设置了赋值器(getter)的属性赋值,会报错。
```javascript
"use strict";
var o = {
get v() { return 1; }
};
o.v = 2; // 报错
```
### 禁止扩展的对象不可扩展
严格模式下,对禁止扩展的对象添加新属性,会报错。
```javascript
'use strict';
var o = {};
Object.preventExtensions(o);
o.v = 1; // 报错
```
### 禁止删除不可删除的属性
严格模式下,删除一个不可删除的属性,会报错。
```javascript
'use strict';
delete Object.prototype; // 报错
```
### 函数不能有重名的参数
正常模式下,如果函数有多个重名的参数,可以用`arguments[i]`读取。严格模式下,这属于语法错误。
```javascript
function f(a, a, b) { // 语法错误
'use strict';
return a + b;
}
```
### 禁止八进制的前缀0表示法
正常模式下,整数的第一位如果是`0`,表示这是八进制数,比如`0100`等于十进制的64。严格模式禁止这种表示法,整数第一位为`0`,将报错。
```javascript
"use strict";
var n = 0100; // SyntaxError
```
## 增强的安全措施
严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误。
### 全局变量显式声明
在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明。
```javascript
'use strict';
v = 1; // 报错,v未声明
for (i = 0; i < 2; i++) { // 报错,i未声明
// ...
}
function f() {
x = 123;
}
f() // 报错,未声明就创建一个全局变量
```
因此,严格模式下,变量都必须先用`var`命令声明,然后再使用。
### 禁止this关键字指向全局对象
正常模式下,函数内部的`this`可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量。
```javascript
// 正常模式
function f() {
console.log(this === window);
}
f() // true
// 严格模式
function f() {
'use strict';
console.log(this === undefined);
}
f() // true
```
这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加`new`,这时`this`不再指向全局对象,而是报错。
```javascript
function f() {
'use strict';
this.a = 1;
};
f();// 报错,this未定义
```
严格模式下,函数直接调用时(不使用`new`调用),函数内部的`this`表示`undefined`,因此可以用`call`、`apply`和`bind`方法,将任意值绑定在`this`上面。
```javascript
'use strict';
function fun() {
return this;
}
fun() //undefined
fun.call(2) // 2
fun.apply(null) // null
fun.call(undefined) // undefined
fun.bind(true)() // true
```
### 禁止使用fn.callee、fn.caller
函数内部不得使用`fn.caller`、`fn.arguments`,否则会报错。这意味着不能在函数内部得到调用栈了。
```javascript
function f1() {
'use strict';
f1.caller; // 报错
f1.arguments; // 报错
}
f1();
```
### 禁止使用arguments.callee、arguments.caller
`arguments.callee`和`arguments.caller`是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用`arguments.callee`、`arguments.caller`将会报错。
```javascript
'use strict';
var f = function() {
return arguments.callee;
};
f(); // 报错
```
### 禁止删除变量
严格模式下无法删除变量,如果使用`delete`命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的`configurable`属性设置为true,才能被`delete`命令删除。
```javascript
"use strict";
var x;
delete x; // 语法错误
var o = Object.create(null, {
x: {
value: 1,
configurable: true
}
});
delete o.x; // 删除成功
```
## 静态绑定
JavaScript语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。
严格模式对动态绑定做了一些限制。某些情况下,只允许静态绑定。也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。
具体来说,涉及以下几个方面。
### 禁止使用with语句
严格模式下,使用`with`语句将报错。因为`with`语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果。
```javascript
'use strict';
var v = 1;
with (o) { // SyntaxError
v = 2;
}
```
### 创设eval作用域
正常模式下,JavaScript语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:`eval`作用域。
正常模式下,`eval`语句的作用域,取决于它处于全局作用域,还是函数作用域。严格模式下,`eval`语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,`eval`所生成的变量只能用于`eval`内部。
```javascript
(function () {
'use strict';
var x = 2;
console.log(eval('var x = 5; x')) // 5
console.log(x) // 2
})()
```
注意,如果希望`eval`语句也使用严格模式,有两种方式。
```javascript
// 方式一
function f1(str){
'use strict';
return eval(str);
}
f1('undeclared_variable = 1'); // 报错
// 方式二
function f2(str){
return eval(str);
}
f2('"use strict";undeclared_variable = 1') // 报错
```
上面两种写法,`eval`内部使用的都是严格模式。
### arguments不再追踪参数的变化
变量`arguments`代表函数的参数。严格模式下,函数内部改变参数与`arguments`的联系被切断了,两者不再存在联动关系。
```javascript
function f(a) {
a = 2;
return [a, arguments[0]];
}
f(1); // 正常模式为[2, 2]
function f(a) {
"use strict";
a = 2;
return [a, arguments[0]];
}
f(1); // 严格模式为[2, 1]
```
上面代码中,改变函数的参数,不会反应到`arguments`对象上来。
## 向下一个版本的JavaScript过渡
JavaScript语言的下一个版本是ECMAScript 6,为了平稳过渡,严格模式引入了一些ES6语法。
### 函数必须声明在顶层
JavaScript的新版本ES6会引入“块级作用域”。为了与新版本接轨,严格模式只允许在全局作用域或函数作用域的顶层声明函数。也就是说,不允许在非函数的代码块内声明函数。
```javascript
"use strict";
if (true) {
function f1() { } // 语法错误
}
for (var i = 0; i < 5; i++) {
function f2() { } // 语法错误
}
```
上面代码在`if`代码块和`for`代码块中声明了函数,在严格模式下都会报错。
### 保留字
为了向将来JavaScript的新版本过渡,严格模式新增了一些保留字:implements, interface, let, package, private, protected, public, static, yield。
使用这些词作为变量名将会报错。
```javascript
function package(protected) { // 语法错误
'use strict';
var implements; // 语法错误
}
```
此外,ECMAscript第五版本身还规定了另一些保留字(`class`, `enum`, `export`, `extends`, `import`, `super`),以及各大浏览器自行增加的`const`保留字,也是不能作为变量名的。