文章目錄
- 第6章 行為委托
- 6.1 面向委托的設計
- 6.1.1 類理論
- 6.1.2 委托理論
- 1.互相委托(禁止)
- 2.除錯
- 6.1.3 比較思維模型
- 6.2 類與物件
- 6.2.1 控制元件“類”
- ES6的class語法糖
- 6.2.2 委托控制元件物件
- 6.3 更簡潔的設計
- 反類
- 6.4 更好的語法
- 反詞法
- 6.5 內省
- 6.6 小結
- 附錄A ES6中的Class
- A.1 class
- A.2 class陷阱
- A.3 靜態大于動態嗎
- A.4 小結
第6章 行為委托
[[Prototype]]機制就是指物件中的一個內部鏈接參考另一個物件,- 如果在第一個物件上沒有找到需要的屬性或者方法參考,引擎就會繼續在
[[Prototype]]關聯的物件上進行查找,同理,如果在后者中也沒有找到需要的參考就會繼續查找它的[[Prototype]],以此類推,這一系列物件的鏈接被稱為“原型鏈”,- 換句話說,JavaScript中這個機制的本質就是物件之間的關聯關系,
6.1 面向委托的設計
類 和 繼 承 的 設 計 模 式 = > 委 托 行 為 的 設 計 模 式 類和繼承的設計模式 => 委托行為的設計模式 類和繼承的設計模式=>委托行為的設計模式
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 {
// ...
}
- 接下來可以實體化子類XYZ然后使用這些實體來執行任務“XYZ”,
- 這些實體會復制Task定義的通用行為以及XYZ定義的特殊行為,
- 同理,ABC類的實體也會復制Task的行為和ABC的行為,
- 在構造完成后,通常只需要通過這些實體(而不是類)來完成任務,因為每個實體都有需要完成任務的所有方法和屬性,
6.1.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物件,
相比類利用子類重寫父類方法達到的優勢,委托相反,需要避免在[[Prototype]]鏈的不同級別中使用相同的命名
這個設計模式要求盡量少使用容易被重寫的通用方法名,提倡使用更有描述性的方法名,尤其是要寫清相應物件行為的型別,這樣做實際上可以創建出更容易理解和維護的代碼,因為方法名(不僅在定義的位置,而是貫穿整個代碼)更加清晰(自檔案),
this.setID(ID); XYZ中的方法首先會尋找XYZ自身是否有setID(…),但是XYZ中并沒有這個方法名,因此會通過[[Prototype]]委托關聯到Task繼續尋找,這時就可以找到setID(…)方法,此外,由于呼叫位置觸發了this的隱式系結規則,因此雖然setID(…)方法在Task中,運行時this仍然會系結到XYZ,這正是想要的,
委托行為意味著某些物件(XYZ)在找不到屬性或者方法參考時會把這個請求委托給另一個物件(Task)
在API介面的設計中,委托最好在內部實作,不要直接暴露出去,在之前的例子中并沒有讓開發者通過API直接呼叫XYZ.setID(),(當然,可以這么做!)相反,把委托隱藏在了API的內部,XYZ.prepareTask(…)會委托Task.setID(…)
1.互相委托(禁止)
避免參考了一個兩邊都不存在的屬性或者方法時,在[[Prototype]]鏈上產生一個無限遞回的回圈
2.除錯
6.1.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() {
console.log("Hello, " + this.identify() + ".");
};
let b1 = new Bar("b1");
let b2 = new Bar("b2");
b1.speak(); // Hello, I am b1.
b2.speak(); // Hello, I am b2.
子類Bar繼承了父類Foo,然后生成了b1和b2兩個實體,b1委托了Bar.prototype, 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();
- 這段代碼中同樣利用
[[Prototype]]把b1委托給Bar并把Bar委托給Foo- 非常重要的一點是,這段代碼簡潔了許多,只是把物件關聯起來,并不需要那些既復雜又令人困惑的模仿類的行為(建構式、原型以及new),
類風格代碼的思維模型強調物體以及物體間的關系:

簡化版:

物件關聯風格:

