## 2.6 行為委托
### 2.6.1 面向委托的設計
**1. 類理論**
假設我們需要在軟件中建模一些類似的任務(“XYZ”、“ABC”等)。
如果使用類,那設計方法可能是這樣的:定義一個通用父(基)類,可以將其命名為Task,在Task 類中定義所有任務都有的行為。接著定義子類XYZ 和ABC,它們都繼承自Task 并且會添加一些特殊的行為來處理對應的任務。
類設計模式鼓勵你在繼承時使用方法重寫(和多態),比如說在XYZ 任務中重寫Task 中定義的一些通用方法,甚至在添加新行為時通過super 調用這個方法的原始版本。你會發現許多行為可以先“抽象”到父類然后再用子類進行特殊化(重寫)。
下面是對應的偽代碼:
~~~
class Task {
id;
// 構造函數Task()
Task(ID) { id = ID; }
outputTask() { output( id ); }
}
class XYZ inherits Task {
label;
// 構造函數XYZ()
XYZ(ID,Label) { super( ID ); label = Label; }
outputTask() { super(); output( label ); }
}
class ABC inherits Task {
// ...
}
~~~
**2. 委托理論**
首先你會定義一個名為Task 的對象(既不是類也不是函數),它會包含所有任務都可以使用(委托)的具體行為。接著,對于每個任務(“XYZ”、“ABC”)你都會定義一個對象來存儲對應的數據和行為。你會把特定的任務對象都關聯到Task 功能對象上,讓它們在需要的時候可以進行委托。
基本上你可以想象成,執行任務“XYZ”需要兩個兄弟對象(XYZ 和Task)協作完成。但是我們并不需要把這些行為放在一起,通過類的復制,我們可以把它們分別放在各自獨立的對象中,需要時可以允許XYZ 對象委托給Task。
~~~
Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};
// 讓XYZ 委托Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...
~~~
在這段代碼中,Task 和XYZ 并不是類( 或者函數), 它們是對象。XYZ 通過`Object.create(..)` 創建,它的[[Prototype]] 委托了Task 對象。相比于面向類(或者說面向對象),這種編碼風格應稱為“對象關聯”(OLOO,objects linked to other objects)。我們真正關心的只是XYZ 對象(和ABC 對象)委托了Task 對象。
對象關聯風格的代碼還有一些不同之處。
* 1)在上面的代碼中,id 和label 數據成員都是直接存儲在XYZ 上(而不是Task)。通常來說,在[[Prototype]] 委托中最好把狀態保存在委托者(XYZ、ABC)而不是委托目標(Task)上。
* 2) 在類設計模式中,我們故意讓父類(Task)和子類(XYZ)中都有outputTask 方法,這樣就可以利用重寫(多態)的優勢。在委托行為中則恰好相反:我們會盡量避免在`[[Prototype]] `鏈的不同級別中使用相同的命名,否則就需要使用笨拙并且脆弱的語法來消除引用歧義。
這個設計模式要求盡量少使用容易被重寫的通用方法名,提倡使用更有描述性的方法名,尤其是要寫清相應對象行為的類型。這樣做實際上可以創建出更容易理解和維護的代碼,因為方法名(不僅在定義的位置,而是貫穿整個代碼)更加清晰(自文檔)。
* 3) `this.setID(ID)`;XYZ 中的方法首先會尋找XYZ 自身是否有setID(..),但是XYZ 中并沒有這個方法名,因此會通過[[Prototype]] 委托關聯到Task 繼續尋找,這時就可以找到setID(..) 方法。此外,由于調用位置觸發了this 的隱式綁定規則,因此雖然setID(..) 方法在Task 中,運行時this 仍然會綁定到XYZ,這正是我們想要的。后面的this.outputID(),原理相同。
**委托行為**意味著某些對象(XYZ)在找不到屬性或者方法引用時會把這個請求委托給另一個對象(Task)。
#### **互相委托(禁止)**
你無法在兩個或兩個以上互相(雙向)委托的對象之間創建循環委托。如果你把B 關聯到A 然后試著把A 關聯到B,就會出錯。
之所以要禁止互相委托,是因為引擎的開發者們發現在設置時檢查(并禁止!)一次無限循環引用要更加高效,否則每次從對象中查找屬性時都需要進行檢查。
#### **調試**
通常來說,JavaScript 規范并不會控制瀏覽器中開發者工具對于特定值或者結構的表示方式,瀏覽器和引擎可以自己選擇合適的方式來進行解析,因此瀏覽器和工具的解析結果并不一定相同。
這段傳統的“類構造函數”JavaScript 代碼在Chrome 開發者工具的控制臺中結果如下所示:
~~~
function Foo() {}
var a1 = new Foo();
a1; // Foo {}
~~~
分析原理:
~~~
var Foo = {};
var a1 = Object.create( Foo );
a1; // Object {}
Object.defineProperty( Foo, "constructor", {
enumerable: false,
value: function Gotcha(){}
});
a1; // Gotcha {}
~~~
本例中Chrome 的控制臺確實使用了`.constructor.name`。Chrome 內部跟蹤(只用于調試輸出)“構造函數名稱”的方法是Chrome自身的一種擴展行為,并不包含在JavaScript 的規范中。
如果你并不是使用“構造函數”來生成對象,比如使用對象關聯風格來編寫代碼,那Chrome 就無法跟蹤對象內部的“構造函數名稱”,這樣的對象輸出是`Object {}`,意思是“Object() 構造出的對象”。
當然,這并不是對象關聯風格代碼的缺點。當你使用對象關聯風格來編寫代碼并使用行為委托設計模式時,并不需要關注是誰“構造了”對象(就是使用new 調用的那個函數)。只有使用類風格來編寫代碼時Chrome 內部的“構造函數名稱”跟蹤才有意義,使用對象關聯時這個功能不起任何作用。
**3. 比較思維模型**
下面是典型的(“原型”)面向對象風格:
~~~
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
~~~
子類Bar 繼承了父類Foo,然后生成了b1 和b2 兩個實例。b1 委托了Bar.prototype,后者委托了Foo.prototype。
使用對象關聯風格來編寫功能完全相同的代碼:
~~~
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
~~~
#### **兩段代碼對應的思維模型**
首先,類風格代碼的思維模型強調實體以及實體間的關系:

