你不懂js系列學習筆記-this與對象原型- 06

第六章: 行爲委託

原文:You-Dont-Know-JSjavascript

簡單地複習一下第五章的結論,[[Prototype]] 機制是一種存在於一個對象上的內部連接,它指向一個其餘對象。java

當一個屬性/方法引用在一個對象上發生,而這樣的屬性/方法又不存在時,這個連接就會被使用。在這種狀況下,[[Prototype]] 連接告訴引擎去那個被連接的對象上尋找該屬性/方法。接下來,若是那個對象也不能知足查詢,就沿着它的 [[Prototype]] 查詢,如此繼續。這種對象間的一系列連接構成了所謂的「原形鏈」。git

換句話說,對於咱們能在 JavaScript 中利用的功能的實際機制來講,其重要的實質 所有在於被鏈接到其餘對象的對象。github

這個觀點是理解本章其他部分的動機和方法的重要基礎!ajax

1. 邁向面向委託的設計

1.1 類理論

比方說咱們有幾個類似的任務(「XYZ」,「ABC」,等)須要在咱們的軟件中建模。編程

使用類,你設計這個場景的方式是:定義一個泛化的父類(基類)好比 Task,爲全部的「同類」任務定義共享的行爲。而後,你定義子類 XYZABC,它們都繼承自 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 的行爲。在構建完成以後,你一般僅會與這些實例交互(而不是類),由於每一個實例都拷貝了完成計劃任務的全部行爲。閉包

1.2 委託理論

可是如今讓咱們試着用 行爲委託 代替 來思考一樣的問題。

你將首先定義一個稱爲 Task對象(不是一個類,也不是一個大多數 JS 開發者想讓你相信的 function),並且它將擁有具體的行爲,這些行爲包含各類任務可使用的(讀做:委託至!)工具方法。而後,對於每一個任務(「XYZ」,「ABC」),你定義一個 對象 來持有這個特定任務的數據/行爲。你 連接 你的特定任務對象到 Task 工具對象,容許它們在必要的時候能夠委託到它。

基本上,你認爲執行任務「XYZ」就是從兩個兄弟/對等的對象(XYZTask)中請求行爲來完成它。與其經過類的拷貝將它們組合在一塊兒,咱們能夠將它們保持在分離的對象中,並且能夠在須要的狀況下容許 XYZ 對象 委託到 Task

這裏是一些簡單的代碼,示意你如何實現它:

var Task = {
  setID: function(ID) {
    this.id = ID;
  },
  outputID: function() {
    console.log(this.id);
  }
};

// 使 `XYZ` 委託到 `Task`
var 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 ... = ...
複製代碼

在這段代碼中,TaskXYZ不是類(也不是函數),它們 僅僅是對象XYZ 經過 Object.create() 建立,來 [[Prototype]] 委託到 Task 對象。

做爲與面向類(也就是,OO —— 面向對象)的對比,我稱這種風格的代碼爲 「OLOO」(objects-linked-to-other-objects(連接到其餘對象的對象))。全部咱們 真正 關心的是,對象 XYZ 委託到對象 Task(對象 ABC 也同樣)。

在 JavaScript 中,[[Prototype]] 機制將 對象 連接到其餘 對象。不管你多麼想說服本身這不是真的,JavaScript 沒有像「類」那樣的抽象機制。這就像逆水行舟:你 能夠 作到,但你 選擇 了逆流而上,因此很明顯地,你會更困難地達到目的地。

OLOO 風格的代碼 中有一些須要注意的不一樣:

  1. 前一個類的例子中的 idlabel 數據成員都是 XYZ 上的直接數據屬性(它們都不在 Task 上)。通常來講,當 [[Prototype]] 委託引入時,你想使狀態保持在委託者上XYZABC),不是在委託上(Task)。
  2. 在類的設計模式中,咱們故意在父類(Task)和子類(XYZ)上採用相同的命名 outputTask,以致於咱們能夠利用覆蓋(多態)。在委託的行爲中,咱們反其道而行之:咱們盡一切可能避免在 [[Prototype]] 鏈的不一樣層級上給出相同的命名(稱爲「遮蔽」 —— 見第五章),由於這些命名衝突會致使尷尬/脆弱的語法來消除引用的歧義(見第四章),而咱們想避免它。 這種設計模式不那麼要求那些傾向於被覆蓋的泛化的方法名,而是要求針對於每一個對象的 具體 行爲類型給出更具描述性的方法名。這實際上會產生更易於理解/維護的代碼,由於方法名(不只在定義的位置,而是擴散到其餘代碼中)變得更加明白(代碼即文檔)。
  3. this.setID(ID); 位於對象 XYZ 的一個方法內部,它首先在 XYZ 上查找 setID(..),但由於它不能在 XYZ 上找到叫這個名稱的方法,[[Prototype]] 委託意味着它能夠沿着連接到 Task 來尋找 setID(),這樣固然就找到了。另外,因爲調用點的隱含 this 綁定規則(見第二章),當 setID() 運行時,即使方法是在 Task 上找到的,這個函數調用的 this綁定依然是咱們指望和想要的 XYZ。咱們在代碼稍後的 this.outputID() 中也看到了一樣的事情。 換句話說,咱們可使用存在於 Task 上的泛化工具與 XYZ 互動,由於 XYZ 能夠委託至 Task

