专栏首页zaking's《JavaScript 模式》读书笔记(6)— 代码复用模式2

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

  上一篇讲了最简单的代码复用模式,也是最基础的,我们普遍知道的继承模式,但是这种继承模式却有不少缺点,我们下面再看看其它可以实现继承的模式。

四、类式继承模式#2——借用构造函数

本模式解决了从子构造函数道父构造函数的参数传递问题。本模式借用了父构造函数,它传递子对象以绑定到this,并且还转发任意参数。

function Child(a,c,b,d) {
    parent.apply(this,arguments);
}

  在这种方式中,只能继承在父构造函数中添加到this的属性。同时,并不能继承那些已添加到原型中的成员。

  使用该借用构造函数模式时,子对象获得了继承成员的副本,这与类式继承模式#1中,仅获取引用的方式是不同的。下面的例子演示了其差异:

// 父构造函数
function Article() {
    this.tags = ['js','css'];
}
var article = new Article();

// blog 文章对象继承了article对象
// via the classical pattern #1
function BlogPost() {}
BlogPost.prototype = article;
var blog = new BlogPost();

// 注意以上代码,你不需要new Article()
// 是因为你已经有一个可用的实例

// static page (静态页面)继承了article
// 通过借用构造函数模式

function StaticPage() {
    Article.call(this);
}

var page = new StaticPage();

console.log(article.hasOwnProperty('tags')); //true
console.log(blog.hasOwnProperty('tags')); //false
console.log(page.hasOwnProperty('tags')); //true

  在以上代码片段中,有两种方式都继承了父构造函数Article()。默认模式导致了blog对象通过原型以获得tags属性的访问,因此blog对象中没有将article作为自身的属性,因此当调用hasOwnProperty()时会返回false。相反,page对象本身则具有一个tags属性,这是由于它在使用借用构造函数的时候,新对象会获得父对象中tags成员的副本(不是引用)。

  请注意修改继承的tags属性时表现出来的差异:

blog.tags.push('html');
page.tags.push('php');
console.log(article.tags.join(', '));// 'js, css, html'

  在上面这个例子中,子对象blog修改了其tags属性,而这种方式同时也会修改父对象article,这是由于本质上blog.tags和article.tags都指向了同一个数组。但是,修改page.tags时却不会影响其父对象article,这是由于在继承过程中page.tags是独立创建的一个副本。

原型链

  当使用本模式以及熟悉的Parent()和Child()构造函数时,让我们来看原型链(prototype chain)的工作流程。其中,Child()需要根据这个新模式的需求略加修改:

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

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

// 子构造函数

function Child(name) {
    Parent.apply(this,arguments);
}

var kid = new Child('Patrick');
console.log(kid.name); // 输出“Patrick”
console.log(typeof kid.say); //输出undefined

  如果仔细查看下图,将会注意到在new Child对象和Parent对象之间不再有链接。出现这种现象的原因在于本模式中根本就没有使用Child.prototype,并且它只是指向一个空对象。使用本模式时,kid获得了自身的属性name,但是却从未继承过say()方法,如果试图调用该方法将会导致错误。继承是一个一次性的操作,它仅会复制父对象的属性并将其作为子对象自身的属性,仅此而已。因此,也就不会保留__proto__链接。

通过借用构造函数实现多重继承

当使用借用构造函数模式时,可以通过借用多个构造函数从而简单的实现多重继承。

function Cat() {
    this.legs = 4;
    this.say = function () {
        return "meaowww";
    }
}
function Bird() {
    this.wings = 2;
    this.fly = true;
}

function CatWings() {
    Cat.apply(this);
    Bird.apply(this);
}

var jane = new CatWings();
console.log(jane);

  上述代码的运行结果是这样的:

legs: 4
say: ƒ ()
wings: 2
fly: true

  在解析任意的副本属性时,将会通过最后一个获胜的方式来解析该属性(这句话的意思是,如果复制的属性中有相同的属性名,那么会后者优先)。

借用构造函数模式的优缺点

借用构造函数模式的缺点是很明显的,如前面所述,其问题在于根本无法从原型中继承任何东西,并且原型也仅是添加可重用方法以及属性的位置,它并不会为每个实例重新创建原型。

  本模式的一个优点在于可以获得父对象自身成员的真实副本,并且也不会存在于子对象意外覆盖父对象属性的风险。

  因此,在前面的情况中,如何才能使子对象也能够继承原型属性?以及如何使kid能够访问say()方法?下面这个模式将解决这个问题

