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


风晓
风晓 2024-01-03 00:59:15 49005 赞同 0 反对 0
分类: 资源
精读JavaScript模式(九),JS类式继承与现代继承模式其二

壹 ❀ 引

本文将接着第八篇未说完的知识继续介绍类式继承,让我们开始。

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

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

先让我们对于第八篇的类式继承模式做个简单的复习,需求是这样,有两个构造函数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;
    };
}());

伍 ❀ 总

那么到这里,关于类式继承的相关知识就介绍完毕了

如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!

评价 0 条
风晓L1
粉丝 1 资源 2038 + 关注 私信
最近热门资源
银河麒麟桌面操作系统备份用户数据  126
统信桌面专业版【全盘安装UOS系统】介绍  121
银河麒麟桌面操作系统安装佳能打印机驱动方法  114
银河麒麟桌面操作系统 V10-SP1用户密码修改  105
最近下载排行榜
银河麒麟桌面操作系统备份用户数据 0
统信桌面专业版【全盘安装UOS系统】介绍 0
银河麒麟桌面操作系统安装佳能打印机驱动方法 0
银河麒麟桌面操作系统 V10-SP1用户密码修改 0
作者收入月榜
1

prtyaa 收益393.62元

2

zlj141319 收益218元

3

1843880570 收益214.2元

4

IT-feng 收益209.03元

5

风晓 收益208.24元

6

777 收益172.71元

7

Fhawking 收益106.6元

8

信创来了 收益105.84元

9

克里斯蒂亚诺诺 收益91.08元

10

技术-小陈 收益79.5元

请使用微信扫码

加入交流群

请使用微信扫一扫!