行爲委託 意味着:在某個對象(XYZ)的屬性或方法沒能在這個對象(XYZ)上找到時,讓這個對象(XYZ)爲屬性或方法引用提供一個委託(Task)。

這是一個 極其強大 的設計模式,與父類和子類,繼承,多態等有很大的不一樣。與其在你的思惟中縱向地,從上面父類到下面子類地組織對象,你應當並列地,對等地考慮對象,並且對象間擁有方向性的委託連接。

注意: 委託更適於做爲內部實現的細節,而不是直接暴露在 API 接口的設計中。在上面的例子中,咱們的 API 設計不必有意地讓開發者調用 XYZ.setID()(固然咱們能夠!)。咱們以某種隱藏的方式將委託做爲咱們 API 的內部細節,即 XYZ.prepareTask(..) 委託到 Task.setID(..)

相互委託(不容許)

你不能在兩個或多個對象間相互地委託(雙向地)對方來建立一個 循環 。若是你使 B 連接到 A,而後試着讓 A 連接到 B,那麼你將獲得一個錯誤。

這樣的事情不被容許有些惋惜(不是很是使人驚訝,但稍稍有些惱人)。若是你製造一個在任意一方都不存在的屬性/方法引用,你就會在 [[Prototype]] 上獲得一個無限遞歸的循環。但若是全部的引用都嚴格存在,那麼 B 就能夠委託至 A,或相反,並且它能夠工做。這意味着你能夠爲了多種任務用這兩個對象互相委託至對方。有一些狀況這可能會有用。

但它不被容許是由於引擎的實現者發現,在設置時檢查(並拒絕!)無限循環引用一次,要比每次你在一個對象上查詢屬性時都作相同檢查的性能要高。

調試

考慮這段傳統的「類構造器」風格的 JS 代碼,正如它將在 Chrome 開發者工具 控制檯 中出現的:

function Foo() {}

var a1 = new Foo();

a1; // Foo {}
複製代碼

讓咱們看一下這個代碼段的最後一行:對錶達式 a1 進行求值的輸出,打印 Foo {}。若是你在 FireFox 中試用一樣的代碼,你極可能會看到 Object {}。爲何會有不一樣?這些輸出意味着什麼?

Chrome 實質上在說「{} 是一個由名爲‘Foo’的函數建立的空對象」。Firefox 在說「{} 是一個由 Object 普通構建的空對象」。這種微妙的區別是由於 Chrome 在像一個 內部屬性 同樣,動態跟蹤執行建立的實際方法的名稱,而其餘瀏覽器不會跟蹤這樣的附加信息。

考慮下面的代碼:

function Foo() {}

var a1 = new Foo();

Foo.prototype.constructor = function Gotcha() {};

a1.constructor; // Gotcha(){}
a1.constructor.name; // "Gotcha"

a1; // Foo {}
複製代碼

即使咱們將 a1.constructor.name 合法地改變爲其餘的東西(「Gotcha」),Chrome 控制檯依舊使用名稱「Foo」。

1.3 思惟模型比較

如今你至少在理論上能夠看到「類」和「委託」設計模式的不一樣了,讓咱們看看這些設計模式在咱們用來推導咱們代碼的思惟模型上的含義。

咱們將查看一些更加理論上的(「Foo」,「Bar」)代碼,而後比較兩種方法(OO vs. OLOO)的代碼實現。第一段代碼使用經典的(「原型的」)OO 風格:

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();
複製代碼

父類 Foo,被子類 Bar 繼承,以後 Bar 被初始化兩次:b1b2。咱們獲得的是 b1 委託至 Bar.prototypeBar.prototype 委託至 Foo.prototype。這對你來講應當看起來十分熟悉。沒有太具開拓性的東西發生。

如今,讓咱們使用 OLOO 風格的代碼 實現徹底相同的功能:

var Foo = {
  init: function(who) {
    this.me = who;
  },
  identify: function() {
    return "I am " + this.me;
  }
};

