我们之前聊了聊基本的继承的概念,也聊了很多在JavaScript中模拟类的方法。这篇文章,我们主要来学习一下现代继承的一些方法。

九、原型继承

  下面我们开始讨论一种称之为原型继承(prototype inheritance)的“现代”无类继承模式。在本模式中并不涉及类,这里的对象都是继承自其他对象。以这种方式考虑:有一个想要复用的对象,并且想创建的第二个对象需要从第一个对象中获取其功能。

  下面的代码展示了该如何开始着手实现这种模式:

  1. // 要继承的对象
  2. var parent = {
  3. name:"Papa"
  4. };
  5. // 新对象
  6. var child = object(parent);
  7.  
  8. // 测试
  9. alert(child.name) //"Papa"

  在前面的代码片段中,存在一个以对象字面量(object literal)创建的名为parent的现有对象,并且要创建另外一个与parent具有相同属性和方法的名为child的对象。child对象是由一个名为object()的函数所创建。JavaScript中并不存在该函数(不要与构造函数object()弄混淆),为此,让我们看看该如何定义该函数。

  与类似继承模式的圣杯版本相似,首先,可以使用空的临时构造函数F()。然后,将F()的原型属性设置为父对象。最后,返回一个临时构造函数的新实例:

  1. function object(o) {
  2. function F() {}
  3. F.prototype = o;
  4. return new F();
  5. }

  下图展示了在使用原型继承模式时的原型链。图中的child最初是一个空对象,他没有自身的属性,但是同时他又通过受益于__proto__链接而具有其父对象的全部功能。

讨论

  在原型继承模式中,并不需要使用字面量符合(literal notation)来创建父对象(尽管这可能是一种比较常见的方式)。如下代码所示,可以使用构造函数创建父对象,请注意,如果这样做的话,“自身”属性和构造函数的原型的属性都将被继承:

  1. // 父构造函数
  2. function Person() {
  3. // an "own" property
  4. this.name = "Adam"
  5. }
  6.  
  7. // 添加到原型的属性
  8. Person.prototype.getName = function () {
  9. return this.name;
  10. };
  11.  
  12. // 创建一个新的Person类对象
  13. var papa = new Person();
  14.  
  15. // 继承
  16. var kid = object(papa);
  17.  
  18. // 测试自身的属性
  19. // 和继承的原型属性
  20. console.log(kid.getName());

  在本模式的另外一个变化中,可以选择仅继承现有构造函数的原型对象。请记住,对象继承自对象,而不论父对象是如何创建的。下面使用了前面的例子演示该变化,仅需稍加修改代码即可:

  1. // 父构造函数
  2. function Person() {
  3. // an "own" property
  4. this.name = "Adam"
  5. }
  6.  
  7. // 添加到原型的属性
  8. Person.prototype.getName = function () {
  9. return this.name;
  10. };
  11.  
  12. // 继承
  13. var kid = object(Person.prototype);
  14.  
  15. console.log(typeof kid.getName);
  16. console.log(typeof kid.name);

增加到ECMAScript5中

  在ECMAScript5中,原型继承模式已经正式成为该语言的一部分。这种模式是通过方法Object.create()来实现的。也就是说,不需要推出与object()类似的函数,它已经内嵌在语言中:

  1. var child = Object.create(parent);

  Object.create()接受一个额外的参数,即一个对象。这个额外对象的属性将会被添加到新对象中,以此作为新对象自身的属性,然后Object.create()返回该新对象。这提供了很大的方便,使您可以仅采用一个方法调用即可实现继承并在此基础上构建子对象。比如:

  1. var child = Object.create(parent,{
  2. age : { value : 2}
  3. });
  4. child.hasOwnProperty("age");

  可能还会发现一些JavaScript库中已经实现了原型继承模式。例如,在YUI3中是Y.Object()方法。

