精读JavaScript模式(九),JS类式继承与现代继承模式其二

壹 ❀ 引

二零一九年的三月二十号,我记录了精读JavaScript模式(八)这篇读书笔记,今天是二零二零年三月十五号,相差五天,其实也算时隔一年,我重新拿起了这本书。当前为什么没继续写下去呢,主要还是因为自己JavaScript基础薄弱,理解书中的知识需要耗费大量时间,所以与其在这耗时间,不如打好基础,这也是为什么后面一年内我写了不少深入学习JavaScript相关博客的原因。为啥现在又捡起来读呢,主要原因是上上周参与程序设计,对于一个工厂函数类图的光理解就花了一天,工厂函数,设计模式,我立马想起JavaScript模式不就是讲设计模式的书吗,在写这篇博客前,我还特意把第八篇讲类式继承的文章又读了一遍,不得不说!在学习了原型,call、apply、this等相关知识后,理解起来真的是非常顺利,那么本文将接着第八篇未说完的知识继续介绍类式继承,让我们开始。

贰 ❀ 类式继承的默认模式与构造函数模式

贰 ❀ 壹 复习默认模式与构造函数模式

先让我们对于第八篇的类式继承模式做个简单的复习,需求是这样,有两个构造函数Parent与Child:

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

Parent.prototype.sayName = function (){
    console.log(this.name);
};

function Child(name){};

可以看到Parent中存在一个构造器属性(或者叫实例属性)this.name以及原型属性sayName,现在要求Child创造的实例都能获得Parent的属性。

这里我们引出了类式继承模式的第一种---默认模式,实现很简单,也就是将Child的原型指向由Parent创建的实例:

//类式继承--默认模式
Child.prototype = new Parent();
var child = new Child();
child.sayName();// echo

这种模式的缺陷是,Child除了继承了Parent的原型属性,还继承了new Parent()之后的实例属性name,这导致我们的Child并不支持传参,比如:

var child = new Child('听风是风');
child.sayName();// echo

这是因为Child函数中并无类似于this.name相关代码,所以它在创建实例时,并没有修改name属性的权利。怎么办呢,这里我们可以借用Parent函数中的this.name代码,怎么借用呢?我们可以通过构造函数中this指向实例的特性来做到这一点。

这里我们得先修改Child函数,Parent函数不用变,如下:

function Child(name) {
    Parent.apply(this,arguments);
};
var child = new Child('听风是风');
console.log(chid.name);//听风是风

你看,在new Child()时,函数内部的this指向实例child,我们利用apply方法,在调用Parent方法的同时,将Parent内部的this指向了child,从而达到了动态给实例child添加name属性的目的。

此时的类式继承仍然存在缺陷,比如当我们执行如下代码就会报错:

child.sayName()// 报错,并不存在sayName方法

原因是上述构造函数继承只是单纯继承了Parent的构造器属性,并未继承Parent的原型属性。

贰 ❀ 贰 类式继承--构造函数模式plus版

有同学可能想到了,我把默认模式与构造函数模式一起用,不就达到又能动态设置name属性,又能继承Parent原型的目的了,比如:

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

var child = new Child('听风是风');
child.sayName();//听风是风

但这样的缺陷非常明显,其一,我们调用了两次构造函数Parent,其二,我们设置了两次name属性,打印实例child就非常清楚了:

一个name是自己的属性,还有一个name是我们在new Parent时绑在Child原型上的属性。

虽然目的达到了,只是看起来不那么美观,而对于这个问题,书中再无给出优化方案,这里我给出优化方法,如下:

function Child(name) {
    var child = Object.create(Parent.prototype);
    Parent.apply(child, arguments);
    return child;
};

var child = new Child('听风是风');
child.sayName();//听风是风

这里我们利用了构造函数如果有手动返回对象,则new运算符调用时会得到手动返回对象的特性,在创建手动返回对象child时我们优先继承了构造函数Parent的原型,再使用apply继承Parent的构造器属性。

聪明的同学马上想到了,这不就是一个简易版的模拟new运算符的方法吗!你看,我们学来学去,用到的知识始终是这一大块,我们将上面的Child方法修改成一个模拟new的方法,如下:

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

Parent.prototype.sayName = function () {
    console.log(this.name);
};

//模拟的new运算符
function new_(Parent, ...rest) {
    var child = Object.create(Parent.prototype);
    var child_ = Parent.apply(child, rest);
    return typeof child_ === 'object' ? child_ : child;
};

//这是通过模拟new调用出来的实例
var child = new_(Parent, '听风是风');
child.sayName();//听风是风

//这是正常new得到的实例
var kid = new Parent('听风是风');
kid.sayName();//听风是风

console.log(child, kid);

通过打印两种方法得到的实例,可以看到效果是一模一样:

叁 ❀ 共享原型模式

共享原型模式不需要借用构造函数,正如同它的名字一样,就是将需要继承的属性统统加在原型上,而不是this上,之后我们只要将子对象的原型设置为父对象的原型即可。

function inherit(Child, Parent) {
    Child.prototype = Parent.prototype;
};

这种模式的原型链非常短,所以在查找属性时会更快,缺点也非常明显,因为大家都共用的是同一条原型链,所以不管是父对象还是子对象,只要一方修改了原型属性,双方都会受到影响。

function inherit(Child, Parent) {
    Child.prototype = Parent.prototype;
};

function Parent() { };
Parent.prototype.name = '听风是风';

function Child() { };
// Child继承Parent的原型
inherit(Child, Parent);