var 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();
複製代碼

咱們利用了徹底相同的從 BarFoo[[Prototype]] 委託,正如咱們在前一個代碼段中 b1Bar.prototype,和 Foo.prototype 之間那樣。咱們仍然有三個對象連接在一塊兒。

但重要的是,咱們極大地簡化了發生的 全部其餘事項,由於咱們如今僅僅創建了相互連接的 對象,而不須要全部其餘討厭且困惑的看起來像類(但動起來不像)的東西,還有構造器,原型和 new 調用。

首先,類風格的代碼段意味着這樣的實體與它們的關係的思惟模型:

img

OLOO 風格代碼的思惟模型:

img

正如你比較它們所獲得的,十分明顯,OLOO 風格的代碼 須要關心的東西少太多了,由於 OLOO 風格代碼接受了 事實:咱們惟一須要真正關心的事情是 連接到其餘對象的對象。

2.更簡單的設計

OLOO 除了提供表面上更簡單(並且更靈活!)的代碼以外,行爲委託做爲一個模式實際上會帶來更簡單的代碼架構。讓咱們講解最後一個例子來講明 OLOO 是如何簡化你的總體設計的。

這個場景中咱們將講解兩個控制器對象,一個用來處理網頁的登陸 form(表單),另外一個實際處理服務器的認證(通訊)。

咱們須要幫助工具來進行與服務器的 Ajax 通訊。咱們將使用 JQuery(雖然其餘的框架均可以),由於它不只爲咱們處理 Ajax,並且還返回一個相似 Promise 的應答,這樣咱們就能夠在代碼中使用 .then(..) 來監聽這個應答。

注意: 咱們不會再這裏講到 Promise,但咱們會在之後的 你不懂 JS 系列中講到。

根據典型的類的設計模式,咱們在一個叫作 Controller 的類中將任務分解爲基本功能,以後咱們會衍生出兩個子類,LoginControllerAuthController,它們都繼承自 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(..)。咱們的子類 LoginControllerAuthController 覆蓋了 failure(..)success(..) 來加強基本類的行爲。還要注意的是,AuthController 須要一個 LoginController 實例來與登陸 form 互動,因此它變成了一個數據屬性成員。

另一件要提的事情是,咱們選擇一些 合成 散佈在繼承的頂端。AuthController 須要知道 LoginController,因此咱們初始化它(new LoginController()),並用一個稱爲 this.login 的類屬性成員來引用它,這樣 AuthController 才能夠調用 LoginController 上的行爲。

注意: 這裏可能會存在一絲衝動,就是使 AuthController 繼承 LoginController,或者反過來,這樣的話咱們就會經過繼承鏈獲得 虛擬合成。可是這是一個很是清晰的例子,代表對這個問題來說,將類繼承做爲模型有什麼問題,由於 AuthControllerLoginController 都不特化對方的行爲,因此它們之間的繼承沒有太大的意義,除非類是你惟一的設計模式。與此相反的是,咱們在一些簡單的合成中分層,而後它們就能夠合做了,同時它倆都享有繼承自父類 Controller 的好處。

若是你熟悉面向類(OO)的設計,這都應該看起來十分熟悉和天然。

去類化

可是,咱們真的須要用一個父類,兩個子類,和一些合成來對這個問題創建模型嗎?有辦法利用 OLOO 風格的行爲委託獲得 簡單得多 的設計嗎?有的!

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();
複製代碼

固然,經過 OLOO,若是你確實須要在委託鏈上建立一個或多個附加的對象時也很容易,並且仍然不須要任何像類實例化那樣的東西:

var controller1 = Object.create(AuthController);
var controller2 = Object.create(AuthController);
複製代碼

使用行爲委託,AuthControllerLoginController 僅僅是對象,互相是 水平 對等的,並且沒有被安排或關聯成面向類中的父與子。咱們有些隨意地選擇讓 AuthController 委託至 LoginController —— 相反方向的委託也一樣是有效的。

第二個代碼段的主要要點是,咱們只擁有兩個實體(LoginController and AuthController),而 不是以前的三個。

咱們不須要一個基本的 Controller 類來在兩個子類間「分享」行爲,由於委託是一種能夠給咱們所需功能的,足夠強大的機制。同時,就像以前注意的,咱們也不須要實例化咱們的對象來使它們工做,由於這裏沒有類,只有對象自身。 另外,這裏不須要 合成 做爲委託來給兩個對象 差別化 地合做的能力。

最後,因爲沒有讓名稱 success(..)failure(..) 在兩個對象上相同,咱們避開了面向類的設計的多態陷阱:它將會須要難看的顯式假想多態。相反,咱們在 AuthController 上稱它們爲 accepted()rejected(..) —— 對於它們的具體任務來講,稍稍更具描述性的名稱。