十、通过复制属性实现继承

  让我们看另一种继承模式,即通过复制属性实现继承。在这种模式中,对象将从另一个对象中获取功能,其方法是仅需将其复制即可。下面是一个示例函数extend()实现复制继承的例子:

  1. function extend(parent, child){
  2. var i;
  3. child = child || {};
  4. for(i in parent) {
  5. if(parent.hasOwnProperty(i)) {
  6. child[i] = parent[i]
  7. }
  8. }
  9. return child
  10. }

  上面点的代码是一个简单的实现,它仅遍历父对象的成员并将其复制出来。在本示例实现中,child对象是可选的。如果不传递需要扩展的已有对象,那么他会创建并返回一个全新的对象。

  1. var dad = {name : "Adam"};
  2. var kid = extend(dad);
  3. console.log(kid.name)

  上面给出的是一种所谓浅复制的对象。另一方面,深度复制意味着属性检查,如果即将复制的属性是一个对象或者一个数组,这样的话,它将会递归遍历该属性并且还会将属性中的元素复制出来。在使用前复制(由于JavaScript中的对象是通过引用而传递的)的时候,如果改变了子对象的属性,并且该属性恰好是一个对象,那么这种操作表示也正在修改父对象。其实,这也是更可取的方法,但是当处理其他对象和数组时,这种前复制也可能导致意外发生。考虑下列情况:

  1. var dad = {
  2. counts:[1,2,3],
  3. reads:{paper:true}
  4. }
  5.  
  6. var kid = extend(dad);
  7. kid.counts.push(4);
  8. console.log(dad.counts.toString());
  9. console.log(dad.reads === kid.reads)

  现在让我们修改extend()函数以实现深度复制。所有需要做的事情就是检查某个属性的类型是否为对象,如果是这样的话,需要递归复制出该对象的属性。另外,还需要检查该对象是否为一个真实对象或者一个数组,我们可以使用第三章中讨论的方法检查其数组性质。因此,深度复制版本的extend()函数看起来是这样的:

  1. function extendDeep(parent,child) {
  2. var i,
  3. toStr = Object.prototype.toString,
  4. astr = "[object Array]";
  5.  
  6. child = child || {};
  7.  
  8. for(i in parent) {
  9. if(parent.hasOwnProperty(i)) {
  10. if(typeof parent[i] === 'object'){
  11. child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
  12. extendDeep(parent[i],child[i])
  13. } else {
  14. child[i] = parent[i]
  15. }
  16. }
  17. }
  18. return child;
  19. }

  现在开始测试这种新的实现方式,由于它能够为我们创建对象的真实副本,因此子对象的修改并不会影响其父对象。

  1. var kid = extendDeep(dad);
  2. kid.counts.push(4);
  3. console.log(kid.counts.toString());
  4. console.log(dad.counts.toString());
  5.  
  6. console.log(dad.reads === kid.reads);
  7. kid.reads.paper = false;
  8.  
  9. kid.reads.web = true;
  10. console.log(dad.reads.paper)

  这种属性复制模式比较简单且得到了广泛的应用。值得注意的是,本模式中根本没有涉及到任何原型,本模式仅与对象以及它们自身的属性相关。

混入

  可以针对这种通过属性复制实现继承的思想作进一步的扩展,现在让我们思考一种“mix-in”混入模式。mix-in模式并不是复制一个完整的对象,而是从多个对象中复制出任意的成员并将这些成员组合成一个新的对象。

  mix-in实现比较简单,只需遍历每个参数,并且复制出传递给该函数的每个对象中的每个属性。

  1. function mix() {
  2. var arg,prop,child = {};
  3. for(arg = 0;arg < arguments.length; arg += 1) {
  4. for(prop in arguments[arg]) {
  5. if(arguments[arg].hasOwnProperty(prop)) {
  6. child[prop] = arguments[arg][prop];
  7. }
  8. }
  9. }
  10. return child;
  11. }

  现在,您有一个通用的mix-in函数,可以向他传递任意数量的对象,其结果将获得一个具有所有源对象属性的新对象。下面是一个使用示例:

  1. var cake = mix(
  2. {eggs:2,large:true},
  3. {butter:2,salted:true},
  4. {flour:'3 cups'},
  5. {sugar:'sure!'}
  6. )
  7.  
  8. console.dir(cake)

  注意:如果已经学习过那些正式包含mix-in概念的语言,并且习惯于mix-in的概念,那么可能希望修改一个或多个父对象时可以影响其子对象,但是在本节给定的实现中并不是这样的。子啊这里我们仅简单循环、复制自身的属性,以及断开与父对象之间的链接。