五、类式继承模式#3——借用和设置原型

类式继承模式#3主要思想是结合前两种模式,即先借用构造函数,然后还设置子构造函数的原型使其指向一个构造函数创建的新实例。如下所示:

function Child(a,c,b,d) {
    Parent.apply(this,arguments);
}

Child.prototype = new Parent()

  这样做的优点在于,以上代码运行后的结果对象能够获得父对象本身的成员副本以及指向父对象中可复用功能(以原型成员方式实现的那些功能)的引用。同时,子对象也能够将任意参数传递到父构造函数中。这种行为可能是最接近您希望在Java中实现的方式。可以继承父对象中的一切东西,同时这种方法也能够安全的修改自身属性,且不会带来修改其父对象的风险。

  这种模式的一个缺点是,父构造函数被调用了两次,因此这导致了其效率低下的问题。最后,自身的属性(比如本例中扽ame属性)会被继承两次:

function Parent(name) {
    this.name = name || 'Adam';
}

// adding functionality to the prototype
Parent.prototype.say = function () {
    return this.name;
}

// 子构造函数
function Child(name) {
    Parent.apply(this,arguments);
}

Child.prototype = new Parent();

var kid = new Child('Patrick');
console.log(kid.name); //输出“Patrick”
console.log(kid.say());// 输出“Patrick”
delete kid.name;
console.log(kid.say());// 输出“Adam”

  在上面的代码中,不同于先前的模式,现在say()方法已被正确的继承。还可以注意到name属性却被继承了两次,在我们删除了kid本身的name属性的副本后,随后看到的输出是原型链表现出来所引出的name属性。

  下图显示了对象之间的链接关系。这些关系非常类似于之前#1模式的最后一张图中所示的原型链,但这里我们所采用的继承方式是不同的。

