【第773期】你不懂JS:行为委托

图片

前言前端

这一系列的分享,好多伙伴都说文章太长了。平时都讲究快阅读,有时候能够试试慢阅读,何不挑战下本身呢?今天分享的是this与对象原型的倒数第二章。今天继续由前端早读课专栏做者@HetfieldJoe带来连载《你不懂JS》的分享。编程


正文从这开始~设计模式


你不懂JS:this与对象原型 第六章:行为委托浏览器


【第772期】你不懂JS:原型(Prototype)中,咱们详细地讨论了[[Prototype]]机制,和 为何 对于描述“类”或“继承”来讲它是那么令人糊涂和不合适。咱们一路跋涉,不只涉及了至关繁冗的语法(使代码凌乱的.prototype),还有各类陷阱(好比令人吃惊的.constructor解析和难看的假想多态语法)。咱们探索了许多人试图用抹平这些粗糙的区域而使用的各类“mixin”方法。服务器


这时一个常见的反应是,想知道为何这些看起来如此简单的事情这么复杂。如今咱们已经拉开帷幕看到了它是多么麻烦,这并不奇怪:大多数JS开发者从不探究得这么深,而将这一团糟交给一个“类”包去帮他们处理。闭包


我但愿到如今你不会甘心于敷衍了事并把这样的细节丢给一个“黑盒”库。如今咱们来深刻讲解咱们 如何与应当如何 以一种比类形成的困惑 简单得多并且更直接的方式 来考虑JS中对象的[[Prototype]]机制。架构


简单地复习一下第五章的结论,[[Prototype]]机制是一种存在于一个对象上的内部连接,它指向一个其余对象。app


当一个属性/方法引用在第一个对象上发生,而这样的属性/方法又不存在时,这个连接就会被使用。在这种状况下,[[Prototype]]连接告诉引擎去那个被连接的对象上寻找该属性/方法。接下来,若是那个对象也不能知足查询,就沿着它的[[Prototype]]查询,如此继续。这种对象间一系列的连接构成了所谓的“原形链”。框架


换句话说,对于咱们能在JavaScript中利用的功能的实际机制来讲,其重要的实质 所有在于被链接到其余对象的对象。ide


这个观点是理解本章其他部分的动机和方法的重要基础!


迈向面向委托的设计

为了将咱们的思想恰当地集中在如何用最直截了当的方法使用[[Prototype]],咱们必须认识到它表明一种根本上与类不一样的设计模式(【第767期】你不懂JS:混合(淆)“类”的对象)。


注意* 某些 面相类的设计依然是颇有效的,因此不要扔掉你知道的每一件事(扔掉大多数就好了!)。好比,封装 就十分强大,并且与委托兼容的(虽然不那么常见)。


咱们须要试着将咱们的思惟从类/继承的设计模式转变为行为代理设计模式。若是你已经用在教育/工做生涯中思考类的方式作了大多数或全部的编程工做,这可能感受不舒服或不天然。你可能须要尝试这种思惟过程好几回,才能适应这种很是不一样的思考方式。


我将首先带你进行一些理论练习,以后咱们会一对一地看一些更实际的例子来为你本身的代码提供实践环境。


类理论

比方说咱们有几个类似的任务(“XYZ”,“ABC”,等)须要在咱们的软件中建模。


使用类,你设计这个场景的方式是:定义一个泛化的父类(基类)好比Task,为全部的“同类”任务定义共享的行为。而后,你定义子类XYZ和ABC,它们都继承自Task,每一个都分别添加了特化的行为来处理各自的任务。


重要的是, 类设计模式将鼓励你发挥继承的最大功效,当你在XYZ任务中覆盖Task的某些泛化方法的定义时,你将会想利用方法覆盖(和多态),也许会利用super来调用这个方法泛化版本,为它添加更多的行为。你极可能会找到几个能够“抽象”到父类中,或在子类中特化(覆盖)的地方。


这是一些关于这个场景的假想代码:

图片