十一、借用方法

  有时候,可能恰好仅需要现有对象其中的一个或两个方法。在想要重用这些方法的同时,但是又不希望与源对象形成父-子继承关系。也就是说,指向使用所需要的方法,而不希望继承那些永远都不会用到的其他方法。在这种情况下,可以通过使用借用方法模式来实现,而这时受益于call()和apply()函数方法。您已经在本书中见到过这种模式,比如,甚至于在本章中extendDeep()函数的实现内部都见到过。

  如您所知,JavaScript中的函数也是对象,并且它们自身也附带着一些有趣的方法,比如apply()和call()方法。这两者之间的唯一区别在于其中一个可以接受传递给将被调用方法的参数数组,而另一个仅逐个接受参数。可以使用这些方法以借用现有对象的功能。

  1. //call()例子
  2. notmyobj.doStuff.call(myobj,param1,p2,p3);
  3. // apply()例子
  4. notmyobj.doStuff.apply(myobj,[param1,p2,p3]);

  在以上代码中,存在一个名为MyObj的对象,并且还知道其他名为notmyobj的对象中有一个名为doStuff()的有用方法。您无需经历继承所带来的麻烦,也无需继承myobj对象永远都不会用到的一些方法,可以仅临时性的借用方法doStuff()即可。

  可以传递对象、任意参数以及借用方法,并将它们绑定到您的对象中以作为this本身的成员。从根本上说,您的对象将在一小段时间内伪装成其他对象,从而借用其所需的方法。这就像得到了继承的好处,但是却无需支付遗产税(这里指其他您不需要的属性或方法)。

例子:借用数组方法

  本模式的一个常见实现方法是借用数组方法。

  数组具有一些有用的放啊,而形如arguments的类似数组的对象并不具有这些方法。因此,arguments可以借用数组的方法,比如slice()方法:

  1. function f() {
  2. var args = [].slice.call(arguments,1,3);
  3. return args;
  4. }
  5. console.log(f(1,2,3,4,5,6));

  在这个例子中,创建一个空数组的原因只是为了使用数组的方法。此外,能够实现同样功能但是语句稍微长一点的方式是直接从Array的原型中借用方法,即使用Array.prototype.slice.call()方法。这种方式需要输入更长一点的字符,但是却可以节省创建一个空数组的工作。

借用和绑定

  考虑到借用方法不是通过调用call()/apply()就是通过简单的赋值,在借用方法的内部,this所指向的对象是基于调用表达式而确定的。但是有时候,最好能够“锁定”this的值,或者将其绑定到特定对象并预先确定该对象。

  让我们看下面这个例子,其中存在一个名为one的对象,且具有say()方法:

  1. var one = {
  2. name:"object",
  3. say: function(greet) {
  4. return greet + ', ' + this.name;
  5. }
  6. };
  7.  
  8. // 测试
  9. console.log(one.say('hi')); //结果为“hi,object”

  现在,另一个对象two中并没有say()方法,但是可以从对象one中借用该方法,如下所示:

  1. var two = {
  2. name:"another object"
  3. };
  4.  
  5. console.log(one.say.apply(two,["hello"]));

  在上述例子中,借用的say()方法内部的this指向了two对象,因而this.name的值为“another object”。但是在什么样的场景中,应该将函数指针赋值给一个全局变量,或者将该函数作为回调函数来传递?在客户端编程中有许多事件和回调函数,因此确实发生了很多这样混淆的事情。

  1. // 给变量赋值
  2. // this将指向全局变量
  3. var say = one.say;
  4. console.log(say('hoho'));
  5. // 作为回调函数传递
  6. var yetanother = {
  7. name:"Yet another object",
  8. method:function(callback) {
  9. return callback("Hola");
  10. }
  11. };
  12. console.log(yetanother.method(one.say));

  在以上两种情况下,say()方法内部的this指向了全局对象,并且整个代码段都无法按照预期正常运行。为了修复(也就是说,绑定)对象与方法之间的关系,我们可以使用如下的一个简单函数:

  1. function bind(o,m) {
  2. return function () {
  3. return m.apply(o,[].slice.call(arguments))
  4. }
  5. }

  这个bind()函数接受了一个对象o和一个方法m,并且将两者绑定起来,然后返回另一个函数。其中,返回的函数可以通过闭包来访问o和m。因此,即时在bind()返回后,内部函数热盎然可以访问o和m,并且总是指向原始对象和方法。下面,让我们使用bind()创建一个新的函数:

  1. var twosay = bind(two,one.say);
  2. console.log(twosay('yo'))

  正如您上面所看到的,即时twosay()以全局函数方式而创建,但是say()方法内部的this并没有指向全局对象,实际上它指向了传递给bind()的对象two。无论您如何调用twosay(),该方法永远是绑定到对象two上。

  奢侈的拥有绑定所需要付出的代价就是额外的必报的开销。

