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

  我们有开始进入新篇章了。这篇内容主要讲代码复用模式,实际上代码复用,就是继承啊,原型啊,构造函数啊等等这一类的内容。对于前端进阶来说,是很重要的基础知识。这一篇内容会对原型、 继承有很深入的讲解。我也会尽我所能的为大家讲清楚、分析透彻。

  代码复用是一个非常重要而且有趣的主题,简而言之,这是由于人们很自然的争取编写尽可能少的代码。尤其是那些具有质量优秀、通过测试、可维护、可扩展性、文档化的可复用代码。

  在谈及代码复用的时候,首先想到的是代码的继承性(inheritance),而本章中大部分也专门致力于代码复用这个主题。在这里可以看到多种方法都可以实现“基于类特性的(classical)”和“非基于类特性的(nonclassical)”继承特性。但重要的是要记住其最终目标,我们要复用代码。继承性就是程序员用以实现代码复用这个目标的一种方法或手段,而且它也并不是唯一的方法。在本章中,可以看到如何利用其他对象组合成所需的对象,也可以看到如何使用mix-in技术(混入或者渗元技术),还可以看到如何在技术上没有永久继承的情况下仅借用和复用所需的功能。

  当开始接触代码复用任务时,请记住GoF(Gang of Four,指《Design Patterns》的四位作者)等人在其著作中提出的有关创建对象的建议原则“优先使用对象组合,而不是类继承”。

 

一、传统与现代继承模式的比较

  经常会在有关JavaScript继承模式的主题讨论中听到术语“传统继承(classical inheritance)”,为此,让我们先阐明“传统(classical)”所代表的意义。该术语从词义上来说,并不是与用于古董、历史沉积、或者广为接受并认定为正确的处理事务的方法这些词义相同。实际上,该术语只是单词“Class(类)”的一种表现形式。

  许多编程语言都具有类的概念,并以此作为对象的蓝图。在那些编程语言中,每个对象都是一个类的特定实例(比如,Java语言环境中),并且在不存在某个类的时候并不能创建该类的对象。在JavaScript中,由于没有类的概念,因此实例的概念也就没有多大的意义。JavaScript中的对象是简单的键-值(key-value)对,可以动态的创建和修改这些对象。

  但JavaScript具有构造函数,并且new操作符的语法与那些使用类的编程语言在语法上有许多相似之处。

  在Java中可以采用下列方式创建对象:

Person adam = new Person();

  而在JavaScript中则可以采用下列方式创建对象:

var adam = new Person();

  除了与Java中强类型限制的情况不同之外,在JavaScript中也必须声明adam是Person类型,其语法与Java看起来是一样的。JavaScript的构造函数在调用时Person看起来似乎是一个类,但重要的是要记住Person仍然只是一个函数。这种语法上的相似性导致了很多程序员按照类的方式考虑JavaScript,并产生了一些假定在类的基础上的开发思路和继承模式。我们将这种实现方式称之为“类式(classical)”继承模式。在这里让我们将“现代(modern)”模式表述为:其他任何不需要以类的方式考虑的模式。

  在项目中,首先建议使用现代模式,除非你或你的团队真的不适应这样。本章先讨论类式继承模式,再讨论其他现代模式。

 

二、使用类式继承时的预期结果

  实现类式继承(classical inheritance)的目标是通过构造函数Child()获取来自于另一个构造函数Parent()的属性,从而创建对象。

  注意:虽然这里讨论的是类式继承模式,但是请让我们尽量避免使用“class(类)”这个单词。将其表述为“构造函数(constructor function 或 constructor)”时虽然字数更长一些,但是其表述更为精确且不会产生歧义。一般情况下,在开发团队交流时请努力消除单词“class”的使用,因为当涉及JavaScript时,“class”这个词对于不同的人可能意味着不同的含义。

  下面是定义两个构造函数Parent()和Child()的一个例子:

// 父构造函数
function Parent(name) {
    this.name = name || 'Adam';
}

// 向该原型添加功能
Parent.prototype.say = function () {
    return this.name;
};

// 空白的子构造函数
function Child() {}

// 继承的魔力在这里发生
inherit(Child,Parent);

  上面的方法中,存在父、子两个构造函数,say()方法被添加到父构造函数的原型(prototype)中,并且一个名为inherit()的函数调用负责处理它们之间的继承关系。其中,inherit()函数并非由编程语言提供的,为此,程序员必须自己来实现该函数。下面,让我们看看实现该函数的几种方法。

 