底線: 咱們最終獲得了相同的結果,可是用了(顯著的)更簡單的設計。這就是 OLOO 風格代碼和 行爲委託 設計模式的力量。

3. 更好的語法

一個使 ES6 class 看似如此誘人的更好的東西是(見附錄 A 來了解爲何要避免它!),聲明類方法的速記語法:

class Foo {
  methodName() {
    /* .. */
  }
}
複製代碼

咱們從聲明中扔掉了單詞 function,這使全部的 JS 開發者歡呼!

你可能已經注意到,並且爲此感到沮喪:上面推薦的 OLOO 語法出現了許多 function,這看起來像是對 OLOO 簡化目標的詆譭。但它沒必要是!

在 ES6 中,咱們能夠在任何字面對象中使用 簡約方法聲明,因此一個 OLOO 風格的對象能夠用這種方式聲明(與 class 語法中相同的語法糖):

var LoginController = {
  errors: [],
  getUser() {
    // 看,沒有 `function`!
    // ...
  },
  getPassword() {
    // ...
  }
  // ...
};
複製代碼

惟一的區別是字面對象的元素間依然須要 , 逗號分隔符,而 class 語法沒必要如此。這是在整件事情上很小的讓步。

還有,在 ES6 中,一個你使用的更笨重的語法(好比 AuthController 的定義中):你一個一個地給屬性賦值而不使用字面對象,能夠改寫爲使用字面對象(因而你可使用簡約方法),並且你可使用 Object.setPrototypeOf(..) 來修改對象的 [[Prototype]],像這樣:

// 使用更好的字面對象語法 w/ 簡約方法!
var AuthController = {
  errors: [],
  checkAuth() {
    // ...
  },
  server(url, data) {
    // ...
  }
  // ...
};

// 如今, 連接 `AuthController` 委託至 `LoginController`
Object.setPrototypeOf(AuthController, LoginController);
複製代碼

ES6 中的 OLOO 風格,與簡明方法一塊兒,變得比它之前 友好得多(即便在之前,它也比經典的原型風格代碼簡單好看的多)。 你沒必要非得選用類(複雜性)來獲得乾淨漂亮的對象語法!

沒有詞法

簡約方法確實有一個缺點,一個重要的細節。考慮這段代碼:

var Foo = {
  bar() {
    /*..*/
  },
  baz: function baz() {
    /*..*/
  }
};
複製代碼

這是去掉語法糖後,這段代碼將如何工做:

var Foo = {
  bar: function() {
    /*..*/
  },
  baz: function baz() {
    /*..*/
  }
};
複製代碼

看到區別了?bar() 的速記法變成了一個附着在 bar 屬性上的 匿名函數表達式function()..),由於函數對象自己沒有名稱標識符。和擁有詞法名稱標識符 baz,附着在 .baz 屬性上的手動指定的 命名函數表達式function baz()..)作個比較。

那又怎麼樣?在 「你不懂 JS」 系列的 「做用域與閉包」 這本書中,咱們詳細講解了 匿名函數表達式 的三個主要缺點。咱們簡單地重複一下它們,以便於咱們和簡明方法相比較。

一個匿名函數缺乏 name 標識符:

  1. 使調試時的棧追蹤變得困難
  2. 使自引用(遞歸,事件綁定等)變得困難
  3. 使代碼(稍稍)變得難於理解

第一和第三條不適用於簡明方法。

雖然去掉語法糖使用 匿名函數表達式 通常會使棧追蹤中沒有 name。簡明方法在語言規範中被要求去設置相應的函數對象內部的 name 屬性,因此棧追蹤應當可使用它(這是依賴於具體實現的,因此不能保證)。

不幸的是,第二條 仍然是簡明方法的一個缺陷。 它們不會有詞法標識符用來自引用。考慮:

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(){..}

4. 自省

若是你花了很長時間在面向類的編程方式(無論是 JS 仍是其餘的語言)上,你可能會對 類型自省 很熟悉:自省一個實例來找出它是什麼 種類 的對象。在類的實例上進行 類型自省 的主要目的是根據 對象是如何建立的 來推斷它的結構/能力。

考慮這段代碼,它使用 instanceof(見第五章)來自省一個對象 a1 來推斷它的能力:

function Foo() {
  // ...
}
Foo.prototype.something = function() {
  // ...
};

var a1 = new Foo();

// 稍後

if (a1 instanceof Foo) {
  a1.something();
}
複製代碼