Function.prototype.bind()

  ECMAScript5中将bind()方法添加到了Function.prototype中,使得bind()就像apply()和call()一样简单易用。因此,可以执行如下表达式:

  1. var newFunc = obj.someFunc.bind(myobj,1,2,3);

  上述表达式的含义是将someFunc()和myobj绑定在一起,并且预填充someFunc()期望的前三个参数。这也是第四章中所讨论的部分函数应用的一个例子。

  当程序在ES5之前的环境运行时,让我们看看应该如何实现Function.prototype.bind():

  1. if(typeof Function.prototype.bind === 'undefined') {
  2. Function.prototype.bind = function(thisArg) {
  3. var fn = this,
  4. slice = Array.prototype.slice,
  5. args = slice.call(arguments,1);
  6. return function () {
  7. return fn.apply(thisArg,args.concat(slice.call(arguments)));
  8. }
  9. }
  10. }

  这个实现看起来可能有点熟悉,它使用了部分应用并拼接了参数列表,即那些传递给bind()的参数(除了第一个以外),以及那些传递给由bind()所返回的新函数的参数,其中该新函数将在以后被调用。下面是一个使用示例:

  1. var twosay2 = one.say.bind(two);
  2. console.log(twosay2("Bonjour"));

  在前面的例子中,除了提供了将被绑定的对象以外,并没有向bind()传递任何参数。在下面的例子,让我们传递一个参数以实现部分应用:

  1. var twosay3 = one.say.bind(two,"Enchante");
  2. console.log(twosay3())


小结

  当在JavaScript中涉及到继承时,有很多可供选择的方法。这些方法对于学习和理解多种不同的模式大有裨益,因为它们有助于提高您对语言的掌握程度。在本章中,您了解了几种类式继承模式以及集中现代继承模式,从而可以解决继承相关的问题。

  然而,在开发过程中经常面临的继承可能并不是一个问题。其中一部分的原因在于,事实上使用的JavaScript库可能以这样或那样的方式解决了该问题,而另一个方面的原因在于很少需要在JavaScript中建立长而且复杂的继承链。在静态强类型的语言中,继承可能是唯一复用代码的方法。在JavaScript中,经常有更简洁且优美的方法,其中包括借用方法、绑定、复制属性以及从多个对象中混入属性等多种方法。

  最后,请记住,代码重用才是最终目的,而继承只是实现这一目标的方法之一。

  到这里,这一篇就结束了,后面,我们开始学习设计模式!