如今,你能够初始化一个或多个XYZ子类的 拷贝,而且使用这些实例来执行“XYZ”任务。这些实例已经 同时拷贝 了泛化的Task定义的行为和具体的XYZ定义的行为。相似地,ABC类的实例将拷贝Task的行为和具体的ABC的行为。在构建完成以后,你通常会仅与这些实例互动(而不是类),由于每一个实例都拷贝了完成计划任务的全部行为。


委托理论

可是如今然咱们试着用 行为委托 代替 类 来思考一样的问题。


你将首先定义一个称为Task的 对象(不是一个类,也不是一个大多数JS开发者想让你相信的function),并且它将拥有具体的行为,这些行为包含各类任务可使用的(读做:委托至!)工具方法。而后,对于每一个任务(“XYZ”,“ABC”),你定义一个 对象 来持有这个特定任务的数据/行为。你 连接 你的特定任务对象到Task工具对象,容许它们在必要的时候能够委托到它。


基本上,你认为执行任务“XYZ”就是从两个兄弟/对等的对象(XYZ和Task)中请求行为来完成它。与其经过类的拷贝将它们组合在一块儿,咱们能够将他们保持在分离的对象中,并且能够在须要的状况下容许XYZ对象来 委托到 Task。


这里是一些简单的代码,示意你如何实现它:

图片


在这段代码中,Task和XYZ不是类(也不是函数),它们 仅仅是对象。XYZ经过Object.create()建立,来[[Prototype]]委托到Task对象(见第五章)。


做为与面相类(也就是,OO——面相对象)的对比,我称这种风格的代码为 “OLOO”(objects-linked-to-other-objects(连接到其余对象的对象))。全部咱们 真正 关心的是,对象XYZ委托到对象Task(对象ABC也同样)。


在JavaScript中,[[Prototype]]机制将 对象 连接到其余 对象。不管你多么想说服本身这不是真的,JavaScript没有像“类”那样的抽象机制。这就像逆水行舟:你 能够 作到,但你 选择 了逆流而上,因此很明显地,你会更困难地达到目的地。


OLOO风格的代码 中有一些须要注意的不一样:

  • 前一个类的例子中的id和label数据成员都是XYZ上的直接数据属性(它们都不在Task上)。通常来讲,当[[Prototype]]委托引入时,你想使状态保持在委托者上(XYZ,ABC),不是在委托上(Task)。

  • 在类的设计模式中,咱们故意在父类(Task)和子类(XYZ)上采用相同的命名outputTask,以致于咱们能够利用覆盖(多态)。在委托的行为中,咱们反其道而行之:咱们尽一切可能避免在[[Prototype]]链的不一样层级上给出相同的命名(称为“遮蔽”——见第五章),由于这些命名冲突会致使尴尬/脆弱的语法来消除引用的歧义(见第四章),而咱们想避免它。

  • 这种设计模式不那么要求那些倾向于被覆盖的泛化的方法名,而是要求针对于每一个对象的 具体 行为类型给出更具描述性的方法名。这实际上会产生更易于理解/维护的代码,由于方法名(不只在定义的位置,而是扩散到其余代码中)变得更加明白(代码即文档)。

  • 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的开发者工具中观察到的。


考虑这段传统的“类构造器”风格的JS代码,正如它将在Chrome开发者工具 控制台 中出现的:

图片


让咱们看一下这个代码段的最后一行:对表达式a1进行求值的输出,打印Foo {}。若是你在FireFox中试用一样的代码,你极可能会看到Object {}。为何会有不一样?这些输出意味着什么?


Chrome实质上在说“{}是一个由名为‘Foo’的函数建立的空对象”。Firefox在说“{}是一个由Object普通构建的空对象”。这种微妙的区别是由于Chrome在像一个 内部属性 同样,动态跟踪执行建立的实际方法的名称,而其余浏览器不会跟踪这样的附加信息。


试图用JavaScript机制来解释它很吸引人:

图片