三、类式继承模式#1——默认模式

  最常用的一种默认方法是使用Parent()构造函数创建一个对象,并将该对象赋值给Child()的原型。下面是可复用继承函数inherit()的第一种用法:

function inherit(C,P) {
    C.prototype = new P();
}

  重要的是需要记住,原型的属性应该指向一个对象,而不是一个函数,因此它必须指向一个由父构造函数所创建的实例(一个对象),而不是指向构造函数本身。也就是说,要注意使用new操作符来创建新对象,因为需要new才能使用这种模式。

  在以后的程序中,当使用new Child()语句创建一个对象时,它会通过原型从Parent()实例中获取它的功能,如下所示:

var kid = new Child();
kid.say();

 

追溯原型链

  使用这种默认的继承模式时,同时继承了自身的属性(即,加入到this的实例相关属性,比如name),以及原型属性和方法【比如say()】。

  让我们回顾一下在这种继承模式下原型链的工作原理。出于讨论的目的,让我们将对象视做存在于内存中某处的块,该内存块可以包含数据以及指向其他块的引用。

  当使用new Parent()语句创建一个对象时,会创建一个这样的块,如下图所示中的#2块。

  在#2块中保存了name属性的数据。如果您尝试访问say()方法,虽然块#2中并不包含say()方法,但是通过使用指向构造函数Parent()的prototype(原型)属性的隐式链接__proto__,便可以访问对象#1(Parent.prototype),而对象#1又确实知道关于say()的地址。所有这一切都在后台发生,并不用为这种复杂的原型链而烦恼,重要的是需要理解它的工作原理以及所需要访问或可能修改的数据位于何处。请注意,这里仅使用__proto__来解释原型链,即使在一些环境中提供了该属性,在程序开发语言中也并不能使用该属性。

  现在,让我们来看一下在使用inherit()函数后,当使用var kid = new Child()创建新对象时会发生什么情况,如下图所示:

  从上图可以看出,child()构造函数是空的,并且没有任何属性添加到Child.prototype中。因此,使用new Child()语句所创建的对象除了包含隐式链接__proto__以外,它几乎是空的。在这种情况下,__proto__指向了在inherit()函数中使用new Parent()语句所创建的对象。

  现在,当执行kid.say()时会发生什么情况?对象#3中并没有这样的say()方法,因此它将通过原型链查询到#2.然而,#2中也没有该方法,因此它又顺着原型链查询到对象#1,而对象#1正好具有say()方法。然而,在say()中引用了this.name,该引用仍然还需要解析。因此,查询再次启动。在这种情况下,this指向对象#3,对象#3中没有name属性。为此,将查询对象#2,而对象#2中确实有name属性,其值为“Adam”。

  最后,让我们更进一步查看原型链的概念,比如说,我们有以下这样的代码:

var kid = new Child();
kid.name = 'Patrick'
kid.say();

  下图显示了上述这种情况下原型链的工作过程。

  设置kid.name语句并不会修改对象#2的name属性,但是它却直接在kid对象#3上创建了一个自身的属性。当执行kid.say()时,将依次在对象#3、对象#2中查询say()方法,并且最终在对象#1中找到该方法,这与前面所描述的过程相似。但是,如果这次是查找this.name(这是与kid.name相同的),那么其过程是很快的,这是由于该属性立刻就能够在对象#3中找到,而无需通过原型链。

  如果使用delee kid.name语句删除新属性,那么对象#2的name属性将会“表现出来”,并在连续的查找过程中找到其name属性。

 

使用模式#1时的缺点

  本模式的其中一个缺点在于:同时继承了两个对象的属性,即添加到this的属性以及原型属性。在绝大多数的时候,并不需要这些自身的属性(比如这里的name),因为它们很可能是指向一个特定的实例,而不是复用。

  注意,对于构造函数的一般经验法则是:应该将可复用的成员添加到原型中。

  另一个关于使用通用inherit()函数的问题在于它并不支持将参数传递到子构造函数中,而子构造函数然后又将参数传递到父构造函数中,考虑以下这个例子:

var s = new Child('Seth');
s.say(); // 输出“Adam”

  以上的输出结果可能并不是您所期望的。虽然子构造函数可以将参数传递到父构造函数中,但是那样的话,在每次需要一个新的子对象时都必须重新执行这种继承机制,而且该机制的效率时很低的,其原因在于最终会反复的重新创建父对象。

posted @ 2020-04-19 15:18  Zaking  阅读(200)  评论(0编辑  收藏  举报