<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`保留字,也是不能作為變量名的。