那么,Chrome就是经过简单地查看对象的.Constructor.name来输出“Foo”的?使人费解的是,答案既是“是”也是“不”。


考虑下面的代码:

图片

即使咱们将a1.constructor.name合法地改变为其余的东西(“Gotcha”),Chrome控制台依旧使用名称“Foo”。


那么,说明前面问题(它使用.constructor.name吗?)的答案是 不,他必定在内部追踪其余的什么东西。


可是,且慢!让咱们看看这种行为如何与OLOO风格的代码一块儿工做:

图片

啊哈!Gotcha,Chrome的控制台 确实 寻找而且使用了.constructor.name。实际上,就在写这本书的时候,正是这个行为被认定为是Chrome的一个Bug,并且就在你读到这里的时候,它可能已经被修复了。因此你可能已经看到了被修改过的 a1; // Object{}。


这个bug暂且不论,Chrome执行的(刚刚在代码段中展现的)“构造器名称”内部追踪(目前仅用于调试输出的目的),是一个仅在Chrome内部存在的扩张行为,它已经超出了JS语言规范要求的范围。


若是你不使用“构造器”来制造你的对象,就像咱们在本章的OLOO风格代码中不鼓励的那样,那么你将会获得一个Chrome不会为其追踪内部“构造器名称”的对象,因此这样的对象将正确地仅仅被输出“Object {}”,意味着“从Object()构建生成的对象”。


不要认为 这表明一个OLOO风格代码的缺点。当你用OLOO编码并且用行为代理做为你的设计模式时,谁 “建立了”(也就是,哪一个函数 被和new一块儿调用了?)一些对象是一个无关的细节。Chrome特殊的内部“构造器名称”追踪仅仅在你彻底接受“类风格”编码时才有用,而在你接受OLOO委托时是没有意义的。


思惟模型比较

如今你至少在理论上能够看到“类”和“委托”设计模式的不一样了,让咱们看看这些设计模式在咱们用来推导咱们代码的思惟模型上的含义。


咱们将查看一些更加理论上的(“Foo”,“Bar”)代码,而后比较两种方法(OO vs. OLOO)的代码实现。第一段代码使用经典的(“原型的”)OO风格:

图片

父类Foo,被子类Bar继承,以后Bar被初始化两次:b1和b2。咱们获得的是b1委托至Bar.prototype,Bar.prototype委托至Foo.prototype。这对你来讲应当看起来十分熟悉。没有太具开拓性的东西发生。


如今,让咱们使用 OLOO 风格的代码 实现彻底相同的功能:

image.png

咱们利用了彻底相同的从Bar到Foo的[[Prototype]]委托,正如咱们在前一个代码段中b1,Bar.prototype,和Foo.prototype之间那样。咱们仍然有3个对象连接在一块儿。


但重要的是,咱们极大地简化了发生的 全部其余事项,由于咱们如今仅仅创建了相互连接的 对象,而不须要全部其余讨厌且困惑的看起来像类(但动起来不像)的东西,还有构造器,原型和new调用。


问问你本身:若是我能用OLOO风格代码获得我用“类”风格代码获得的同样的东西,但OLOO更简单并且须要考虑的事情更少,OLOO不是更好吗?


让咱们讲解一下这两个代码段间涉及的思惟模型。


首先,类风给的代码段意味着这样的实体与它们的关系的思惟模型:

图片


实际上,这有点儿不公平/误导,由于它展现了许多额外的,你在 技术上 一直不须要知道(虽然你 须要 理解它)的细节。一个关键是,它是一系列十分复杂的关系。但另外一个关键是:若是你花时间来沿着这些关系的箭头走,在JS的机制中 有数量惊人的内部统一性。


例如,JS函数能够访问call(..),apply(..)和bind(..)(见第二章)的能力是由于函数自己是对象,而函数对象还拥有一个[[Prototype]]连接,链到Function.prototype对象,它定义了那些任何函数对象均可以委托到的默认方法。JS能够作这些事情,你也能!