從圖中可以看出這是一張十分復雜的關系網。此外,如果你跟著圖中的箭頭走就會發現,JavaScript 機制有很強的內部連貫性。
舉例來說,JavaScript 中的函數之所以可以訪問`call(..)、apply(..) 和bind(..)`,就是因為函數本身是對象。而函數對象同樣有`[[Prototype]]` 屬性并且關聯到`Function.prototype` 對象,因此所有函數對象都可以通過委托調用這些默認方法。
好,下面我們來看一張簡化版的圖,它更“清晰”一些——只展示了必要的對象和關系:

虛線表示的是Bar.prototype 繼承Foo.prototype 之后丟失的.constructor屬性引用,它們還沒有被修復。即使移除這些虛線,這個思維模型在你處理對象關聯時仍然非常復雜。
現在我們看看對象關聯風格代碼的思維模型:

這種代碼只關注一件事:**對象之間的關聯關系**。
### 2.6.2 類與對象
真實場景應用“類”與“行為委托”
**1. 控件“類”**
下面這段代碼展示的是如何在不使用任何“類”輔助庫或者語法的情況下,使用純
JavaScript 實現類風格的代碼:
~~~
// 父類
function Widget(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
};
// 子類
function Button(width,height,label) {
// 調用“super”構造函數
Widget.call( this, width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
// 讓Button“繼承”Widget
Button.prototype = Object.create( Widget.prototype );
// 重寫render(..)
Button.prototype.render = function($where) {
// “super”調用
Widget.prototype.render.call( this, $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
~~~
在面向對象設計模式中我們需要先在父類中定義基礎的render(..),然后在子類中重寫它。子類并不會替換基礎的render(..),只是添加一些按鈕特有的行為。可以看到代碼中出現了丑陋的顯式偽多態,即通過`Widget.call` 和`Widget.prototype.render.call` 從“子類”方法中引用“父類”中的基礎方法。
#### ES6的class語法糖
~~~
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Button '" + this.label + "' clicked!" );
}
}
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
~~~
盡管語法上得到了改進,但實際上這里并沒有真正的類,class 仍然是通過`[[Prototype]]`機制實現的。無論你使用的是傳統的原型語法還是ES6 中的新語法糖,你仍然需要用“類”的概念來對問題(UI 控件)進行建模。
**2. 委托控件對象**
~~~
var Widget = {
init: function (width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function ($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
};
var Button = Object.create(Widget);
Button.setup = function (width, height, label) {
// 委托調用
this.init(width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
};
Button.build = function ($where) {
// 委托調用
this.insert($where);
this.$elem.click(this.onClick.bind(this));
};
Button.onClick = function (evt) {
console.log("Button '" + this.label + "' clicked!");
};
$(document).ready(function () {
var $body = $(document.body);
var btn1 = Object.create(Button);
btn1.setup(125, 30, "Hello");
var btn2 = Object.create(Button);
btn2.setup(150, 40, "World");
btn1.build($body);
btn2.build($body);
});
~~~
使用對象關聯風格來編寫代碼時不需要把Widget 和Button 當作父類和子類。相反,Widget 只是一個對象,包含一組通用的函數,任何類型的控件都可以委托,Button 同樣只是一個對象。
從設計模式的角度來說, 我們并沒有像類一樣在兩個對象中都定義相同的方法名render(..),相反,我們定義了兩個更具描述性的方法名(insert(..) 和build(..))。同理,初始化方法分別叫作init(..) 和setup(..)。
之前的一次調用(`var btn1 = new Button(..)`)現在變成了兩次(`var btn1 = Object.create(Button) 和btn1.setup(..)`)使用類構造函數的話,你需要(并不是硬性要求,但是強烈建議)在同一個步驟中實現構造和初始化。然而,在許多情況下把這兩步分開(就像對象關聯代碼一樣)更靈活。
舉例來說,假如你在程序啟動時創建了一個實例池,然后一直等到實例被取出并使用時才執行特定的初始化過程。這個過程中兩個函數調用是挨著的,但是完全可以根據需要讓它們出現在不同的位置。對象關聯可以更好地支持關注分離(separation of concerns)原則,創建和初始化并不需要合并為一個步驟。
### 2.6.3 更簡潔的設計
假定場景:我們有兩個控制器對象,一個用來操作網頁中的登錄表單,另一個用來與服務器進行驗證(通信)。
我們需要一個輔助函數來創建Ajax 通信。我們使用的是jQuery,它不僅可以處理Ajax 并且會返回一個類Promise 的結果,因此我們可以使用`.then(..) `來監聽響應。
在傳統的類設計模式中,我們會把基礎的函數定義在名為Controller 的類中,然后派生兩個子類LoginController 和AuthController,它們都繼承自Controller 并且重寫了一些基礎行為:
~~~
// 父類
function Controller() {
this.errors = [];
}
Controller.prototype.showDialog(title, msg) {
// 給用戶顯示標題和消息
};
Controller.prototype.success = function (msg) {
this.showDialog("Success", msg);
};
Controller.prototype.failure = function (err) {
this.errors.push(err);
this.showDialog("Error", err);
};
// 子類
function LoginController() {
Controller.call(this);
}
// 把子類關聯到父類
LoginController.prototype =
Object.create(Controller.prototype);
LoginController.prototype.getUser = function () {
return document.getElementById("login_username").value;
};
LoginController.prototype.getPassword = function () {
return document.getElementById("login_password").value;
};
LoginController.prototype.validateEntry = function (user, pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure(
"Please enter a username & password!"
);
} else if (user.length < 5) {
return this.failure(
"Password must be 5+ characters!"
);
}
// 如果執行到這里說明通過驗證
return true;
};
// 重寫基礎的failure()
LoginController.prototype.failure = function (err) {
// “super”調用
Controller.prototype.failure.call(
this,
"Login invalid: " + err
);
};
// 子類
function AuthController(login) {
Controller.call(this);
// 合成
this.login = login;
}
// 把子類關聯到父類
AuthController.prototype =
Object.create(Controller.prototype);
AuthController.prototype.server = function (url, data) {
return $.ajax({
url: url,
data: data
});
};
AuthController.prototype.checkAuth = function () {
var user = this.login.getUser();
var pw = this.login.getPassword();
if (this.login.validateEntry(user, pw)) {
this.server("/check-auth", {
user: user,
pw: pw
})
.then(this.success.bind(this))
.fail(this.failure.bind(this));
}
};
// 重寫基礎的success()
AuthController.prototype.success = function () {
// “super”調用
Controller.prototype.success.call(this, "Authenticated!");
};
// 重寫基礎的failure()
AuthController.prototype.failure = function (err) {
// “super”調用
Controller.prototype.failure.call(
this,
"Auth Failed: " + err
);
};
var auth = new AuthController();
auth.checkAuth(
// 除了繼承,我們還需要合成
new LoginController()
);
~~~
所有控制器共享的基礎行為是`success(..)、failure(..) 和showDialog(..)`。子類`LoginController` 和`AuthController` 通過重寫failure(..) 和success(..) 來擴展默認基礎類行為。此外,注意`AuthController` 需要一個`LoginController` 的實例來和登錄表單進行交互,因此這個實例變成了一個數據屬性。
#### 反類
~~~
var LoginController = {
errors: [],
getUser: function () {
return document.getElementById(
"login_username"
).value;
},
getPassword: function () {
return document.getElementById(
"login_password"
).value;
},
validateEntry: function (user, pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure(
"Please enter a username & password!"
);
} else if (user.length < 5) {
return this.failure(
"Password must be 5+ characters!"
);
}
// 如果執行到這里說明通過驗證
return true;
},
showDialog: function (title, msg) {
// 給用戶顯示標題和消息
},
failure: function (err) {
this.errors.push(err);
this.showDialog("Error", "Login invalid: " + err);
}
};
// 讓AuthController 委托LoginController
var AuthController = Object.create(LoginController);
AuthController.errors = [];
AuthController.checkAuth = function () {
var user = this.getUser();
var pw = this.getPassword();
if (this.validateEntry(user, pw)) {
this.server("/check-auth", {
user: user,
pw: pw
})
.then(this.accepted.bind(this))
.fail(this.rejected.bind(this));
}
};
AuthController.server = function (url, data) {
return $.ajax({
url: url,
data: data
});
};
AuthController.accepted = function () {
this.showDialog("Success", "Authenticated!")
};
AuthController.rejected = function (err) {
this.failure("Auth Failed: " + err);
};
~~~
由于AuthController 只是一個對象(LoginController 也一樣),因此不需要實例化(比如new AuthController()),只需要一行代碼就行:
~~~
AuthController.checkAuth();
~~~
借助對象關聯,你可以簡單地向委托鏈上添加一個或多個對象,而且同樣不需要實例化:
~~~
var controller1 = Object.create( AuthController );
var controller2 = Object.create( AuthController );
~~~
在行為委托模式中,AuthController 和LoginController 只是對象,它們之間是兄弟關系,并不是父類和子類的關系。代碼中AuthController 委托了LoginController,反向委托也完全沒問題。
這種模式的重點在于只需要兩個實體(LoginController 和AuthController),而之前的模式需要三個。
### 2.6.4 更好的語法
ES6 的class 語法可以簡潔地定義類方法,這個特性讓class 乍看起來更有吸引力(但應避免使用)
~~~
class Foo {
methodName() { /* .. */ }
}
~~~
在ES6 中可以在任意對象的字面形式中使用簡潔方法聲明(concise methoddeclaration),所以對象關聯風格的對象可以這樣聲明(和class 的語法糖一樣):
~~~
var LoginController = {
errors: [],
getUser() {
// ...
},
getPassword() {
// ...
}
// ...
};
~~~
唯一的區別是對象的字面形式仍然需要使用“,”來分隔元素,而class 語法不需要。
此外,在ES6 中,你可以使用對象的字面形式來改寫之前繁瑣的屬性賦值語法( 比如AuthController 的定義), 然后用`Object.setPrototypeOf(..) `來修改它的`[[Prototype]]`:
~~~
// 使用更好的對象字面形式語法和簡潔方法
var AuthController = {
errors: [],
checkAuth() {
// ...
},
server(url,data) {
// ...
}
// ...
};
// 現在把AuthController 關聯到LoginController
Object.setPrototypeOf( AuthController, LoginController );
~~~
#### 反詞法
簡潔方法有一個非常小但是非常重要的缺點。思考下面的代碼:
~~~
var Foo = {
bar() { /*..*/ },
baz: function baz() { /*..*/ }
};
~~~
去掉語法糖之后的代碼如下所示:
~~~
var Foo = {
bar: function() { /*..*/ },
baz: function baz() { /*..*/ }
};
~~~
由于函數對象本身沒有名稱標識符, 所以bar() 的縮寫形式(`function()..`)實際上會變成一個匿名函數表達式并賦值給bar 屬性。相比之下,具名函數表達式(`function baz()..`)會額外給`.baz` 屬性附加一個詞法名稱標識符baz。
匿名函數沒有name 標識符,這會導致:
* 調試棧更難追蹤;
* 自我引用(遞歸、事件(解除)綁定,等等)更難;
* 代碼(稍微)更難理解。
去掉語法糖的版本使用的是**匿名函數表達式**,通常來說并不會在追蹤棧中添加name,但是簡潔方法很特殊,會給對應的函數對象設置一個內部的name 屬性,這樣理論上可以用在追蹤棧中。(但是追蹤的具體實現是不同的,因此無法保證可以使用。)
簡潔方法無法避免第2 個缺點,它們不具備可以自我引用的詞法標識符。思考下面的代碼:
~~~
var Foo = {
bar: function(x) {
if(x<10){
return Foo.bar( x * 2 );
}
return x;
},
baz: function baz(x) {
if(x < 10){
return baz( x * 2 );
}
return x;
}
};
~~~
在本例中使用`Foo.bar(x*2)` 就足夠了,但是在許多情況下無法使用這種方法,比如多個對象通過代理共享函數、使用this 綁定,等等。這種情況下最好的辦法就是使用函數對象的name 標識符來進行真正的自我引用。使用簡潔方法時一定要小心這一點。如果你需要自我引用的話,那最好使用傳統的具名函數表達式來定義對應的函數(` baz: function baz(){..}`),不要使用簡潔方法。
### 2.6.5 內省
**自省**就是檢查實例的類型。類實例的自省主要目的是通過創建方式來判斷對象的結構和功能。
~~~
function Foo() {
// ...
}
Foo.prototype.something = function(){
// ...
}
var a1 = new Foo();
// 之后
if (a1 instanceof Foo) {
a1.something();
}
~~~
因為Foo.prototype( 不是Foo !) 在a1 的[[Prototype]] 鏈上, 所以instanceof 操作(會令人困惑地)告訴我們a1 是Foo“類”的一個實例。知道了這點后,我們就可以認為a1 有Foo“類”描述的功能。
當然,Foo 類并不存在, 只有一個普通的函數Foo, 它引用了a1 委托的對象(Foo.prototype)。從語法角度來說,instanceof 似乎是檢查a1 和Foo 的關系,但是實際上它想說的是**a1 和Foo.prototype(引用的對象)是互相關聯的。**
之前介紹的抽象的Foo/Bar/b1 例子,簡單來說是這樣的:
~~~
function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
~~~
如果要使用`instanceof` 和`.prototype `語義來檢查例子中實體的關系,那必須這樣做:
~~~
// 讓Foo 和Bar 互相關聯
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf( Bar.prototype )
=== Foo.prototype; // true
Foo.prototype.isPrototypeOf( Bar.prototype ); // true
// 讓b1 關聯到Foo 和Bar
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf( b1 ) === Bar.prototype; // true
Foo.prototype.isPrototypeOf( b1 ); // true
Bar.prototype.isPrototypeOf( b1 ); // true
~~~
還有一種常見但是可能更加脆弱的內省模式,叫“**鴨子類型**”。
舉例來說:
~~~
if (a1.something) {
a1.something();
}
~~~
我們并沒有檢查a1 和委托something() 函數的對象之間的關系,而是假設如果a1 通過了測試a1.something 的話,那a1 就一定能調用.something()(無論這個方法存在于a1 自身還是委托到其他對象)。這個假設的風險其實并不算很高。
ES6 的Promise 就是典型的“鴨子類型”,出于各種各樣的原因,我們需要判斷一個對象引用是否是Promise,但是判斷的方法是檢查對象是否有then() 方法。換句話說,如果對象有then() 方法,ES6 的Promise 就會認為這個對象是“可持續”(thenable)的,因此會期望它具有Promise 的所有標準行為。(如果有一個不是Promise 但是具有then() 方法的對象,那你千萬不要把它用在ES6 的Promise 機制中,否則會出錯。)
對象關聯風格代碼,其內省更加簡潔。
之前的Foo/Bar/b1 對象關聯例子(只包含關鍵代碼):
~~~
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
Bar...
var b1 = Object.create( Bar );
~~~
使用對象關聯時,所有的對象都是通過[[Prototype]] 委托互相關聯,下面是內省的方法,非常簡單:
~~~
// 讓Foo 和Bar 互相關聯
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
// 讓b1 關聯到Foo 和Bar
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
~~~
- 前言
- 第一章 JavaScript簡介
- 第三章 基本概念
- 3.1-3.3 語法、關鍵字和變量
- 3.4 數據類型
- 3.5-3.6 操作符、流控制語句(暫略)
- 3.7函數
- 第四章 變量的值、作用域與內存問題
- 第五章 引用類型
- 5.1 Object類型
- 5.2 Array類型
- 5.3 Date類型
- 5.4 基本包裝類型
- 5.5 單體內置對象
- 第六章 面向對象的程序設計
- 6.1 理解對象
- 6.2 創建對象
- 6.3 繼承
- 第七章 函數
- 7.1 函數概述
- 7.2 閉包
- 7.3 私有變量
- 第八章 BOM
- 8.1 window對象
- 8.2 location對象
- 8.3 navigator、screen與history對象
- 第九章 DOM
- 9.1 節點層次
- 9.2 DOM操作技術
- 9.3 DOM擴展
- 9.4 DOM2和DOM3
- 第十章 事件
- 10.1 事件流
- 10.2 事件處理程序
- 10.3 事件對象
- 10.4 事件類型
- 第十一章 JSON
- 11.1-11.2 語法與序列化選項
- 第十二章 正則表達式
- 12.1 創建正則表達式
- 12.2-12.3 模式匹配與RegExp對象
- 第十三章 Ajax
- 13.1 XMLHttpRequest對象
- 你不知道的JavaScript
- 一、作用域與閉包
- 1.1 作用域
- 1.2 詞法作用域
- 1.3 函數作用域與塊作用域
- 1.4 提升
- 1.5 作用域閉包
- 二、this與對象原型
- 2.1 關于this
- 2.2 全面解析this
- 2.3 對象
- 2.4 混合對象“類”
- 2.5 原型
- 2.6 行為委托
- 三、類型與語法
- 3.1 類型
- 3.2 值
- 3.3 原生函數