由於 Foo.prototype(不是 Foo!)在 a1[[Prototype]] 鏈上(見第五章),instanceof 操做符(令人困惑地)僞裝告訴咱們 a1 是一個 Foo 「類」的實例。有了這個知識,咱們假定 a1Foo 「類」中描述的能力。

固然,這裏沒有 Foo 類,只有一個普通的函數 Foo,它剛好擁有一個引用指向一個隨意的對象(Foo.prototype),而 a1剛好委託連接至這個對象。經過它的語法,instanceof 僞裝檢查了 a1Foo 之間的關係,但它實際上告訴咱們的是 a1Foo.prototype(這個隨意被引用的對象)是否有關聯。

instanceof 在語義上的混亂(和間接)意味着,要使用以 instanceof 爲基礎的自省來查詢對象 a1 是否與討論中的對象有關聯,你 不得不 擁有一個持有對這個對象引用的函數 —— 你不能直接查詢這兩個對象是否有關聯。

回想本章前面的抽象 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
複製代碼

能夠說,其中有些爛透了。舉個例子,直覺上(用類)你可能想說這樣的東西 Bar instanceof Foo(由於很容易混淆「實例」的意義認爲它包含「繼承」),但在 JS 中這不是一個合理的比較。你不得不說 Bar.prototype instanceof Foo

另外一個常見,但也許健壯性更差的 類型自省 模式叫「duck typing(鴨子類型)」,比起 instanceof 來許多開發者都傾向於它。這個術語源自一則諺語,「若是它看起來像鴨子,叫起來像鴨子,那麼它必定是一隻鴨子」。

例如:

if (a1.something) {
  a1.something();
}
複製代碼

與其檢查 a1 和一個持有可委託的 something() 函數的對象的關係,咱們假設 a1.something 測試經過意味着 a1 有能力調用 .something()(無論是直接在 a1 上直接找到方法,仍是委託至其餘對象)。就其自己而言,這種假設沒什麼風險。

可是「鴨子類型」經常被擴展用於 除了被測試關於對象能力之外的其餘假設,這固然會在測試中引入更多風險(好比脆弱的設計)。

「鴨子類型」的一個值得注意的例子來自於 ES6 的 Promises(就是咱們前面解釋過,將再也不本書內涵蓋的內容)。

因爲種種緣由,須要斷定任意一個對象引用是否 是一個 Promise,但測試是經過檢查對象是否剛好有 then() 函數出如今它上面來完成的。換句話說,若是任何對象 剛好有一個 then() 方法,ES6 的 Promises 將會無條件地假設這個對象 是「thenable」的,並且所以會指望它按照全部的 Promises 標準行爲那樣一致地動做。

若是你有任何非 Promise 對象,而卻無論由於什麼它剛好擁有 then() 方法,你會被強烈建議使它遠離 ES6 的 Promise 機制,來避免破壞這種假設。

這個例子清楚地展示了「鴨子類型」的風險。你應當僅在可控的條件下,保守地使用這種方式。

再次將咱們的注意力轉向本章中出現的 OLOO 風格的代碼,類型自省 變得清晰多了。讓咱們回想(並縮寫)本章的 Foo / Bar / b1 的 OLOO 示例:

var Foo = { /* .. */ };

var Bar = Object.create( Foo );
Bar...

var b1 = Object.create( Bar );
複製代碼

使用這種 OLOO 方式,咱們所擁有的一切都是經過 [[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(..) 來間接地查詢了。

我想能夠說這些檢查比起前面一組自省檢查,極大地減小了複雜性/混亂。又一次,咱們看到了在 JavaScript 中 OLOO 要比類風格的編碼簡單(但有着相同的力量)。

複習

在你的軟件體系結構中,類和繼承是你能夠 選用不選用 的設計模式。多數開發者理所固然地認爲類是組織代碼的惟一(正確的)方法,但咱們在這裏看到了另外一種不太常被提到的,但實際上十分強大的設計模式:行爲委託。

行爲委託意味着對象彼此是對等的,在它們本身當中相互委託,而不是父類與子類的關係。JavaScript 的 [[Prototype]] 機制的設計本質,就是行爲委託機制。這意味着咱們能夠選擇掙扎着在 JS 上實現類機制,也能夠欣然接受 [[Prototype]] 做爲委託機制的本性。

當你僅用對象設計代碼時,它不只能簡化你使用的語法,並且它還能實際上引領更簡單的代碼結構設計。

OLOO(連接到其餘對象的對像)是一種沒有類的抽象,而直接建立和關聯對象的代碼風格。OLOO 十分天然地實現了基於 [[Prototype]] 的行爲委託。

相關文章
相關標籤/搜索