好了,如今让咱们看一个这张图的 稍稍 简化的版本,用它来进行比较稍微“公平”一点——它仅展现了 相关 的实体与关系。

图片


任然很是复杂,对吧?虚线描绘了当你在Foo.prototype和Bar.prototype间创建“继承”时的隐含关系,并且尚未 修复 丢失的 .constructor属性引用(见第五章“终极构造器”)。即使将虚线去掉,每次你与对象连接打交道时,这个思惟模型依然要变不少可怕的戏法。


如今,然咱们看看OLOO风格代码的思惟模型:

图片


正如你所比较它们获得的,十分明显,OLOO风格的代码 须要关心的东西少太多了,由于OLOO风格代码接受了 事实:咱们惟一须要真正关心的事情是 连接到其余对象的对象。


全部其余“类”的烂设计用一种使人费解并且复杂的方式获得相同的结果。去掉那些东西,事情就变得简单得多(还不会失去任何功能)。


Classes vs. Objects

咱们已经看到了各类理论的探索和“类”与“行为委托”的思惟模型的比较。如今让咱们来看看更具体的代码场景,来展现你如何实际应用这些想法。


咱们将首先讲解一种在前端网页开发中的典型场景:建造UI部件(按钮,下拉列表等等)。


Widget“类”

由于你可能仍是如此地习惯于OO设计模式,你极可能会当即这样考虑这个问题:一个父类(也许称为Wedget)拥有全部共通的基本部件行为,而后衍生的子类拥有具体的部件类型(好比Button)。


注意: 为了DOM和CSS的操做,咱们将在这里使用JQuery,这仅仅是由于对于咱们如今的讨论,它不是一个咱们真正关心的细节。这些代码中不关心你用哪一个JS框架(JQuery,Dojo,YUI等等)来解决如此无趣的问题。


让咱们来看看,在没有任何“类”帮助库或语法的状况下,咱们如何用经典风格的纯JS来实现“类”设计:

图片


OO设计模式告诉咱们要在父类中声明一个基础render(..),以后在咱们的子类中覆盖它,但不是彻底替代它,而是用按钮特定的行为加强这个基础功能。


注意 显示假想多态 的丑态,Widget.call和Widget.prototype.render.call引用是为了假装从子“类”方法获得“父类”基础方法支持的“super”调用。呃。


ES6 class 语法糖

咱们会在附录A中讲解ES6的class语法糖,可是让咱们演示一下咱们如何用class来实现相同的代码。

图片


毋庸置疑,经过使用ES6的class,许多前面经典方法中语法的丑态被改善了。super(..)的存在看起来很是适宜(但当你深刻挖掘它时,不全是好事!)。


除了语法上的改进,这些都不是 真正的 类,由于他们仍然工做在[[Prototype]]机制之上。它们依然会受到思惟模型不匹配的拖累,就像咱们在第四,五章中,和直到如今探索的那样。附录A将会详细讲解ES6class语法和他的含义。咱们将会看到为何解决语法上的小问题不会实质上解决咱们在JS中的类的困惑,虽然它作出了勇敢的努力伪装解决了问题!


不管你是使用经典的原型语法仍是新的ES6语法糖,你依然选择了使用“类”来对问题(UI部件)进行建模。正如咱们前面几章试着展现的,在JavaScript中作这个选择会带给你额外的头疼和思惟上的弯路。


委托部件对象

这是咱们更简单的Widget/Button例子,使用了 OLOO风格委托:

图片


使用这种OLOO风格的方法,咱们不认为Widget是一个父类而Button是一个子类,Wedget只是一个对象 和某种具体类型的部件也许想要代理到的工具的集合,并且Button也只是一个独立的对象(固然,带有委托至Wedget的连接!)。


从设计模式的角度来看,咱们 没有 像类的方法建议的那样,在两个对象中共享相同的render(..)方法名称,而是选择了更能描述每一个特定任务的不一样的名称。一样的缘由,初始化 方法被分别称为init(..)和setup(..)。