物件關聯風格的代碼顯然更加簡潔,因為這種代碼只關注一件事:物件之間的關聯關系,其他的“類”技巧都是非常復雜并且令人困惑的,去掉它們之后,事情會變得簡單許多(同時保留所有功能),
6.2 類與物件
Web開發中非常典型的一種前端場景:創建UI控制元件(按鈕、下拉串列,等等):
6.2.1 控制元件“類”
一個包含所有通用控制元件行為的父類(可能叫作Widget)和繼承父類的特殊控制元件子類(比如Button),
在不使用任何“類”輔助庫或者語法的情況下,使用純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);
} );
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.render($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);
} );
6.2.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同樣只是一個物件,(當然,它會通過委托關聯到Widget!)從設計模式的角度來說,我們并沒有像類一樣在兩個物件中都定義相同的方法名
render(..),相反,我們定義了兩個更具描述性的方法名(insert(..)和build(..)),同理,初始化方法分別叫作init(..)和setup(..),在委托設計模式中,除了建議使用不相同并且更具描述性的方法名之外,還要通過物件關聯避免丑陋的顯式偽多型呼叫(
Widget.call和Widget.prototype.render.call),代之以簡單的相對委托呼叫this.init(..)和this.insert(..),從語法角度來說,我們同樣沒有使用任何建構式、
.prototype或new,實際上也沒必要使用它們,如果你仔細觀察就會發現,之前的一次呼叫(var btn1 = new Button(..))現在變成了兩次(var btn1 = Object.create(Button)和btn1.setup(..)),乍一看這似乎是一個缺點(需要更多代碼),但是這一點其實也是物件關聯風格代碼相比傳統原型風格代碼有優勢的地方,為什么呢?
使用類建構式的話,你需要(并不是硬性要求,但是強烈建議)在同一個步驟中實作構造和初始化,然而,在許多情況下把這兩步分開(就像物件關聯代碼一樣)更靈活,
舉例來說,假如你在程式啟動時創建了一個實體池,然后一直等到實體被取出并使用時才執行特定的初始化程序,這個程序中兩個函式呼叫是挨著的,但是完全可以根據需要讓它們出現在不同的位置,
物件關聯可以更好地支持關注分離(separation of concerns)原則,創建和初始化并不需要合并為一個步驟,
,,,這部分需要好好理解,直接照書書全搬過來了,后面仔細研讀后再歸納,,,
6.3 更簡潔的設計
有兩個控制器物件,一個用來操作網頁中的登錄表單,另一個用來與服務器進行驗證(通信),
需要一個輔助函式來創建Ajax通信,它不僅可以處理Ajax并且會回傳一個類Promise的結果,因此可以使用.then(…)來監聽回應,
類設計模式中,會把基礎的函式定義在名為Controller的類中,然后派生兩個子類LoginController和AuthController,它們都繼承自Controller并且重寫了一些基礎行為:
// 父類
function Controller() {
this.errors = [];
}
Controller.prototype.showDialog = function(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 (pw.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(
// 除了繼承,我們還需要合成
new LoginController()
);
auth.checkAuth();
所有控制器共享的基礎行為是success(…)、failure(…)和showDialog(…),子類LoginController和AuthController通過重寫failure(…)和success(…)來擴展默認基礎類行為,此外,注意AuthController需要一個LoginController的實體來和登錄表單進行互動,因此這個實體變成了一個資料屬性,
另一個需要注意的是我們在繼承的基礎上進行了一些合成,AuthController需要使用LoginController,因此我們實體化后者(new LoginController())并用一個類成員屬性this.login來參考它,這樣AuthController就可以呼叫LoginController的行為,
你可能想讓AuthController繼承LoginController或者相反,這樣我們就通過繼承鏈實作了真正的合成,但是這就是類繼承在問題領域建模時會產生的問題,因為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 (pw.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),而之前的模式需要三個,
.
我們不需要Controller基類來“共享”兩個物體之間的行為,因為委托足以滿足我們需要的功能,同樣,前面提到過,我們也不需要實體化類,因為它們根本就不是類,它們只是物件,此外,我們也不需要合成,因為兩個物件可以通過委托進行合作,
.
最后,我們避免了面向類設計模式中的多型,我們在不同的物件中沒有使用相同的函式名success(…)和failure(…),這樣就不需要使用丑陋的顯示偽多型,相反,在AuthController中它們的名字是accepted(…)和rejected(…)——可以更好地描述它們的行為,
.
總結:我們用一種(極其)簡單的設計實作了同樣的功能,這就是物件關聯風格代碼和行為委托設計模式的力量,
————————
———————————— 以上是書內原文,暫時照搬了,待后續細品,傳統類思維與委托思維需要在實踐中不斷對比,才能越來越體會到之間的內涵!
6.4 更好的語法
- ES6的class語法可以簡潔地定義類方法
- 在ES6中可以在任意物件的字面形式中使用簡潔方法宣告(concisemethod declaration),所以物件關聯風格的物件可以這樣宣告(和class的語法糖一樣):
var LoginController = {
errors: [],
getUser() { // 媽媽再也不用擔心代碼里有function了!
// ...
},
getPassword() {
// ...
}
// ...
};
- 在ES6中,可以使用物件的字面形式(這樣就可以使用簡潔方法定義)來改寫之前繁瑣的屬性賦值語法(比如AuthController的定義),然后用Object. setPrototypeOf(…)來修改它的[[Prototype]]:
// 使用更好的物件字面形式語法和簡潔方法
var AuthController = {
errors: [],
checkAuth() {
// ...
},
server(url, data) {
// ...
}
// ...
};
// 現在把AuthController關聯到LoginController
Object.setPrototypeOf(AuthController, LoginController);
反詞法
匿名函式運算式的三大主要缺點:匿名函式沒有name識別符號,這會導致:
- 除錯堆疊更難追蹤;
- 自我參考(遞回、事件(解除)系結,等等)更難;
- 代碼(稍微)更難理解,
簡潔方法沒有第1和第3個缺點,
去掉語法糖的版本使用的是匿名函式運算式,通常來說并不會在追蹤堆疊中添加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(){…}·),不要使用簡潔方法,
6.5 內省
類實體的內省主要目的是通過創建方式來判斷物件的結構和功能,
instanceof語法會產生語意困惑而且非常不直觀,如果想檢查物件a1和某個物件的關系,那必須使用另一個參考該物件的函式才行——不能直接判斷兩個物件是否關聯,
如果要使用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
顯然這是一種非常糟糕的方法,,,
還有一種常見但是可能更加脆弱的內省模式,許多開發者認為它比instanceof更好,這種模式被稱為“鴨子型別”,這個術語源自這句格言“如果看起來像鴨子,叫起來像鴨子,那就一定是鴨子,”
if (a1.something) {
a1.something();
}
有,即可用,,,但這樣會有一定的風險,,,
使用物件關聯時,所有的物件都是通過[[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
沒有使用instanceof,因為它會產生一些和類有關的誤解,現在問的問題是“你是我的原型嗎?”并不需要使用間接的形式,比如Foo.prototype或者繁瑣的Foo.prototype.isPrototypeOf(..),
…
6.6 小結
行為委托認為物件之間是兄弟關系,互相委托,而不是父類和子類的關系,JavaScript的[[Prototype]]機制本質上就是行為委托機制,
只用物件來設計代碼時,不僅可以讓語法更加簡潔,而且可以讓代碼結構更加清晰,物件關聯(物件之前互相關聯)是一種編碼風格,它倡導的是直接創建和關聯物件,不把它們抽象成類,物件關聯可以用基于[[Prototype]]的行為委托非常自然地實作,
附錄A ES6中的Class
類是一種可選(而不是必須)的設計模式,而且在JavaScript這樣的[[Prototype]]語言中實作類是很別扭的,
繁瑣雜亂的.prototype參考、試圖呼叫原型鏈上層同名函式時的顯式偽多型以及不可靠、不美觀而且容易被誤解成“建構式”的.constructor,
繁瑣雜亂的.prototype參考、試圖呼叫原型鏈上層同名函式時的顯式偽多型以及不可靠、不美觀而且容易被誤解成“建構式”的.constructor,
A.1 class
回顧一下第6章中的Widget/Button例子:
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.render($where);
this.$elem.click(this.onClick.bind(this));
}
onClick(evt) {
console.log("Button '" + this.label + "' clicked! ");
}
}
除了語法更好看之外,ES6還解決了什么問題呢?
-
(基本上,下面會詳細介紹)不再參考雜亂的
.prototype了, -
Button宣告時直接“繼承”了
Widget,不再需要通過Object.create(..)來替換.prototype物件,也不需要設定.__proto__或者Object.setPrototypeOf(..), -
可以通過
super(..)來實作相對多型,這樣任何方法都可以參考原型鏈上層的同名方法,這可以解決第4章提到過的那個問題:建構式不屬于類,所以無法互相參考——super()可以完美解決建構式的問題, -
class字面語法不能宣告屬性(只能宣告方法),看起來這是一種限制,但是它會排除掉許多不好的情況,如果沒有這種限制的話,原型鏈末端的“實體”可能會意外地獲取其他地方的屬性(這些屬性隱式被所有“實體”所“共享”),所以,class語法實際上可以幫助你避免犯錯, -
可以通過
extends很自然地擴展物件(子)型別,甚至是內置的物件(子)型別,比如Array或RegExp,沒有class...extends語法時,想實作這一點是非常困難的,基本上只有框架的作者才能搞清楚這一點,但是現在可以輕而易舉地做到!平心而論,class語法確實解決了典型原型風格代碼中許多顯而易見的(語法)問題和缺點,
A.2 class陷阱
class基本上只是現有[[Prototype]](委托!)機制的一種語法糖
class并不會像傳統面向類的語言一樣在宣告時靜態復制所有行為,如果你(有意或無意)修改或者替換了父“類”中的一個方法,那子“類”和所有實體都會受到影響,因為它們在定義時并沒有進行復制,只是使用基于[[Prototype]]的實時委托
class語法無法定義類成員屬性(只能定義方法),如果為了跟蹤實體之間共享狀態必須要這么做,那你只能使用丑陋的.prototype語法
這種方法最大的問題是,它違背了class語法的本意,在實作中暴露(泄露!)了.prototype,
此外,class語法仍然面臨意外屏蔽的問題:
class C {
constructor(id) {
// 噢,郁悶,我們的id屬性屏蔽了id()方法
this.id = id;
}
id() {
console.log("Id: " + id);
}
}
var c1 = new C("c1");
c1.id(); // TypeError -- c1.id現在是字串"c1"
除此之外,super也存在一些非常細微的問題,可能認為super的系結方法和this類似,也就是說,無論目前的方法在原型鏈中處于什么位置,super總會系結到鏈中的上一層,
然而,出于性能考慮(this系結已經是很大的開銷了), super并不是動態系結的,它會在宣告時“靜態”系結,實際上,每次執行這些操作時都必須重新系結super,
此外,根據應用方式的不同,super可能不會系結到合適的物件,所以可能需要用toMethod(..)來手動系結super(類似用bind(..)來系結this),使用this系結時,把方法應用到不同的物件上,從而可以自動利用this的隱式系結規則,但是這對于super來說是行不通的,
思考下面代碼中super的行為(D和E上):
class P {
foo() { console.log("P.foo"); }
}
class C extends P {
foo() {
super();
}
}
var c1 = new C();
c1.foo(); // "P.foo"
var D = {
foo: function() { console.log("D.foo"); }
};
var E = {
foo: C.prototype.foo
};
// 把E委托到D
Object.setPrototypeOf(E, D);
E.foo(); // "P.foo"
如果認為super會動態系結(非常合理!),那可能期望super()會自動識別出E委托了D,所以E.foo()中的super()應該呼叫D.foo(),
但事實并不是這樣,出于性能考慮,super并不像this一樣是晚系結(late bound,或者說動態系結)的,它在[[HomeObject]].[[Prototype]]上,[[HomeObject]]會在創建時靜態系結,
在本例中,super()會呼叫P.foo(),因為方法的[[HomeObject]]仍然是C, C.[[Prototype]]是P,確實可以手動修改super系結,使用toMethod(..)系結或重新系結方法的[[HomeObject]](就像設定物件的[[Prototype]]一樣!)就可以解決本例的問題:
var D = {
foo: function() { console.log("D.foo"); }
};
// 把E委托到D
var E = Object.create(D);
// 手動把foo的[[HomeObject]]系結到E, E.[[Prototype]]是D,所以super()是D.foo()
E.foo = C.prototype.foo.toMethod(E, "foo");
E.foo(); // "D.foo"
toMethod(..)會復制方法并把homeObject當作第一個引數(也就是傳入的E),第二個引數(可選)是新方法的名稱(默認是原方法名),
無論如何,對于引擎自動系結的super來說,必須時刻警惕是否需要進行手動系結,,,
A.3 靜態大于動態嗎
ES6的class最大的問題在于,(像傳統的類一樣)它的語法有時會讓人認為,定義了一個class后,它就變成了一個(未來會被實體化的)東西的靜態定義,會讓人徹底忽略C是一個物件,是一個具體的可以直接互動的東西,
在傳統面向類的語言中,類定義之后就不會進行修改,所以類的設計模式就不支持修改,但是JavaScript最強大的特性之一就是它的動態性,任何物件的定義都可以修改(除非把它設定成不可變),
class似乎不贊成這樣做,所以強制讓你使用丑陋的.prototype語法以及super問題,等等,而且對于這種動態產生的問題,class基本上都沒有提供解決方案,
總地來說,ES6的class想偽裝成一種很好的語法問題的解決方案,但是實際上卻讓問題更難解決而且讓JavaScript更加難以理解,
A.4 小結
class很好地偽裝成JavaScript中類和繼承設計模式的解決方案,但是它實際上起到了反作用:它隱藏了許多問題并且帶來了更多更細小但是危險的問題,
這部分可以算是上卷中最難啃的了,不僅需要一定的知識儲備(包括面向物件),還需要大量的實踐以及對文中所提到的痛點的感悟,,,,重在理解,后續慢慢來吧,多多回顧,,,
———— 2021.07.28晚,勉強瀏覽完,,,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/291327.html
標籤:其他
