<h2 id="9.1">Promise</h2>
Promise是JavaScript異步操作解決方案。介紹Promise之前,先對異步操作做一個詳細介紹。
## JavaScript的異步執(zhí)行
### 概述
Javascript語言的執(zhí)行環(huán)境是"單線程"(single thread)。所謂"單線程",就是指一次只能完成一件任務(wù)。如果有多個任務(wù),就必須排隊,前面一個任務(wù)完成,再執(zhí)行后面一個任務(wù)。
這種模式的好處是實現(xiàn)起來比較簡單,執(zhí)行環(huán)境相對單純;壞處是只要有一個任務(wù)耗時很長,后面的任務(wù)都必須排隊等著,會拖延整個程序的執(zhí)行。常見的瀏覽器無響應(yīng)(假死),往往就是因為某一段Javascript代碼長時間運行(比如死循環(huán)),導(dǎo)致整個頁面卡在這個地方,其他任務(wù)無法執(zhí)行。
JavaScript語言本身并不慢,慢的是讀寫外部數(shù)據(jù),比如等待Ajax請求返回結(jié)果。這個時候,如果對方服務(wù)器遲遲沒有響應(yīng),或者網(wǎng)絡(luò)不通暢,就會導(dǎo)致腳本的長時間停滯。
為了解決這個問題,Javascript語言將任務(wù)的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。"同步模式"就是傳統(tǒng)做法,后一個任務(wù)等待前一個任務(wù)結(jié)束,然后再執(zhí)行,程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的。這往往用于一些簡單的、快速的、不涉及讀寫的操作。
"異步模式"則完全不同,每一個任務(wù)分成兩段,第一段代碼包含對外部數(shù)據(jù)的請求,第二段代碼被寫成一個回調(diào)函數(shù),包含了對外部數(shù)據(jù)的處理。第一段代碼執(zhí)行完,不是立刻執(zhí)行第二段代碼,而是將程序的執(zhí)行權(quán)交給第二個任務(wù)。等到外部數(shù)據(jù)返回了,再由系統(tǒng)通知執(zhí)行第二段代碼。所以,程序的執(zhí)行順序與任務(wù)的排列順序是不一致的、異步的。
以下總結(jié)了"異步模式"編程的幾種方法,理解它們可以讓你寫出結(jié)構(gòu)更合理、性能更出色、維護更方便的JavaScript程序。
### 回調(diào)函數(shù)
回調(diào)函數(shù)是異步編程最基本的方法。
假定有兩個函數(shù)f1和f2,后者等待前者的執(zhí)行結(jié)果。
```javascript
f1();
f2();
```
如果`f1`是一個很耗時的任務(wù),可以考慮改寫`f1`,把`f2`寫成`f1`的回調(diào)函數(shù)。
```javascript
function f1(callback){
setTimeout(function () {
// f1的任務(wù)代碼
callback();
}, 1000);
}
```
執(zhí)行代碼就變成下面這樣:
```javascript
f1(f2);
```
采用這種方式,我們把同步操作變成了異步操作,f1不會堵塞程序運行,相當(dāng)于先執(zhí)行程序的主要邏輯,將耗時的操作推遲執(zhí)行。
回調(diào)函數(shù)的優(yōu)點是簡單、容易理解和部署,缺點是不利于代碼的閱讀和維護,各個部分之間高度[耦合](http://en.wikipedia.org/wiki/Coupling_(computer_programming))(Coupling),使得程序結(jié)構(gòu)混亂、流程難以追蹤(尤其是回調(diào)函數(shù)嵌套的情況),而且每個任務(wù)只能指定一個回調(diào)函數(shù)。
### 事件監(jiān)聽
另一種思路是采用事件驅(qū)動模式。任務(wù)的執(zhí)行不取決于代碼的順序,而取決于某個事件是否發(fā)生。
還是以f1和f2為例。首先,為f1綁定一個事件(這里采用的jQuery的[寫法](http://api.jquery.com/on/))。
```javascript
f1.on('done', f2);
```
上面這行代碼的意思是,當(dāng)f1發(fā)生done事件,就執(zhí)行f2。然后,對f1進行改寫:
```javascript
function f1(){
setTimeout(function () {
// f1的任務(wù)代碼
f1.trigger('done');
}, 1000);
}
```
上面代碼中,`f1.trigger('done')`表示,執(zhí)行完成后,立即觸發(fā)`done`事件,從而開始執(zhí)行`f2`。
這種方法的優(yōu)點是比較容易理解,可以綁定多個事件,每個事件可以指定多個回調(diào)函數(shù),而且可以"[去耦合](http://en.wikipedia.org/wiki/Decoupling)"(Decoupling),有利于實現(xiàn)模塊化。缺點是整個程序都要變成事件驅(qū)動型,運行流程會變得很不清晰。
### 發(fā)布/訂閱
"事件"完全可以理解成"信號",如果存在一個"信號中心",某個任務(wù)執(zhí)行完成,就向信號中心"發(fā)布"(publish)一個信號,其他任務(wù)可以向信號中心"訂閱"(subscribe)這個信號,從而知道什么時候自己可以開始執(zhí)行。這就叫做"[發(fā)布/訂閱模式](http://en.wikipedia.org/wiki/Publish-subscribe_pattern)"(publish-subscribe pattern),又稱"[觀察者模式](http://en.wikipedia.org/wiki/Observer_pattern)"(observer pattern)。
這個模式有多種[實現(xiàn)](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的任務(wù)代碼
jQuery.publish("done");
}, 1000);
}
```
jQuery.publish("done")的意思是,f1執(zhí)行完成后,向"信號中心"jQuery發(fā)布"done"信號,從而引發(fā)f2的執(zhí)行。
f2完成執(zhí)行后,也可以取消訂閱(unsubscribe)。
```javascript
jQuery.unsubscribe("done", f2);
```
這種方法的性質(zhì)與"事件監(jiān)聽"類似,但是明顯優(yōu)于后者。因為我們可以通過查看"消息中心",了解存在多少信號、每個信號有多少訂閱者,從而監(jiān)控程序的運行。
## 異步操作的流程控制
如果有多個異步操作,就存在一個流程控制的問題:確定操作執(zhí)行的順序,以后如何保證遵守這種順序。
```javascript
function async(arg, callback) {
console.log('參數(shù)為 ' + arg +' , 1秒后返回結(jié)果');
setTimeout(function() { callback(arg * 2); }, 1000);
}
```
上面代碼的async函數(shù)是一個異步任務(wù),非常耗時,每次執(zhí)行需要1秒才能完成,然后再調(diào)用回調(diào)函數(shù)。
如果有6個這樣的異步任務(wù),需要全部完成后,才能執(zhí)行下一步的final函數(shù)。
```javascript
function final(value) {
console.log('完成: ', value);
}
```
請問應(yīng)該如何安排操作流程?
```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個回調(diào)函數(shù)的嵌套,不僅寫起來麻煩,容易出錯,而且難以維護。
### 串行執(zhí)行
我們可以編寫一個流程控制函數(shù),讓它來控制異步任務(wù),一個任務(wù)完成以后,再執(zhí)行另一個。這就叫串行執(zhí)行。
```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());
```
上面代碼中,函數(shù)series就是串行函數(shù),它會依次執(zhí)行異步任務(wù),所有任務(wù)都完成后,才會執(zhí)行final函數(shù)。items數(shù)組保存每一個異步任務(wù)的參數(shù),results數(shù)組保存每一個異步任務(wù)的運行結(jié)果。
### 并行執(zhí)行
流程控制函數(shù)也可以是并行執(zhí)行,即所有異步任務(wù)同時執(zhí)行,等到全部完成以后,才執(zhí)行final函數(shù)。
```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方法會同時發(fā)起6個異步任務(wù),等到它們?nèi)客瓿梢院?,才會?zhí)行final函數(shù)。
并行執(zhí)行的好處是效率較高,比起串行執(zhí)行一次只能執(zhí)行一個任務(wù),較為節(jié)約時間。但是問題在于如果并行的任務(wù)較多,很容易耗盡系統(tǒng)資源,拖慢運行速度。因此有了第三種流程控制方式。
### 并行與串行的結(jié)合
所謂并行與串行的結(jié)合,就是設(shè)置一個門檻,每次最多只能并行執(zhí)行n個異步任務(wù)。這樣就避免了過分占用系統(tǒng)資源。
```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();
```
上面代碼中,最多只能同時運行兩個異步任務(wù)。變量running記錄當(dāng)前正在運行的任務(wù)數(shù),只要低于門檻值,就再啟動一個新的任務(wù),如果等于0,就表示所有任務(wù)都執(zhí)行完了,這時就執(zhí)行final函數(shù)。
## Promise對象
### 簡介
Promise對象是CommonJS工作組提出的一種規(guī)范,目的是為異步操作提供[統(tǒng)一接口](http://wiki.commonjs.org/wiki/Promises/A)。
那么,什么是Promises?
首先,它是一個對象,也就是說與其他JavaScript對象的用法,沒有什么兩樣;其次,它起到代理作用(proxy),充當(dāng)異步操作與回調(diào)函數(shù)之間的中介。它使得異步操作具備同步操作的接口,使得程序具備正常的同步運行的流程,回調(diào)函數(shù)不必再一層層嵌套。
簡單說,它的思想是,每一個異步任務(wù)立刻返回一個Promise對象,由于是立刻返回,所以可以采用同步操作的流程。這個Promises對象有一個then方法,允許指定回調(diào)函數(shù),在異步任務(wù)完成后調(diào)用。
比如,異步操作`f1`返回一個Promise對象,它的回調(diào)函數(shù)`f2`寫法如下。
```javascript
(new Promise(f1)).then(f2);
```
這種寫法對于多層嵌套的回調(diào)函數(shù)尤其方便。
```javascript
// 傳統(tǒng)寫法
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對象的生成格式,做了簡化,真正的語法請參照下文。
總的來說,傳統(tǒng)的回調(diào)函數(shù)寫法使得代碼混成一團,變得橫向發(fā)展而不是向下發(fā)展。Promises規(guī)范就是為了解決這個問題而提出的,目標(biāo)是使用正常的程序流程(同步),來處理異步操作。它先返回一個Promise對象,后面的操作以同步的方式,寄存在這個對象上面。等到異步操作有了結(jié)果,再執(zhí)行前期寄放在它上面的其他操作。
Promises原本只是社區(qū)提出的一個構(gòu)想,一些外部函數(shù)庫率先實現(xiàn)了這個功能。ECMAScript 6將其寫入語言標(biāo)準(zhǔn),因此目前JavaScript語言原生支持Promise對象。
### Promise接口
前面說過,Promise接口的基本思想是,異步任務(wù)返回一個Promise對象。
Promise對象只有三種狀態(tài)。
- 異步操作“未完成”(pending)
- 異步操作“已完成”(resolved,又稱fulfilled)
- 異步操作“失敗”(rejected)
這三種的狀態(tài)的變化途徑只有兩種。
- 異步操作從“未完成”到“已完成”
- 異步操作從“未完成”到“失敗”。
這種變化只能發(fā)生一次,一旦當(dāng)前狀態(tài)變?yōu)椤耙淹瓿伞被颉笆 ?,就意味著不會再有新的狀態(tài)變化了。因此,Promise對象的最終結(jié)果只有兩種。
- 異步操作成功,Promise對象傳回一個值,狀態(tài)變?yōu)閌resolved`。
- 異步操作失敗,Promise對象拋出一個錯誤,狀態(tài)變?yōu)閌rejected`。
Promise對象使用`then`方法添加回調(diào)函數(shù)。`then`方法可以接受兩個回調(diào)函數(shù),第一個是異步操作成功時(變?yōu)閌resolved`狀態(tài))時的回調(diào)函數(shù),第二個是異步操作失?。ㄗ?yōu)閌rejected`)時的回調(diào)函數(shù)(可以省略)。一旦狀態(tài)改變,就調(diào)用相應(yīng)的回調(diào)函數(shù)。
```javascript
// po是一個Promise對象
po.then(
console.log,
console.error
);
```
上面代碼中,Promise對象`po`使用`then`方法綁定兩個回調(diào)函數(shù):操作成功時的回調(diào)函數(shù)`console.log`,操作失敗時的回調(diào)函數(shù)`console.error`(可以省略)。這兩個函數(shù)都接受異步操作傳回的值作為參數(shù)。
`then`方法可以鏈?zhǔn)绞褂谩?
```javascript
po
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
```
上面代碼中,`po`的狀態(tài)一旦變?yōu)閌resolved`,就依次調(diào)用后面每一個`then`指定的回調(diào)函數(shù),每一步都必須等到前一步完成,才會執(zhí)行。最后一個`then`方法的回調(diào)函數(shù)`console.log`和`console.error`,用法上有一點重要的區(qū)別。`console.log`只顯示回調(diào)函數(shù)`step3`的返回值,而`console.error`可以顯示`step1`、`step2`、`step3`之中任意一個發(fā)生的錯誤。也就是說,假定`step1`操作失敗,拋出一個錯誤,這時`step2`和`step3`都不會再執(zhí)行了(因為它們是操作成功的回調(diào)函數(shù),而不是操作失敗的回調(diào)函數(shù))。Promises對象開始尋找,接下來第一個操作失敗時的回調(diào)函數(shù),在上面代碼中是`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構(gòu)造函數(shù),用來生成Promise實例。
下面代碼創(chuàng)造了一個Promise實例。
```javascript
var promise = new Promise(function(resolve, reject) {
// 異步操作的代碼
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
```
Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是`resolve`和`reject`。它們是兩個函數(shù),由JavaScript引擎提供,不用自己部署。
`resolve`函數(shù)的作用是,將Promise對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸腵Pending`變?yōu)閌Resolved`),在異步操作成功時調(diào)用,并將異步操作的結(jié)果,作為參數(shù)傳遞出去;`reject`函數(shù)的作用是,將Promise對象的狀態(tài)從“未完成”變?yōu)椤笆 保磸腵Pending`變?yōu)閌Rejected`),在異步操作失敗時調(diào)用,并將異步操作報出的錯誤,作為參數(shù)傳遞出去。
Promise實例生成以后,可以用`then`方法分別指定`Resolved`狀態(tài)和`Reject`狀態(tài)的回調(diào)函數(shù)。
```javascript
po.then(function(value) {
// success
}, function(value) {
// failure
});
```
### 用法辨析
Promise的用法,簡單說就是一句話:使用`then`方法添加回調(diào)函數(shù)。但是,不同的寫法有一些細微的差別,請看下面四種寫法,它們的差別在哪里?
```javascript
// 寫法一
doSomething().then(function () {
return doSomethingElse();
});
// 寫法二
doSomething().then(function () {
doSomethingElse();
});
// 寫法三
doSomething().then(doSomethingElse());
// 寫法四
doSomething().then(doSomethingElse);
```
為了便于講解,這四種寫法都再用`then`方法接一個回調(diào)函數(shù)`finalHandler`。寫法一的`finalHandler`回調(diào)函數(shù)的參數(shù),是`doSomethingElse`函數(shù)的運行結(jié)果。
```javascript
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
```
寫法二的`finalHandler`回調(diào)函數(shù)的參數(shù)是`undefined`。
```javascript
doSomething().then(function () {
doSomethingElse();
return;
}).then(finalHandler);
```
寫法三的`finalHandler`回調(diào)函數(shù)的參數(shù),是`doSomethingElse`函數(shù)返回的回調(diào)函數(shù)的運行結(jié)果。
```javascript
doSomething().then(doSomethingElse())
.then(finalHandler);
```
寫法四與寫法一只有一個差別,那就是`doSomethingElse`會接收到`doSomething()`返回的結(jié)果。
```javascript
doSomething().then(doSomethingElse)
.then(finalHandler);
```
## Promise的應(yīng)用
### 加載圖片
我們可以把圖片的加載寫成一個`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操作是典型的異步操作,傳統(tǒng)上往往寫成下面這樣。
```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('圖片加載失?。?#039; + request.statusText));
}
};
request.onerror = function() {
reject(new Error('發(fā)生網(wǎng)絡(luò)錯誤'));
};
request.send();
});
}
```
### 小結(jié)
Promise對象的優(yōu)點在于,讓回調(diào)函數(shù)變成了規(guī)范的鏈?zhǔn)綄懛ǎ绦蛄鞒炭梢钥吹煤芮宄K囊徽捉涌?,可以實現(xiàn)許多強大的功能,比如為多個異步操作部署一個回調(diào)函數(shù)、為多個回調(diào)函數(shù)中拋出的錯誤統(tǒng)一指定處理方法等等。
而且,它還有一個前面三種方法都沒有的好處:如果一個任務(wù)已經(jīng)完成,再添加回調(diào)函數(shù),該回調(diào)函數(shù)會立即執(zhí)行。所以,你不用擔(dān)心是否錯過了某個事件或信號。這種方法的缺點就是,編寫和理解都相對比較難。
<h2 id="9.2">JavaScript與有限狀態(tài)機</h2>
## 概述
有限狀態(tài)機(Finite-state machine)是一個非常有用的模型,可以模擬世界上大部分事物。
簡單說,它有三個特征:
- 狀態(tài)總數(shù)(state)是有限的。
- 任一時刻,只處在一種狀態(tài)之中。
- 某種條件下,會從一種狀態(tài)轉(zhuǎn)變(transition)到另一種狀態(tài)。
它對JavaScript的意義在于,很多對象可以寫成有限狀態(tài)機。
舉例來說,網(wǎng)頁上有一個菜單元素。鼠標(biāo)點擊,菜單顯示;鼠標(biāo)再次點擊,菜單隱藏。如果使用有限狀態(tài)機描述,就是這個菜單只有兩種狀態(tài)(顯示和隱藏),鼠標(biāo)會引發(fā)狀態(tài)轉(zhuǎn)變。
代碼可以寫成下面這樣:
```javascript
var menu = {
// 當(dāng)前狀態(tài)
currentState: 'hide',
// 綁定事件
initialize: function() {
var self = this;
self.on("click", self.transition);
},
// 狀態(tài)轉(zhuǎn)換
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;
}
}
};
```
可以看到,有限狀態(tài)機的寫法,邏輯清晰,表達力強,有利于封裝事件。一個對象的狀態(tài)越多、發(fā)生的事件越多,就越適合采用有限狀態(tài)機的寫法。
另外,JavaScript語言是一種異步操作特別多的語言,常用的解決方法是指定回調(diào)函數(shù),但這樣會造成代碼結(jié)構(gòu)混亂、難以測試和除錯等問題。有限狀態(tài)機提供了更好的辦法:把異步操作與對象的狀態(tài)改變掛鉤,當(dāng)異步操作結(jié)束的時候,發(fā)生相應(yīng)的狀態(tài)改變,由此再觸發(fā)其他操作。這要比回調(diào)函數(shù)、事件監(jiān)聽、發(fā)布/訂閱等解決方案,在邏輯上更合理,更易于降低代碼的復(fù)雜度。
## Javascript Finite State Machine函數(shù)庫
下面介紹一個有限狀態(tài)機的函數(shù)庫[Javascript Finite State Machine](https://github.com/jakesgordon/javascript-state-machine)。這個庫非常好懂,可以幫助我們加深理解,而且功能一點都不弱。
該庫提供一個全局對象StateMachine,使用該對象的create方法,可以生成有限狀態(tài)機的實例。
```javascript
var fsm = StateMachine.create();
```
生成的時候,需要提供一個參數(shù)對象,用來描述實例的性質(zhì)。比如,交通信號燈(紅綠燈)可以這樣描述:
```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' }
]
});
```
交通信號燈的初始狀態(tài)(initial)為green,events屬性是觸發(fā)狀態(tài)改變的各種事件,比如warn事件使得green狀態(tài)變成yellow狀態(tài),stop事件使得yellow狀態(tài)變成red狀態(tài)等等。
生成實例以后,就可以隨時查詢當(dāng)前狀態(tài)。
- fsm.current :返回當(dāng)前狀態(tài)。
- fsm.is(s) :返回一個布爾值,表示狀態(tài)s是否為當(dāng)前狀態(tài)。
- fsm.can(e) :返回一個布爾值,表示事件e是否能在當(dāng)前狀態(tài)觸發(fā)。
- fsm.cannot(e) :返回一個布爾值,表示事件e是否不能在當(dāng)前狀態(tài)觸發(fā)。
Javascript Finite State Machine允許為每個事件指定兩個回調(diào)函數(shù),以warn事件為例:
- onbefore**warn**:在warn事件發(fā)生之前觸發(fā)。
- onafter**warn**(可簡寫成onwarn) :在warn事件發(fā)生之后觸發(fā)。
同時,它也允許為每個狀態(tài)指定兩個回調(diào)函數(shù),以green狀態(tài)為例:
- onleave**green** :在離開green狀態(tài)時觸發(fā)。
- onenter**green**(可簡寫成ongreen) :在進入green狀態(tài)時觸發(fā)。
假定warn事件使得狀態(tài)從green變?yōu)閥ellow,上面四類回調(diào)函數(shù)的發(fā)生順序如下:onbefore**warn** → onleave**green** → onenter**yellow** → onafter**warn**。
除了為每個事件和狀態(tài)單獨指定回調(diào)函數(shù),還可以為所有的事件和狀態(tài)指定通用的回調(diào)函數(shù)。
- onbeforeevent :任一事件發(fā)生之前觸發(fā)。
- onleavestate :離開任一狀態(tài)時觸發(fā)。
- onenterstate :進入任一狀態(tài)時觸發(fā)。
- onafterevent :任一事件結(jié)束后觸發(fā)。
如果事件的回調(diào)函數(shù)里面有異步操作(比如與服務(wù)器進行Ajax通信),這時我們可能希望等到異步操作結(jié)束,再發(fā)生狀態(tài)改變。這就要用到transition方法。
```javascript
fsm.onleavegreen = function(){
light.fadeOut('slow', function() {
fsm.transition();
});
return StateMachine.ASYNC;
};
```
上面代碼的回調(diào)函數(shù)里面,有一個異步操作(light.fadeOut)。如果不希望狀態(tài)立即改變,就要讓回調(diào)函數(shù)返回StateMachine.ASYNC,表示狀態(tài)暫時不改變;等到異步操作結(jié)束,再調(diào)用transition方法,使得狀態(tài)發(fā)生改變。
Javascript Finite State Machine還允許指定錯誤處理函數(shù),當(dāng)發(fā)生了當(dāng)前狀態(tài)不可能發(fā)生的事件時自動觸發(fā)。
```javascript
var fsm = StateMachine.create({
// ...
error: function(eventName, from, to, args, errorCode, errorMessage) {
return 'event ' + eventName + ': ' + errorMessage;
},
// ...
});
```
比如,當(dāng)前狀態(tài)是green,理論上這時只可能發(fā)生warn事件。要是這時發(fā)生了stop事件,就會觸發(fā)上面的錯誤處理函數(shù)。
Javascript Finite State Machine的基本用法就是上面這些,更詳細的介紹可以參見它的[主頁](https://github.com/jakesgordon/javascript-state-machine)。
<h2 id="9.3">MVC框架與Backbone.js</h2>
## MVC框架
隨著JavaScript程序變得越來越復(fù)雜,往往需要一個團隊協(xié)作開發(fā),這時代碼的模塊化和組織規(guī)范就變得異常重要了。MVC模式就是代碼組織的經(jīng)典模式。
(……MVC介紹。)
**(1)Model**
Model表示數(shù)據(jù)層,也就是程序需要的數(shù)據(jù)源,通常使用JSON格式表示。
**(2)View**
View表示表現(xiàn)層,也就是用戶界面,對于網(wǎng)頁來說,就是用戶看到的網(wǎng)頁HTML代碼。
**(3)Controller**
Controller表示控制層,用來對原始數(shù)據(jù)(Model)進行加工,傳送到View。
由于網(wǎng)頁編程不同于客戶端編程,在MVC的基礎(chǔ)上,JavaScript社區(qū)產(chǎn)生了各種變體框架MVP(Model-View-Presenter)、MVVM(Model-View-ViewModel)等等,有人就把所有這一類框架的各種模式統(tǒng)稱為MV*。
框架的優(yōu)點在于合理組織代碼、便于團隊合作和未來的維護,缺點在于有一定的學(xué)習(xí)成本,且限制你只能采取它的寫法。
## 零框架解決方案
MVC框架(尤其是大型框架)有一個嚴重的缺點,就是會產(chǎn)生用戶的重度依賴。一旦框架本身出現(xiàn)問題或者停止更新,用戶的處境就會很困難,維護和更新成本極高。
ES6的到來,使得JavaScript語言有了原生的模塊解決方案。于是,開發(fā)者有了另一種選擇,就是不使用MVC框架,只使用各種單一用途的模塊庫,組合完成一個項目。下面是可供選擇的各種用途的模塊列表。
輔助功能庫(Helper Libraries)
- [moment.js](http://momentjs.com/):日期和時間的標(biāo)準(zhǔn)化
- [underscore.js](http://underscorejs.org/) / [Lo-Dash](https://lodash.com/):一系列函數(shù)式編程的功能函數(shù)
路由庫(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):同時支持服務(wù)器和瀏覽器的路由庫
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標(biāo)準(zhǔn),同時兼容老式瀏覽器
客戶端與服務(wù)器的通信庫
- [fetch](https://github.com/github/fetch):實現(xiàn)window.fetch功能
- [qwest](https://github.com/pyrsmk/qwest):支持XHR2和Promise的Ajax庫
- [jQuery](https://github.com/jquery/jquery):jQuery 2.0支持按模塊打包,因此可以創(chuàng)建一個純Ajax功能庫
動畫庫(Animation)
- [cssanimevent](https://github.com/magnetikonline/cssanimevent):兼容老式瀏覽器的CSS3動畫庫
- [Velocity.js](http://julian.com/research/velocity/):性能優(yōu)秀的動畫庫
輔助開發(fā)庫(Development Assistance)
- [LogJS](https://github.com/bfattori/LogJS):輕量級的logging功能庫
- [UserTiming.js](https://github.com/nicjansma/usertiming.js):支持老式瀏覽器的高精度時間戳庫
流程控制和架構(gòu)(Flow Control/Architecture)
- [ondomready](https://github.com/tubalmartin/ondomready):類似jQuery的ready()方法,符合AMD規(guī)范
- [script.js](https://github.com/ded/script.js]):異步的腳本加載和依賴關(guān)系管理庫
- [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)
數(shù)據(jù)綁定(Data-binding)
- Object.observe():Chrome已經(jīng)支持該方法,可以輕易實現(xiàn)雙向數(shù)據(jù)綁定
模板庫(Templating)
- [Mustache](http://mustache.github.io/):大概是目前使用最廣的不含邏輯的模板系統(tǒng)
微框架(Micro-Framework)
某些情況下,可以使用微型框架,作為項目開發(fā)的起點。
- [bottlejs](https://github.com/young-steveo/bottlejs):提供惰性加載、中間件鉤子、裝飾器等功能
- [Stapes.js](http://hay.github.io/stapes/#top):微型MVC框架
- [soma.js](http://somajs.github.io/somajs/site/):提供一個松耦合、易測試的架構(gòu)
- [knockout](http://knockoutjs.com/):最流行的微框架之一,主要關(guān)注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框架,也是最簡化的一個框架。它的設(shè)計思想是,只提供最基本的功能,給用戶提供最大的自由。這意味著,好的一面是它沒有一整套規(guī)則,強制你接受,壞的一面是很多功能你必須自己實現(xiàn)。Backbone的體積相當(dāng)小,最小化后只有30多KB。
定義一個對象,表示W(wǎng)eb應(yīng)用。
```javascript
var AppName = {
Models :{},
Views :{},
Collections :{},
Controllers :{}
};
```
上面代碼表示,應(yīng)用由四部分組成:Model、Collection、Controller和View。
定義Model,表示數(shù)據(jù)的一個基本單位。
```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構(gòu)成。
定義一個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屬性,指明當(dāng)前View綁定的DOM節(jié)點,events屬性指明事件和對應(yīng)的方法。
定義一個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});
});
};
```
最后,定義路由,啟動應(yīng)用程序。
```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>一級標(biāo)題</h1>');
}
});
```
上面代碼通過Backbone.View的extend方法,定義了一個視圖類AppView。該類內(nèi)部有一個render方法,用于將視圖放置在網(wǎng)頁上。
使用的時候,需要先新建視圖類的實例,然后通過實例,調(diào)用render方法,從而讓視圖在網(wǎng)頁上顯示。
```javascript
var appView = new AppView();
appView.render();
```
上面代碼新建視圖類AppView的實例appView,然后調(diào)用appView.render,網(wǎng)頁上就會顯示指定的內(nèi)容。
新建視圖實例時,通常需要指定Model。
```javascript
var document = new Document({
model: doc
});
```
### initialize方法
視圖還可以定義initialize方法,生成實例的時候,會自動調(diào)用該方法對實例初始化。
```javascript
var AppView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
$('main').append('<h1>一級標(biāo)題</h1>');
}
});
var appView = new AppView();
```
上面代碼定義了initialize方法之后,就省去了生成實例后,手動調(diào)用appView.render()的步驟。
### el屬性,$el屬性
除了直接在render方法中,指定“視圖”所綁定的網(wǎng)頁元素,還可以用視圖的el屬性指定網(wǎng)頁元素。
```javascript
var AppView = Backbone.View.extend({
el: $('main'),
render: function(){
this.$el.append('<h1>一級標(biāo)題</h1>');
}
});
```
上面的代碼與render方法直接綁定網(wǎng)頁元素,效果完全一樣。上面代碼中,除了el屬性,還是$el屬性,前者代表指定的DOM元素,后者則表示該DOM元素對應(yīng)的jQuery對象。
### tagName屬性,className屬性
如果不指定el屬性,也可以通過tagName屬性和className屬性指定。
```javascript
var Document = Backbone.View.extend({
tagName: "li",
className: "document",
render: function() {
// ...
}
});
```
### template方法
視圖的template屬性用來指定網(wǎng)頁模板。
```javascript
var AppView = Backbone.View.extend({
template: _.template("<h3>Hello <%= who %><h3>"),
});
```
上面代碼中,underscore函數(shù)庫的template函數(shù),接受一個模板字符串作為參數(shù),返回對應(yīng)的模板函數(shù)。有了這個模板函數(shù),只要提供具體的值,就能生成網(wǎng)頁代碼。
```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就調(diào)用了template方法,從而生成具體的網(wǎng)頁代碼。
實際應(yīng)用中,一般將模板放在script標(biāo)簽中,為了防止瀏覽器按照JavaScript代碼解析,type屬性設(shè)為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屬性用于指定視圖的事件及其對應(yīng)的處理函數(shù)。
```javascript
var Document = Backbone.View.extend({
events: {
"click .icon": "open",
"click .button.edit": "openEditDialog",
"click .button.delete": "destroy"
}
});
```
上面代碼中一個指定了三個CSS選擇器的單擊事件,及其對應(yīng)的三個處理函數(shù)。
### listento方法
listento方法用于為特定事件指定回調(diào)函數(shù)。
```javascript
var Document = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render);
}
});
```
上面代碼為model的change事件,指定了回調(diào)函數(shù)為render。
### remove方法
remove方法用于移除一個視圖。
```javascript
updateView: function() {
view.remove();
view.render();
};
```
### 子視圖(subview)
在父視圖中可以調(diào)用子視圖。下面就是一種寫法。
```javascript
render : function (){
this.$el.html(this.template());
this.child = new Child();
this.child.appendTo($.('.container-placeholder').render();
}
```
## Backbone.Events
`Backbone.Events`是一個事件對象。任何繼承了這個對象的對象,都具備了`Backbone.Events`的事件接口,可以調(diào)用on和trigger方法,發(fā)布和訂閱消息。
```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提供的路由對象,用來將用戶請求的網(wǎng)址與后端的處理函數(shù)一一對應(yīng)。
首先,新定義一個Router類。
```javascript
Router = Backbone.Router.extend({
routes: {
}
});
```
## routes屬性
Backbone.Router對象中,最重要的就是routes屬性。它用來設(shè)置路徑的處理方法。
routes屬性是一個對象,它的每個成員就代表一個路徑處理規(guī)則,鍵名為路徑規(guī)則,鍵值為處理方法。
如果鍵名為空字符串,就代表根路徑。
```javascript
routes: {
'': 'phonesIndex',
},
phonesIndex: function () {
new PhonesIndexView({ el: 'section#main' });
}
```
星號代表任意路徑,可以設(shè)置路徑參數(shù),捕獲具體的路徑值。
```javascript
var AppRouter = Backbone.Router.extend({
routes: {
"*actions": "defaultRoute"
}
});
var app_router = new AppRouter;
app_router.on('route:defaultRoute', function(actions) {
console.log(actions);
})
```
上面代碼中,根路徑后面的參數(shù),都會被捕獲,傳入回調(diào)函數(shù)。
路徑規(guī)則的寫法。
```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
設(shè)置了router以后,就可以啟動應(yīng)用程序。Backbone.history對象用來監(jiān)控url的變化。
```javascript
App = new Router();
$(document).ready(function () {
Backbone.history.start({ pushState: true });
});
```
打開pushState方法。如果應(yīng)用程序不在根目錄,就需要指定根目錄。
```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屬性用來設(shè)置默認屬性,上面代碼表示user對象默認有name和email兩個屬性,它們的值都等于空字符串。
生成實例時,可以提供各個屬性的具體值。
```javascript
var user = new User ({
id: 1,
name: 'name',
email: 'name@email.com'
});
```
上面代碼在生成實例時,提供了各個屬性的具體值。
### idAttribute屬性
Model實例必須有一個屬性,作為區(qū)分其他實例的主鍵。這個屬性的名稱,由idAttribute屬性設(shè)定,一般是設(shè)為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方法用于設(shè)置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方法用于監(jiān)聽對象的變化。
```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方法用于監(jiān)聽事件,“change:name”表示name屬性發(fā)生變化。
### urlroot屬性
該屬性用于指定服務(wù)器端對model進行操作的路徑。
```javascript
var User = Backbone.Model.extend({
urlRoot: '/user'
});
```
上面代碼指定,服務(wù)器對應(yīng)該Model的路徑為/user。
### fetch事件
fetch事件用于從服務(wù)器取出Model。
```javascript
var user = new User ({id: 1});
user.fetch({
success: function (user){
console.log(user.toJSON());
}
})
```
上面代碼中,user實例含有id屬性(值為1),fetch方法使用HTTP動詞GET,向網(wǎng)址“/user/1”發(fā)出請求,從服務(wù)器取出該實例。
### save方法
save方法用于通知服務(wù)器新建或更新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對應(yīng)的網(wǎng)址是/user,然后新建一個實例,最后調(diào)用save方法。它有兩個參數(shù),第一個是實例對象的具體屬性,第二個參數(shù)是一個回調(diào)函數(shù)對象,設(shè)定success事件(保存成功)的回調(diào)函數(shù)。具體來說,save方法會向/user發(fā)出一個POST請求,并將{name: 'name', email: 'name@email.com'}作為數(shù)據(jù)提供。
如果一個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方法向網(wǎng)址“/user/1”發(fā)出請求,從而更新該實例。
### destroy方法
destroy方法用于在服務(wù)器上刪除該實例。
```javascript
var user = new User ({
id: 1,
name: 'name',
email: 'name@email.com'
});
user.destroy({
success: function () {
console.log('Destroyed');
}
});
```
上面代碼的destroy方法,將使用HTTP動詞DELETE,向網(wǎng)址“/user/1”發(fā)出請求,刪除對應(yīng)的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方法的參數(shù)是model實例的id屬性。
### get方法,set方法
get方法用于從Collection中獲取指定id的Model實例。
```javascript
myAlbum.get(2))
```
### fetch方法
fetch方法用于從服務(wù)器取出Collection數(shù)據(jù)。
```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>
## 概述
### 設(shè)計目的
除了正常運行模式,ECMAScript 5添加了第二種運行模式:“嚴格模式”(strict mode)。顧名思義,這種模式使得JavaScript在更嚴格的條件下運行。
設(shè)立”嚴格模式“的目的,主要有以下幾個:
- 消除JavaScript語法的一些不合理、不嚴謹之處,減少一些怪異行為;
- 增加更多報錯的場合,消除代碼運行的一些不安全之處,保證代碼運行的安全;
- 提高編譯器效率,增加運行速度;
- 為未來新版本的JavaScript做好鋪墊。
“嚴格模式”體現(xiàn)了JavaScript更合理、更安全、更嚴謹?shù)陌l(fā)展方向。
同樣的代碼,在”正常模式“和”嚴格模式“中,可能會有不一樣的運行結(jié)果。一些在"正常模式"下可以運行的語句,在"嚴格模式"下將不能運行。掌握這些內(nèi)容,有助于更細致深入地理解JavaScript,讓你變成一個更好的程序員。
### 啟用方法
進入“嚴格模式”的標(biāo)志,是一行字符串`use strict`。
```javascript
'use strict';
```
老版本的瀏覽器會把它當(dāng)作一行普通字符串,加以忽略。新版本的瀏覽器就會進入嚴格模式。
“嚴格模式”可以用于整個腳本,也可以只用于單個函數(shù)。
**(1) 針對整個腳本文件**
將`use strict`放在腳本文件的第一行,則整個腳本都將以“嚴格模式”運行。如果這行語句不在第一行就無效,整個腳本會以“正常模式”運行。(嚴格地說,只要前面不是產(chǎn)生實際運行結(jié)果的語句,`use strict`可以不在第一行,比如直接跟在一個空的分號后面,或者跟在注釋后面。)
```html
<script>
'use strict';
console.log('這是嚴格模式');
</script>
<script>
console.log('這是正常模式');
</script>
```
上面的代碼表示,一個網(wǎng)頁文件中依次有兩段JavaScript代碼。前一個`<script>`標(biāo)簽是嚴格模式,后一個不是。
如果字符串`use strict`出現(xiàn)在代碼中間,則不起作用,即嚴格模式必須從代碼一開始就生效。
兩個不同模式的腳本合并成一個文件,如果嚴格模式的腳本在前,則合并后的腳本都是”嚴格模式“;如果正常模式的腳本在前,則合并后的腳本都是”正常模式“??傊@兩種情況下,合并后的結(jié)果都是不正確的。因此,建議在多個腳本需要合并的場合,”嚴格模式“只在函數(shù)中打開,不針對整個腳本打開。
**(2)針對單個函數(shù)**
將“use strict”放在函數(shù)體的第一行,則整個函數(shù)以“嚴格模式”運行。
```javascript
function strict() {
'use strict';
return '這是嚴格模式';
}
function notStrict() {
return '這是正常模式';
}
```
**(3)腳本文件的變通寫法**
因為在腳本文件第一行放置`use strict`不利于文件合并,所以更好的做法是,借用第二種方法,將整個腳本文件放在一個立即執(zhí)行的匿名函數(shù)之中。
```javascript
(function () {
"use strict";
// some code here
})();
```
## 顯式報錯
嚴格模式使得JavaScript的語法變得更嚴格,更多的操作會顯式報錯。其中有些操作,在正常模式下只會默默地失敗,不會報錯。
### 字符串的length屬性不可寫
嚴格模式下,設(shè)置字符串的`length`屬性,會報錯。
```javascript
'use strict';
'abc'.length = 5;
```
實際上,嚴格模式下,對只讀屬性賦值,或者刪除不可配置(nonconfigurable)屬性都會報錯。
### eval、arguments不可用作函數(shù)名
使用`eval`,或者在函數(shù)內(nèi)部使用`arguments`,作為標(biāo)識名,將會報錯。
下面的語句都會報錯。
```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; // 報錯
```
### 只設(shè)置了賦值器的屬性不可寫
嚴格模式下,對一個只設(shè)置了賦值器(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; // 報錯
```
### 函數(shù)不能有重名的參數(shù)
正常模式下,如果函數(shù)有多個重名的參數(shù),可以用`arguments[i]`讀取。嚴格模式下,這屬于語法錯誤。
```javascript
function f(a, a, b) { // 語法錯誤
'use strict';
return a + b;
}
```
### 禁止八進制的前綴0表示法
正常模式下,整數(shù)的第一位如果是`0`,表示這是八進制數(shù),比如`0100`等于十進制的64。嚴格模式禁止這種表示法,整數(shù)第一位為`0`,將報錯。
```javascript
"use strict";
var n = 0100; // SyntaxError
```
## 增強的安全措施
嚴格模式增強了安全保護,從語法上防止了一些不小心會出現(xiàn)的錯誤。
### 全局變量顯式聲明
在正常模式中,如果一個變量沒有聲明就賦值,默認是全局變量。嚴格模式禁止這種用法,全局變量必須顯式聲明。
```javascript
'use strict';
v = 1; // 報錯,v未聲明
for (i = 0; i < 2; i++) { // 報錯,i未聲明
// ...
}
function f() {
x = 123;
}
f() // 報錯,未聲明就創(chuàng)建一個全局變量
```
因此,嚴格模式下,變量都必須先用`var`命令聲明,然后再使用。
### 禁止this關(guān)鍵字指向全局對象
正常模式下,函數(shù)內(nèi)部的`this`可能會指向全局對象,嚴格模式禁止這種用法,避免無意間創(chuàng)造全局變量。
```javascript
// 正常模式
function f() {
console.log(this === window);
}
f() // true
// 嚴格模式
function f() {
'use strict';
console.log(this === undefined);
}
f() // true
```
這種限制對于構(gòu)造函數(shù)尤其有用。使用構(gòu)造函數(shù)時,有時忘了加`new`,這時`this`不再指向全局對象,而是報錯。
```javascript
function f() {
'use strict';
this.a = 1;
};
f();// 報錯,this未定義
```
嚴格模式下,函數(shù)直接調(diào)用時(不使用`new`調(diào)用),函數(shù)內(nèi)部的`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
函數(shù)內(nèi)部不得使用`fn.caller`、`fn.arguments`,否則會報錯。這意味著不能在函數(shù)內(nèi)部得到調(diào)用棧了。
```javascript
function f1() {
'use strict';
f1.caller; // 報錯
f1.arguments; // 報錯
}
f1();
```
### 禁止使用arguments.callee、arguments.caller
`arguments.callee`和`arguments.caller`是兩個歷史遺留的變量,從來沒有標(biāo)準(zhǔn)化過,現(xiàn)在已經(jīng)取消了。正常模式下調(diào)用它們沒有什么作用,但是不會報錯。嚴格模式明確規(guī)定,函數(shù)內(nèi)部使用`arguments.callee`、`arguments.caller`將會報錯。
```javascript
'use strict';
var f = function() {
return arguments.callee;
};
f(); // 報錯
```
### 禁止刪除變量
嚴格模式下無法刪除變量,如果使用`delete`命令刪除一個變量,會報錯。只有對象的屬性,且屬性的描述對象的`configurable`屬性設(shè)置為true,才能被`delete`命令刪除。
```javascript
"use strict";
var x;
delete x; // 語法錯誤
var o = Object.create(null, {
x: {
value: 1,
configurable: true
}
});
delete o.x; // 刪除成功
```
## 靜態(tài)綁定
JavaScript語言的一個特點,就是允許“動態(tài)綁定”,即某些屬性和方法到底屬于哪一個對象,不是在編譯時確定的,而是在運行時(runtime)確定的。
嚴格模式對動態(tài)綁定做了一些限制。某些情況下,只允許靜態(tài)綁定。也就是說,屬性和方法到底歸屬哪個對象,必須在編譯階段就確定。這樣做有利于編譯效率的提高,也使得代碼更容易閱讀,更少出現(xiàn)意外。
具體來說,涉及以下幾個方面。
### 禁止使用with語句
嚴格模式下,使用`with`語句將報錯。因為`with`語句無法在編譯時就確定,某個屬性到底歸屬哪個對象,從而影響了編譯效果。
```javascript
'use strict';
var v = 1;
with (o) { // SyntaxError
v = 2;
}
```
### 創(chuàng)設(shè)eval作用域
正常模式下,JavaScript語言有兩種變量作用域(scope):全局作用域和函數(shù)作用域。嚴格模式創(chuàng)設(shè)了第三種作用域:`eval`作用域。
正常模式下,`eval`語句的作用域,取決于它處于全局作用域,還是函數(shù)作用域。嚴格模式下,`eval`語句本身就是一個作用域,不再能夠在其所運行的作用域創(chuàng)設(shè)新的變量了,也就是說,`eval`所生成的變量只能用于`eval`內(nèi)部。
```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`內(nèi)部使用的都是嚴格模式。
### arguments不再追蹤參數(shù)的變化
變量`arguments`代表函數(shù)的參數(shù)。嚴格模式下,函數(shù)內(nèi)部改變參數(shù)與`arguments`的聯(lián)系被切斷了,兩者不再存在聯(lián)動關(guān)系。
```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]
```
上面代碼中,改變函數(shù)的參數(shù),不會反應(yīng)到`arguments`對象上來。
## 向下一個版本的JavaScript過渡
JavaScript語言的下一個版本是ECMAScript 6,為了平穩(wěn)過渡,嚴格模式引入了一些ES6語法。
### 函數(shù)必須聲明在頂層
JavaScript的新版本ES6會引入“塊級作用域”。為了與新版本接軌,嚴格模式只允許在全局作用域或函數(shù)作用域的頂層聲明函數(shù)。也就是說,不允許在非函數(shù)的代碼塊內(nèi)聲明函數(shù)。
```javascript
"use strict";
if (true) {
function f1() { } // 語法錯誤
}
for (var i = 0; i < 5; i++) {
function f2() { } // 語法錯誤
}
```
上面代碼在`if`代碼塊和`for`代碼塊中聲明了函數(shù),在嚴格模式下都會報錯。
### 保留字
為了向?qū)鞪avaScript的新版本過渡,嚴格模式新增了一些保留字:implements, interface, let, package, private, protected, public, static, yield。
使用這些詞作為變量名將會報錯。
```javascript
function package(protected) { // 語法錯誤
'use strict';
var implements; // 語法錯誤
}
```
此外,ECMAscript第五版本身還規(guī)定了另一些保留字(`class`, `enum`, `export`, `extends`, `import`, `super`),以及各大瀏覽器自行增加的`const`保留字,也是不能作為變量名的。