不只委托设计模式建议使用不一样并且更具描述性的名称,并且在OLOO中这样作会避免难看的显式假想多态调用,正如你能够经过简单,相对的this.init(..)和this.insert(..)委托调用看到的。


语法上,咱们也没有任何构造器,.prototype或者new出现,它们事实上是没必要要的设计。


如今,若是你再细心考察一下,你可能会注意到以前仅有一个调用(var btn1 = new Button(..)),而如今有了两个(var btn1 = Object.create(Button)和btn1.setup(..))。这猛地看起来像是一个缺点(代码变多了)。


然而,即使是这样的事情,和经典原型风格比起来也是 OLOO风格代码的优势。为何?


用类的构造器,你“强制”(不彻底是这样,可是被强烈建议)构建和初始化在同一个步骤中进行。然而,有许多种状况,可以将这两步分开作(就像你在OLOO中作的)更灵活。


举个例子,咱们假定你在程序的最开始,在一个池中建立全部的实例,但你等到在它们被从池中找出并使用以前再用指定的设置初始化它们。咱们的例子中,这两个调用紧挨在一块儿,固然它们也能够按须要发生在很是不一样的时间和代码中很是不一样的部分。


OLOO 对关注点分离原则有 更好 的支持,也就是建立和初始化没有必要合并在同一个操做中。


更简单的设计

OLOO除了提供表面上更简单(并且更灵活!)的代码以外,行为委托做为一个模式实际上会带来更简单的代码架构。让咱们讲解最后一个例子来讲明OLOO是如何简化你的总体设计的。


这个场景中咱们将讲解两个控制器对象,一个用来处理网页的登陆form(表单),另外一个实际处理服务器的认证(通讯)。


咱们须要帮助工具来进行与服务器的Ajax通讯。咱们将使用JQuery(虽然其余的框架均可以),由于它不只为咱们处理Ajax,并且还返回一个相似Promise的应答,这样咱们就能够在代码中使用.then(..)来监听这个应答。


注意: 咱们不会再这里讲到Promise,但咱们会在之后的 你不懂JS 系列中讲到。


根据典型的类的设计模式,咱们在一个叫作Controller的类中将任务分解为基本功能,以后咱们会衍生出两个子类,LoginController和AuthController,它们都继承自Controller并且特化某些基本行为。

图片

图片

图片

图片



咱们有全部控制器分享的基本行为,它们是success(..),failure(..)和showDialog(..)。咱们的子类LoginController和AuthController覆盖了failure(..)和success(..)来加强基本类的行为。还要注意的是,AuthController须要一个LoginController实例来与登陆form互动,因此它变成了一个数据属性成员。


另一件要提的事情是,咱们选择一些 合成 散布在继承的顶端。AuthController须要知道LoginController,因此咱们初始化它(new LoginController()),使它一个成为this.login的类属性成员来引用它,这样AuthController才能够调用LoginController上的行为。


注意: 这里可能会存在一丝冲动,就是使AuthController继承LoginController,或者反过来,这样的话咱们就会经过继承链获得 虚拟合成。可是这是一个很是清晰地例子,代表对这个问题来说,将类继承做为模型有什么问题,由于AuthController和LoginController都不特化对方的行为,因此它们之间的继承没有太大的意义,除非类是你惟一的设计模式。与此相反的是,咱们在一些简单的合成中分层,而后它们就能够合做了,同时他俩都享有继承自父类Controller的好处。


若是你熟悉面向类(OO)的设计,这都听该看起来十分熟悉和天然。


去类化

可是,咱们真的须要用一个父类,两个子类,和一些合成来对这个问题创建模型吗?有办法利用OLOO风格的行为委托获得 简单得多 的设计吗?有的!

图片

图片



由于AuthController只是一个对象(LoginController也是),咱们不须要初始化(好比new AuthController())就能执行咱们的任务。全部咱们要作的是:

图片


固然,经过OLOO,若是你确实须要在委托链上建立一个或多个附加的对象时也很容易,并且仍然不须要任何像类实例化那样的东西:

图片


使用行为委托,AuthController和LoginController仅仅是对象,互相是 水平 对等的,并且没有被安排或关联成面向类中的父与子。咱们有些随意地选择让AuthController委托至LoginController —— 相反方向的委托也一样是有效的。


第二个代码段的主要要点是,咱们只拥有两个实体(LoginController and AuthController),而 不是以前的三个。


咱们不须要一个基本的Controller类来在两个子类间“分享”行为,由于委托是一种能够给咱们所需功能的,足够强大的机制。同时,就像以前注意的,咱们也不须要实例化咱们的对象来使它们工做,由于这里没有类,只有对象自身。 另外,这里不须要 合成 做为委托来给两个对象 差别化 地合做的能力。


最后,因为没有让名称success(..)和failure(..)在两个对象上相同,咱们避开了面向类的设计的多态陷阱:它将会须要难看的显式假想多态。相反,咱们在AuthController上称它们为accepted()和rejected(..) —— 对于他们的具体任务来讲,稍稍更具描述性的名称。


底线: 咱们最终获得了相同的结果,可是用了(显著的)更简单的设计。这就是OLOO风格代码和 行为委托 设计模式的力量。


更好的语法

一个使ES6class看似如此诱人的更好的东西是(见附录A来了解为何要避免它!),声明类方法的速记语法:

图片


咱们从声明中扔掉了单词function,这使全部的JS开发者欢呼!


你可能已经注意到,并且为此感到沮丧:上面推荐的OLOO语法出现了许多function,这看起来像对OLOO简化目标的诋毁。但它没必要是!


在ES6中,咱们能够在任何字面对象中使用 简约方法声明,因此一个OLOO风格的对象能够用这种方式声明(与class语法中相同的语法糖):

图片


惟一的区别是字面对象的元素间依然须要,逗号分隔符,而class语法没必要如此。这是在整件事情上很小的让步。


还有,在ES6中,一个你使用的更笨重的语法(好比AuthController的定义中):你一个一个地给属性赋值而不使用字面对象,能够改写为使用字面对象(因而你可使用简约方法),并且你可使用Object.setPrototypeOf(..)来修改对象的[[Prototype]],像这样:

图片


ES6中的OLOO风格,与简明方法一块儿,变得比它之前 友好得多(即便在之前,它也比经典的原型风格代码简单好看的多)。 你没必要非得选用类(复杂性)来获得干净漂亮的对象语法!


没有词法

简约方法确实有一个缺点,一个重要的细节。考虑这段代码:

图片


这是去掉语法糖后,这段代码将如何工做:

图片


看到区别了?bar()的速记法变成了一个附着在bar属性上的 匿名函数表达式(function()..),由于函数对象自己没有名称标识符。和拥有词法名称标识符baz,附着在.baz属性上的手动指定的 命名函数表达式(function baz()..)作个比较。


那又怎么样?在 “你不懂JS” 系列的 “做用域与闭包” 这本书中,咱们详细讲解了 匿名函数表达式 的三个主要缺点。咱们简单地重复一下它们,以便于咱们和简明方法相比较。


一个匿名函数缺乏name标识符:

  • 使调试时的栈追踪变得困难

  • 使自引用(递归,事件绑定等)变得困难

  • 使代码(稍稍)变得难于理解

  • 第一和第三条不适用于简明方法。


虽然去掉语法糖使用 匿名函数表达式 通常会使栈追踪中没有name。简明方法在语言规范中被要求去设置相应的函数对象内部的name属性,因此栈追踪应当可使用它(这是依赖于具体实现的,因此不能保证)。


不幸的是,第二条 仍然是简明方法的一个缺陷。 它们不会有词法标识符用来自引用。考虑:

图片


在这个例子中上面的手动Foo.bar(x*2)引用就足够了,可是在许多状况下,一个函数不必可以这样作,好比使用this绑定,函数在委托中被分享到不一样的对象,等等。你将会想要使用一个真正的自引用,而函数对象的name标识符是实现的最佳方式。


