美文网首页js高级程序设计第三版小记
2020-01-03:继承原理(原型链)

2020-01-03:继承原理(原型链)

作者: Lau17 | 来源:发表于2020-01-04 00:24 被阅读0次

    2.4组合使用构造函数模式和原型模式

    综合使用构造函数和原型模式可以避免之前二者的问题,而取其长处。

    通常来说,我们使用构造函数模式定义实例属性,而原型模式定义公用的方法和属性。这样做之后,每个实例都会有一份只属于自己的属性副本,同时又可以调用共有方法:

    同时使用构造函数法和原型模式

    在这里,我们需要的实例属性都是在构造函数中定义的,而实例共享的属性和方法则是在原型中定义的。

    2.5动态原型模式

    js的原型模式和其他OO语言最大的不同就是构造函数和原型分别独立,这就是动态原型模式致力于解决的方案。动态原型模式将所有信息封装在构造函数中,通过构造函数在必要情况下初始化原型:

    只有在sayName方法不存在的情况下,才会动态写入原型中。

    在动态原型模式中,通常会使用instanceof和typeof进行类型检测。并且需要注意,在使用动态原型模式时,不能使用对象字面量重写原型,原因请参照2020-01-02中有关重写原型时可能会切断既有实例和原型之间的联系。

    2.6寄生构造函数模式

    这种使用方式通常用来创建一个基于既有对象,而又比既有对象多一些额外方法或属性的对象。举个例子,我们需要建设一个具有额外方法的特殊数组,那么就可以使用寄生构造函数模式。

    寄生构造函数模式的思想与工厂模式几乎相同:在构造函数内new一个既有对象,通过对这个既有对象的实例增加新的属性或方法,达到不修改既有对象的构造函数便可以添加新的方法。

    举个例子,假如我们需要一个具有额外方法的特殊数组:

    Array是既有对象,通过在构造函数内为数组实例values添加方法实现不修改Array的构造函数就可以为这个新实例添加新的方法。

    2.7稳妥构造函数模式

    稳妥对象,指的是没有公共属性,其方法也不引用this对象。通常会被使用在一些安全的和环境中。定义在其中的属性通常只能用添加的方法访问(类似私有属性的概念)

    看一个例子:

    除了调用sayName方法外,没有其他任何方式可以访问数据成员

    3.继承

    一般的OO语言继承有两种方式:接口继承和实现继承。接口继承只继承方法签名,而实际继承则继承实际方法。但是因为在js中函数没有签名,因此js只支持实现继承。继承的主要原理是依赖原型链实现。

    3.1原型链

    原型链是实现js中继承的主要方法。其基本四项是利用原型让一个引用类型继承另外一个引用类型的属性和方法。

    首先简单回顾以下构造函数,原型对象,和实例之间的关系:

    一个构造函数有一个原型对象,构造函数可以通过prototype访问原型对象;

    一个原型对象包括一个指向构造函数的指针constructor;

    实例包含一个指向原型对象的指针;

    假如我们让原型对象A等于原型对象B的实例b,则原型对象A同时也是原型对象B的实例b,因此其内就会包括一个指向原型对象B的指针。相应的,如果原型对象B等于原型对象C的实例c,则原型对象B同时也是原型对象C的实例c,因此其内部就会包括一个指向原型对象C的指针。这样层层递进,就构成了实例与原型的链条。

    下面我们看一个实现:

    可以看到,child继承了father

    在这个例子中我们定义了两个类型father和child,child继承了father的方法和属性。继承的过程是通过将child的原型对象重写为Father的实例完成的。本质是重写原型对象,以一个新类型的实例代替。

    此时我们也可以在child中再添加其他方法:

    只需在child的原型中添加方法即可

    通过一张关系图可以明晰child和father之间的关系:

    instance是SubType.prototype的实例,SubType.prototype是SuperType.prototype的实例

    这其中的SubType.prototype既是instance实例的原型,又是SuperType.prototype的实例。因此他拥有一个实例才拥有的[[prototype]]的指针,这个指针指向它的原型(SuperType.prototype),同时它的实例instance的[[prototype]]则指向它。

    此时如果调用instance.subproperty属性,则调用了一层搜索(搜索实例)。

    如果调用instance.getSubValue()方法,实际上是我们之前介绍过的二层搜索(搜索实例的原型)。

    而调用instance.getSuperValue()方法,实际上是第三层搜索(搜索实例的原型,而将实例的原型作为实例2,搜索实例2的原型)。

    因此通过实现原型链,本质上是扩展了前面我们介绍的原型搜索机制


    1.1别忘记默认的原型

    实际上在上面的原型链中还少了一环,我们知道所有引用类型都默认继承了Object,这个继承也是通过原型链实现的,因此对于SuperType.Prototype,它实际上也是Object的一个实例。它的[[Prototype]]实际上指向了Object.prototype:

    这里展示一个完整的原型链

    这也正是所有自定义类型都会继承toString,valueOf,isPrototypeOf这些方法的根本原因,实际上,我们在访问instance.toString()时,本质上都是通过4次搜索后,调用了Object.prototype中的方法。

    我们可以看到,只有底层的SubType.Prototype没有constructor指向其构造函数,这是因为SubType.Prototype是我们人工重写过的,因此它的constructor不指向其构造函数。

    我们可以看到,子类的constructor指向了父类的构造函数

    2.确定原型和实例的关系

    子类继承父类后,如果使用instanceof测试针对Object,Father,child三个类型,都会返回true。

    三个构造函数都是instance的构造函数

    同样同一个属性isPrototypeOf()方法,只要时原型链中出现的原型,都时该原型链所派生的实例的原型。如果使用isPrototypeOf()测试针对Object,Father,child三个类型,都会返回true。

    三个原型都是instance的原型

    3.谨慎定义方法

    在这里只要强调无论如何,给原型添加方法的代码一定要放在替换原型的语句之后。换个更浅显的说法:给子类加方法一定要在重写子类之后。

    重写方法和添加新方法

    重写父类方法的本质其实是在二层搜索时就找到了getValue(),因此解释器也就不会再进行三层搜索,去Father.prototype中找getValue()方法了。

    同样,需要注意的是,在给子类添加属性或方法的时候,不能使用字面量法添加新属性和方法。因为我们知道,字面量法本质上也是在重写原型对象。这会导致[[prototype]]指针失灵。

    4.原型链的问题

    原型链的问题之一与之前一样,都来自于包含引用类型值的原型。我们知道,原型中引用类型的属性会被所有实例共享。这会导致在通过原型来实现继承时,原型实际上会变成另外一个类型的实例,原先的实例属性(this.colors)也就顺理成章地变成了现在的原型属性(Subtype.prototype.colors)了。

    和原型的问题一样,都是通过原型继承的引用属性会引起共享

    因为SubType通过原型链继承了SuperType后,SubType.propotype就变成了SuperType的实例,因此它也拥有了colors属性。这和专门创建了一个SubType.prototype.colors属性是一个道理。因此所有它的实现就会共享这个colors属性,这本质上和原型的问题是一致的。

    原型的问题之二是在创建字类型的实例时,无法向父类型的构造函数中传递参数,类似java中super的用法。如果强行这样做,则很有可能印象已经实现的实例。

    基于这两个问题,我们在实践中通常很少会单独使用原型链。

    相关文章

      网友评论

        本文标题:2020-01-03:继承原理(原型链)

        本文链接:https://www.haomeiwen.com/subject/zritactx.html