你不懂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]] 的行为委托。

相关文章
相关标签/搜索