六、类式继承模式#4——共享原型

  不同于前面的那种需要两次调用父构造函数的模式(类式继承模式#3),接下来介绍的模式根本就不涉及调用任何父构造函数。

  本模式的经验法则在于:可复用成员应该转移到原型中而不是放置在this中。因此,出于继承的目的,任何值得继承的东西都应该放置在原型中实现。所以,可以仅将子对象的原型与父对象的原型设置为相同的即可:

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

  这种模式能够向您提供剪短而迅速的原型链查询,这是由于所有的对象实际上共享了同一个原型。但是,这同时也是一个缺点,因为如果在继承链下方的某处存在一个子对象或者孙子对象修改了原型,它将会影响到所有的父对象和祖先对象。

如下图所示,下面的子对象和父对象共享了同一个原型,并且可以同等的访问say()方法。然而,需要注意到子对象并没有继承name属性。

七、类式继承模式#5——临时构造函数

  类式继承模式#5通过断开父对象与子对象的原型之间的直接链接关系,从而解决共享同一个原型所带来的问题,而且同时还能够继续受益于原型链带来的好处。

  下面的代码是本模式的一种实现方式,在该代码中有一个空白函数F(),该函数充当了子对象与父对象之间的代理。F()的prototype属性指向父对象的原型。子对象的原型则是一个空白函数实例。

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

  这种模式在行为上与默认模式(类式继承模式#1)略有不同,这是由于这里的子对象仅继承了原型的属性(见下图)。这种情况通常来说是很好的,实际上也是更加可取的,因为原型也正是放置可复用功能的位置。在这种模式中,父构造函数添加到this中的任何成员都不会被继承。

  让我们创建一个新的子对象,并审查其行为:

var kid = new Child();

  如果访问kid.name,其结果将是undefined类型。在这种情况下,name是父对象所拥有的一个属性,然而在继承的时候我们实际上从未调用过new Parent(),因此也从未创建过该属性。当您访问kid.say()时,在对象#3中该方法并不可用,因此需要开始查询原型链。然而对象#4中也没有该方法,但是对象#1中确实存在该方法并且位于内存中的同一个位置,因此所有继承了Parent()的不同构造函数,以及所有由其子构造函数所创建的对象都可重用该say()方法。

存储超类

在上面模式的基础上,还可以添加一个指向原始父对象的引用。这就像在其他编程语言中访问超类一样,这可以偶尔派上用场。

  该属性被称之为uber,这仅是由于“super”是保留的关键词,并且“superclass”可能导致存心的程序员不加思考便顺势根据该关键词认为JavaScript中具有类(class)。下面是该类式继承模式的一个改进实现:

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

重置构造函数指针

最后,针对这个几乎完美的类式继承函数,还需要做的一件事情就是重置该构造函数的指针,以免在将来的某个时候还需要该构造函数。

  如果不重置该构造函数的指针,那么所有子对象将会报告Parent()是它们的构造函数,这是没有任何用处的。因此,使用前面的inherit()实现代码,可以观察到此行为:

// 父子继承
function Parent() {}
function Child() {}
inherit(Child,Parent);

// 投石问路
var kid = new Child();
console.log(kid.constructor.name); //Parent
console.log(kid.constructor === Parent); //true

虽然我们很少用到constructor属性,但是这种功能却可以很方便的用于运行时对象的内省。可以重置constructor属性使其指向期望的构造函数且不会影响其功能,这是由于该属性主要是用于提供对象的信息。

  这个类式继承模式最后的圣杯版本看起来如下所示:

function inherit(C, P){
    var F = function(){};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
    C.prototype.constructor = C;
}

  如果认为这种模式是适用于项目中的最佳方法,需要说明的是,在开源YUI库或者其他库中也存在一个与本函数相似的函数,并且它还在没有类的情况下实现了类式继承。

  对于该圣杯模式的一个常见优化是避免在每次需要继承时都创建临时(代理)构造函数。仅创建一次临时构造函数,并且修改它的原型,这已经是非常充分的。在具体实现方式上,可以使用即时函数并且在闭包中存储代理函数。

var inherit = (function () {
    var F = function () { };
    return function (C, P) {
        F.prototype = P.prototype;
        C.prototype = new F();
        C.uber = P.prototype;
        C.prototype.constructor = C;
    }
}());

  最基本的类式继承模式到这里就告一段落类,但是这远远不是结束。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 《JavaScript 模式》读书笔记(7)— 设计模式3

    这一篇,我们学习本篇中最为复杂的三个设计模式,代理模式、中介者模式以及观察者模式。这三个模式很重要!!

    zaking
  • 《JavaScript 模式》读书笔记(5)— 对象创建模式4

    JavaScript中没有常量的概念,虽然许多现代的编程环境可能为您提供了用以创建常量的const语句。作为一种变通方案,JavaScript中常见的一种方法...

    zaking
  • 《JavaScript 模式》读书笔记(3)— 字面量和构造函数1

      新的篇章开始了,本章开始,所有的内容都是十分有价值和意义的。本章主要的内容包括对象字面量、构造函数、数组字面量、正则字面量、基本值类型字面量以及JSON等。...

    zaking
  • 前端入门13-JavaScript进阶之原型声明正文-原型

    作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。

    请叫我大苏
  • 【JavaScript】 JS面向对象的模式与实践

    参考书籍 《JavaScript高级语言程序设计》—— Nicholas C.Zakas 《你不知道的JavaScript》  —— KYLE SIMPSON ...

    外婆的彭湖湾
  • 「转」javascript中的prototype

    在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例。但是在Javascript语言体系中,是不存在类(Cla...

    音视频_李超
  • 深入理解javascript中的原型原型的概念使用原型给对象添加方法和属性使用原型对象的属性和方法原型的陷阱小结

    原型prototype是javascript中极其重要的概念之一,但也是比较容易引起混淆的地方。我们需要花费一些时间和精力好好理解原型的概念,这对于我们学习ja...

    desperate633
  • 前端入门14-JavaScript进阶之继承声明正文-继承

    作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。

    请叫我大苏
  • 百度整改动真格,搜索引擎行业会拐点将至?

    2016年上半年百度麻烦缠身。贴吧门、魏则西事件、夜间推广赌博网站等坏消息接踵而至,百度被置于舆论漩涡,网信办《互联网信息搜索服务管理规定》和工商总局《互联网广...

    罗超频道
  • Hadoop--倒排索引过程详解

    倒排索引就是根据单词内容来查找文档的方式,由于不是根据文档来确定文档所包含的内容,进行了相反的操作,所以被称为倒排索引 下面来看一个例子来理解什么是倒排索引 这...

    汤高

作者介绍

zaking
  • 云+社区翻译社勋章

小叶云(北京)信息技术有限公司前端开发工程师

小叶云(北京)信息技术有限公司 · 前端开发工程师 (已认证)

精选专题

活动推荐

扫码关注云+社区

领取腾讯云代金券