// Child修改原型
Child.prototype.name = '听风是风加油';

var parent = new Parent();
console.log(parent.name);//听风是风加油

肆 ❀ 临时(代理)构造函数模式

肆 ❀ 壹 一个简单的临时构造函数模式

共享原型模式固然好用,缺点咱们也说了,就是父子原型太紧密,修改起来影响很大。有没有什么好的做法弥补这个问题呢,那么就轮到临时构造函数模式出场了!

所谓临时构造函数模式,本质上就是通过创建一个空函数作为中间函数,空函数的原型将指向父对象的原型,而子对象的原型指向空函数的实例:

function inherit(Child, Parent) {
    var F = function () { };
    F.prototype = Parent.prototype;
    Child.prototype = new F();
};

这种模式与我们一开始说的默认模式最大的区别在于,中间函数只继承了Parent的原型,所以Child的实例最终也只会继承Parent的原型属性。

来看一个完整的例子:

function inherit(Child, Parent) {
    var F = function () { };
    F.prototype = Parent.prototype;
    Child.prototype = new F();
};

function Parent() {
    this.name = 'Adam';
};
Parent.prototype.say = function () {
    console.log('听风是风很棒');
};

function Child() { };
// Child继承Parent的原型
inherit(Child, Parent);

// Child修改原型
Child.prototype.say = function () {
    console.log('听风是风还得继续加油');
};

// 并没有影响父对象的原型
var parent = new Parent();
parent.say();//听风是风很棒

你看,通过中间函数,我们让Child成功继承了Parent的原型,同时还隔断了两者原型的直接关系,你可以在Child原型上畅心所欲的添加属性,这都不会影响到Parent。

那么这种做法有啥用呢?比如我在js 手动实现bind方法,超详细思路分析!这篇文章中就使用了代理构造函数模式,又一个知识点对应上去了!

肆 ❀ 贰 存储父类(Superclass)

在上一种模式基础上,我们还可以添加一个指向原始父对象原型的引用,这就像其它预言中访问超类(Superclass)一样,有时候会特别方便。有同学就要问了,超类是啥,这里我们直接引用百度的解释:

超类在软件术语中,被继承的类一般称为“超类”,也有叫做父类。是继承中非常重要的概念,它和子类一起形象地描述了继承的层次关系。

这里对应到JavaScript中来,就是被继承的父对象。

书中推荐将存储父类原型的属性名称为uber,因为super是一个保留字。我们来看一个储存了父类原型的升级版临时构造函数模式:

function inherit(Child, Parent) {
    var F = function () { };
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype;
};

function Parent() {
    this.name = 'Adam';
};
Parent.prototype.say = function () {
    console.log('听风是风很棒');
};

function Child() { };
// Child继承Parent的原型
inherit(Child, Parent);

var child = new Child();
child.say();//听风是风很棒
Child.uber.say();//听风是风很棒

肆 ❀ 叁 重置构造函数引用

上述的临时构造函数继承模式已经非常完美了,我们还需要做最后一件事,那就是重置构造函数Child 的constructor指向。

如果我们不重置Child的constructor指向会存在这样一个问题,就是Child创建的实例的constructor全部会指向Parent,这会导致一种实例都是Parent创建的错觉,还是上面的代码,我们输出如下代码:

console.log(child.constructor.name);//Parent
console.log(child.constructor === Parent);//true

我们来输出child,如下图:

这是因为构造函数Child的原型指向本质上还是Parent.prototype,而我们知道每个构造函数原型的constructor属性都指向构造函数自己。

虽然我们使用了临时构造函数过度,但是当查找某个属性时,原型链还是会查找到构造函数Parent,从而获取了Parent原型的constructor的name字段。

重置构造函数的引用也非常简单,如下:

function inherit(Child, Parent) {
    var F = function () { };
    F.prototype = Parent.prototype;
    //继承原型
    Child.prototype = new F();
    //储存父对象原型
    Child.uber = Parent.prototype;
    //重置子对象的constructor指向
    Child.prototype.constructor = Child;
};

然后我们还是创建实例,再次输出constructor的name字段,可以看到这下就完全没问题了:

console.log(child.constructor.name);//Child
console.log(child.constructor === Child);//true

原书中指出,如果你想使用类式继承,代理函数或者称之为代理构造函数模式是目前最棒的做法。

最后,上述代理构造函数模式还存在一个问题,就是我们每次要让一个Child对象继承Parent对象时,每调用一次inherit方法都会创建一个新的空函数Fn,最后一步的优化就是封装inherit函数,达到只创建一次空函数,以后使用只是修改空函数原型的目的,如下:

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

伍 ❀ 总

那么到这里,关于类式继承的相关知识就介绍完毕了。这一章节的知识主要分为两大块,前者是介绍类式继承,后者则是介绍现代继承相关的知识,所谓现代继承模式,就是没有使用类的概念,这块我们在下篇文章中介绍。

如果你对于文中原型相关的知识理解起来有些吃力,特别推荐阅读博主关于原型相关的两篇博客,相信一定会让你有所收获,链接如下:

JS 疫情宅在家,学习不能停,七千字长文助你彻底弄懂原型与原型链,武汉加油!!中国加油!!(破音)

JS 究竟是先有鸡还是有蛋,Object与Function究竟谁出现的更早,Function算不算Function的实例等问题杂谈

对于文中的知识存在难以理解,或者有错误的地方,欢迎大家留言讨论,我会在第一时间回复大家。

那么本文结束。

原文地址:https://www.cnblogs.com/echolun/p/12498966.html