《JavaScript 模式》读书笔记(6)— 代码复用模式3的更多相关文章

  1. 《JavaScript 模式》读书笔记(6)— 代码复用模式2

    上一篇讲了最简单的代码复用模式,也是最基础的,我们普遍知道的继承模式,但是这种继承模式却有不少缺点,我们下面再看看其它可以实现继承的模式. 四.类式继承模式#2——借用构造函数 本模式解决了从子构造函 ...

  2. 《JavaScript模式》第6章 代码复用模式

    @by Ruth92(转载请注明出处) 第6章:代码复用模式 GoF 在其著作中提出的有关创建对象的建议原则: -- 优先使用对象组合,而不是类继承. 传统模式:使用类继承: 现代模式:"类 ...

  3. javascript代码复用模式(二)

    前面说到,javascript的代码复用模式,可分为类式继承和非类式继承(现代继承).这篇就继续类式继承. 类式继承模式-借用构造函数 使用借用构造函数的方法,可以从子构造函数得到父构造函数传任意数量 ...

  4. 深入理解JavaScript系列(46):代码复用模式(推荐篇)

    介绍 本文介绍的四种代码复用模式都是最佳实践,推荐大家在编程的过程中使用. 模式1:原型继承 原型继承是让父对象作为子对象的原型,从而达到继承的目的: function object(o) { fun ...

  5. 《你不知道的javascript》读书笔记2

    概述 放假读完了<你不知道的javascript>上篇,学到了很多东西,记录下来,供以后开发时参考,相信对其他人也有用. 这篇笔记是这本书的下半部分,上半部分请见<你不知道的java ...

  6. 《编写可维护的javascript》读书笔记(中)——编程实践

    上篇读书笔记系列之:<编写可维护的javascript>读书笔记(上) 上篇说的是编程风格,记录的都是最重要的点,不讲废话,写的比较简洁,而本篇将加入一些实例,因为那样比较容易说明问题. ...

  7. javascript代码复用模式

    代码复用有一个著名的原则,是GoF提出的:优先使用对象组合,而不是类继承.在javascript中,并没有类的概念,所以代码的复用,也并不局限于类式继承.javascript中创建对象的方法很多,有构 ...

  8. 深入理解JavaScript系列(45):代码复用模式(避免篇)

    介绍 任何编程都提出代码复用,否则话每次开发一个新程序或者写一个新功能都要全新编写的话,那就歇菜了,但是代码复用也是有好要坏,接下来的两篇文章我们将针对代码复用来进行讨论,第一篇文避免篇,指的是要尽量 ...

  9. SQL反模式读书笔记思维导图

    在写SQL过程以及设计数据表的过程中,我们经常会走一些弯路,会做一些错误的设计.<SQL反模式>这本书针对这些经常容易出错的设计模式进行分析,解释了错误的理由.允许错误的场景,并给出更好的 ...

  10. 《图解设计模式》读书笔记2-1 Template Method模式

    目录 模板方法模式 类图 思想: 模板方法模式 在父类中定义流程,在子类中实现具体的方法. 类图 代码 //抽象类 public abstract class AbstractDisplay { pu ...

随机推荐

  1. Spring装配Bean的三种方式+导入和混合配置

    目录 Spring IoC与bean 基于XML的显式装配 xml配置的基本结构 bean实例的三种创建方式 依赖注入的两种方式 构造器注入方式 setter方法注入方式 利用命名空间简化xml 基于 ...

  2. Python函数之面向过程编程

    一.解释 面向过程:核心是过程二字,过程即解决问题的步骤,基于面向过程去设计程序就像是在设计,流水线式的编程思想,在设计程序时,需要把整个流程设计出来, 一条工业流水线,是一种机械式的思维方式 二.优 ...

  3. Unity 游戏框架搭建 2019 (二十五) 类的第一个作用 与 Obselete 属性

    在上一篇我们整理到了第七个示例,我们今天再接着往下整理.我们来看第八个示例: #if UNITY_EDITOR using UnityEditor; #endif using UnityEngine; ...

  4. JQ前端上传图片显示在页面以及发送到后端服务器

    // 单张上传照片     html: <div class="azwoo"></div> <div class="azwot"& ...

  5. ThinkPHP6.0学习笔记-模型操作

    ThinkPHP模型 模型定义 在app目录下创建Model目录,即可创建模型文件 定义一个和数据库表相匹配的模型 use think\Model; class User extends Model ...

  6. iOS, Xcode11,项目提示第三方库报错无法运行 bundle format unrecognized, invalid, or unsuitable

    检查你有没有把静态库和动态库配置错误!! 下图处是配置动态库的地方! 对于动态库和静态库都有使用的时候,注意把静态库设置成“Do not Embeded”

  7. man手册、zip备份

                                                                                                        ...

  8. 最小生成树算法【图解】--一文带你理解什么是Prim算法和Kruskal算法

    假设以下情景,有一块木板,板上钉上了一些钉子,这些钉子可以由一些细绳连接起来.假设每个钉子可以通过一根或者多根细绳连接起来,那么一定存在这样的情况,即用最少的细绳把所有钉子连接起来. 更为实际的情景是 ...

  9. vscode如何安装eslint插件 代码自动修复

    ESlint:是用来统一JavaScript代码风格的工具,不包含css.html等. 方法和步骤: 通常情况下vue项目都会添加eslint组件,我们可以查看webpack的配置文件package. ...

  10. "为文本添加下划线"组件:<u> —— 快应用组件库H-UI

     <import name="u" src="../Common/ui/h-ui/text/c_tag_underline"></impor ...