只要当心简明方法的这个注意点,并且若是当你陷入缺乏自引用的问题时,仅仅为这个声明 放弃简明方法语法,取代以手动的 命名函数表达式 声明形式:baz: function baz(){..}。


自省

若是你花了很长时间在面向类的编程方式(无论是JS仍是其余的语言),你可能会对 类型自省 很熟悉:自省一个实例来找出它是什么 种类 的对象。在类的实例上进行 类型自省 的主要目的是根据 对象是如何建立的 来推断它的结构/能力。


考虑这段代码,它使用instanceof(见第五章)来自省一个对象a1来推断它的能力:

图片


由于Foo.prototype(不是Foo!)在a1的[[Prototype]]链上(见第五章),instanceof操做符(令人困惑地)伪装告诉咱们a1是一个Foo“类”的实例。有了这个知识,咱们假定a1有Foo“类”中描述的能力。


固然,这里没有Foo类,只有一个普通的函数Foo,它刚好拥有一个引用指向一个随意的对象(Foo.prototype),而a1刚好委托连接至这个对象。经过它的语法,instanceof伪装检查了a1和Foo之间的关系,但它实际上告诉咱们的是a1和Foo.prototype(这个随意被引用的对象)是否有关联。


instanceof在语义上的混乱(和间接)意味着,要使用以instanceof为基础的自省来查询对象a1是否与讨论中的对象有关联,你 不得不 拥有一个持有对这个对象引用的函数 —— 你不能直接查询这两个对象是否有关联。


回想本章前面的抽象Foo / Bar / b1例子,咱们在这里缩写一下:

图片


为了在这个例子中的实体上进行 类型自省, 使用instanceof和.prototype语义,这里有各类你可能须要实施的检查:

图片


能够说,其中有些烂透了。举个例子,直觉上(用类)你可能想说这样的东西Bar instanceof Foo(由于很容易混淆“实例”的意义认为它包含“继承”),但在JS中这不是一个合理的比较。你不得不说Bar.prototype instanceof Foo。


另外一个常见,但也许健壮性更差的 类型自省 模式叫“duck typing(鸭子类型)”,比起instanceof来许多开发者都倾向于它。这个术语源自一则谚语,“若是它看起来像鸭子,叫起来像鸭子,那么它必定是一只鸭子”。


例如:

图片


与其检查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示例:

图片

使用这种OLOO方式,咱们所拥有的一切都是经过[[Prototype]]委托关联起来的普通对象,这是咱们可能会用到的大幅简化后的 类型自省:图片咱们再也不使用instanceof,由于它使人迷惑地伪装与类有关系。如今,咱们只须要(非正式地)问这个问题,“你是个人 一个 原型吗?”。再也不须要用Foo.prototype或者痛苦冗长的Foo.prototype.isPrototypeOf(..)来间接地查询了。


我想能够说这些检查比起前面一组自省检查,极大地减小了复杂性/混乱。又一次,咱们看到了在JavaScript中OLOO要比类风格的编码简单(但有着相同的力量)。


复习

在你的软件体系结构中,类和继承是你能够 选用 或 不选用 的设计模式。多数开发者理所固然地认为类是组织代码的惟一(正确的)方法,但咱们在这里看到了另外一种不太常被提到的,但实际上十分强大的设计模式:行为委托。


行为委托意味着对象彼此是对等的,在它们本身当中相互委托,而不是父类与子类的关系。JavaScript的[[Prototype]]机制的设计本质,就是行为委托机制。这意味着咱们能够选择挣扎着在JS上实现类机制,也能够欣然接受[[Prototype]]做为委托机制的本性。


当你仅用对象设计代码时,它不只能简化你使用的语法,并且它还能实际上引领更简单的代码结构设计。


OLOO(连接到其余对象的对像)是一种没有类的抽象,而直接建立和关联对象的代码风格。OLOO十分天然地实现了基于[[Prototype]]的行为委托。

相关文章
相关标签/搜索