资讯专栏INFORMATION COLUMN

JavaScript面向对象---原型链继承

vspiders / 2929人阅读

摘要:因为这造成了继承链的紊乱,因为的实例是由构造函数创建的,现在其属性却指向了为了避免这一现象,就必须在替换对象之后,为新的对象加上属性,使其指向原来的构造函数。这个函数接收两个参数子类型构造函数和超类型构造函数。

最近一直在研究js面向对象,原型链继承是一个难点,下面是我对继承的理解
以下文章借鉴自CSDN季诗筱的博客

原型链继承的基本概念:

ES中描述了原型链的概念,并将原型链作为实现继承的主要方法;
基本思想:利用一个引用类型继承另一个引用类型的属性和方法:
简单回顾下: 构造函数 -- 原型 -- 实例 三者之间的关系
构造函数:function Person(){}
每个构造函数都有一个原型对象(Person.prototype),
原型对象都包含一个指向构造函数的指针(constructor),
(其实原型对象也是一个对象,也有一个 __proto__ 指针,指向他所继承的对象)
而实例都包含着一个指向圆形对象的内部指针([[prototye]] 又称__proto__);
每个实例也有一个constructor属性默认调用原型对象的constructor属性(!!!)

原型链继承的核心

让原型对象等于另一个构造函数的实例, 显然,此时的原型对象将包含一个指向另一个原型对象的指针([[prototype]]),从而拥有该原型对象的属性和方法

另一个原型对象中也包含着指向另一个构造函数的指针。那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念!

如上如所示,这种关系直到 当某个原型对象的 contructor属性指向 Object 为止

1.原型链继承基本模式:
function SuperType(){
    this.prototype = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
    return this.subproperty;
}
var instance = new SubType();
alert(instance.getSuperValue());  //true

以上代码定义了两个类型:SuperType 和 SubType.
每个类型分别有一个属性和方法
他们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋值给SubType.prototype实现的
实现的本质是重写原型对象,代之以一个新类型的实例
换句话说,原来存在于SuberType的实例中的所有属性和方法,现在存在于SubType.prototype中了。
在确立了继承关系之后,我们给SubType.prototype添加了一个方法,,这样就在继承了SuperType的属性和方法的基础上又添加了一个新方法。
这个例子的实例以及构造函数和原型之间的关系如下图所示
图:

在上面的代码中,我们没有使用SubType默认提供的原型,而是给他换了一个新原型
这个原型就是SuperType的实例
于是,新原型不仅有作为一个SuperType的实例所拥有的全部属性和方法,而且内部还有一个指针指向了SuperType的原型
最终结果是这样的:instace指向SubType的原型,SubType的原型又指向SuperType的原型

在通过原型链继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用instance.getSuperValue()会经历三个步骤:
1.搜索实例
2.搜索SubType.prototype
3.搜索SuperType.prototype
最后一步才会找到该方法,再找不到该属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来.
别忘记默认原型Object
事实上前面例子中展示的原型链还少一环,我们知道,所有引用类型都默认继承了Object,而这个继承也是通过原型链实现的。
大家记住,所有函数的默认原型都是Object的实例,因此默认原型内部都会包含一个指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()等默认方法的根本原因.

原型链存在的问题

原型链虽然很强大,可以用它实现继承,但也存在一些问题。

其中,最主要的问题来自包含引用类型值的原型。
想必大家还记得,我们前面介绍过包含引用类型值的属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。

在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是原先的实例属性也就顺理成章地变成了现在的原型属性了.
例子说明问题:

function SuperType(){
    this.colors = ["red","blue","green"];
}
function SubType(){

}
SubType.prototype = new SuperType();    // 继承了SuperType
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //  red,blue,green,black
var instance2 = new SubType();
alert(instance2.colors);    //  red,blue,green,black

这个例子中的SuperType构造函数定义了一个colors属性,该属性包括一个数组(引用类型值)。SuperType的每个实例都会有各自包含自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此他也拥有了一个他自己的colors属性-----就跟专门创建了一个SubType.prototype.colors属性一样。
但结果是什么呢? 所有实例都会共享这个colors属性!!!

问题1:结果是SubType的所有实例都会共享这一个colors属性。而我们对instance1.colors的修改能够通过instance2.colors反映出来,就已经充分证明这一点了

问题2:在创建自定义类型的时候,不能向超类型的构造函数中传递参数。
实际上,应该说是没有办法在不影响所有实例的情况下,给超类型构造函数传递参数。
有鉴于此,在加上前面刚刚讨论的由于原型中所包含引用类型值所带来的问题,实践中中很少多带带使用原型链

2.原型链继承缺陷的解决方法(红宝书)

1.借用构造函数
2.组合式继承
3.原型式继承
4.寄生式继承
5.寄生组合式继承

这里来谈一下最常用的组合式继承和寄生组合式继承

组合式继承:

组合继承,有时候也叫做经典继承,指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承模式。
其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例的属性的继承
请看例子:

function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}
function SubType(name,age){
    SuperType.call(this,name);   //继承属性, 第二次调用SuperType
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();       //第一次调用SuperType
SubType.prototype.constructor = SubType(); // 相当重要,此处
SubType.prototype.sayAge = function(){
    alert(this.age);
}
var instance1 = new SubType("leo",29);
instance1.colors.push("black");
alert(instance1.colors);  // r,b,g,b
instance1.sayName();      // leo
instance1.sayAge();       // 29

var instance2 = new SubType("lck",34);
alert(instance2.colors);  // r,b,g
instance2.sayName();      // lck
instance2.sayAge();       // 34

在例子中,SuperType构造函数定义了两个属性:name 和 colors。SuperType的原型定义了一个方法sayName()
SubType构造函数在调用SuperType构造函数传入了name参数,紧接着又定义了他自己的属性age,然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()方法
这样一来,就可以让两个不同的SubType实例既分别拥有自己属性和公共的colors属性,又可以使用相同的方法
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为js中最常用的继承模式。
而且 instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象
**另外说一下:

1.任何一个Prototype对象都有一个constructor指针,指向它的构造函数
2.每个实例中也会有一个constructor指针,这个指针默认调用Prototype对象的constructor属性。
结果:当替换了子类的原型之后,即 SubType.prototype = new SuperType()之后,
SubType.prototype.constructor 就指向了SuperType(),
SubType的实例的constructor也指向了SuperType(),这就出现问题了。

因为这造成了继承链的紊乱,因为SubType的实例是由SubType构造函数创建的,现在其constructor属性却指向了SuperType,为了避免这一现象,就必须在替换prototype对象之后,为新的prototype对象加上constructor属性,使其指向原来的构造函数。

组合式继承的缺点
组合继承最大的问题就是无论在什么情况下,都会两次调用超类型构造函数;
一次是在创建子类型的原型的时候,
另一次是在子类型构造函数内部。
没错子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类构造函数时重写这些属性

寄生组合式继承

此种模式解决了组合式继承的缺点
原理:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
思路:不必为了指定子类的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已

本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

寄生式组合继承的基本模式如下所示:

function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);    // 创建对象
    prototype.constructor = subType;                // 增强对象
    subType.prototype = prototype;                  // 指定对象
}

这个示例中的inheritPrototype()函数实现了寄生式组合继承的最简单形式。
这个函数接收两个参数:子类型构造函数和超类型构造函数。
在函数内部:
第一步:是创建超类型原型的一个副本
第二步:为创建的副本添加constructor属性,从而弥补因失去原型而失去的默认的constructor属性
第三步:将创建的对象(即副本)赋值给子类型的原型。
这样,我们就可以调用inherit-Protoype()函数的语句,去替换前面例中为了子类型原型赋值的语句了

实例如下:
//借助原型可以基于已有对象创建新对象
function object(o){
    var F = function(){};    // 创建一个空对象
    F.prototype = o;
    return new F();          //返回出一个实例对象
}
//寄生式组合继承
function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);  //创建父构造函数实例对象副本
    prototype.constructor = subType;    // 重写将子类原型对象的constructor属性
    subType.prototype = prototype;     // 父类实例赋值给子类原型
};         

function SuperType(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
};
function SubType(name, age){
    SuperType.call(this, name);
    this.age = age;
}
//调用此方法代替前面赋值语句,可解决两次调用超类型构造函数的问题
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
   alert(this.age);
}; 
var instance1 = new SubType("lck",29);  console.log(instance1.name,instance1.age,instance1.colors); // lck,29,r,b,g
instance1.sayName();  // lck
instance1.sayAge()   // 29

优点:
这个例子的高效率体现在他只调用了一次SuperType构造函数,
并且因此避免了在SubType.prototype上面创建不必要的,多余的属性。
与此同时,原型链还能保持不变
因此,还能正常使用instanceof 和 isPrototypeOf()。
开发人员普遍认为寄生式组合式继承是引用类型最理想的继承范式

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81113.html

相关文章

  • 面向对象JavaScript

    摘要:是完全的面向对象语言,它们通过类的形式组织函数和变量,使之不能脱离对象存在。而在基于原型的面向对象方式中,对象则是依靠构造器利用原型构造出来的。 JavaScript 函数式脚本语言特性以及其看似随意的编写风格,导致长期以来人们对这一门语言的误解,即认为 JavaScript 不是一门面向对象的语言,或者只是部分具备一些面向对象的特征。本文将回归面向对象本意,从对语言感悟的角度阐述为什...

    novo 评论0 收藏0
  • JS对象(1)重新认识面向对象

    摘要:对象重新认识面向对象面向对象从设计模式上看,对象是计算机抽象现实世界的一种方式。除了字面式声明方式之外,允许通过构造器创建对象。每个构造器实际上是一个函数对象该函数对象含有一个属性用于实现基于原型的继承和共享属性。 title: JS对象(1)重新认识面向对象 date: 2016-10-05 tags: JavaScript 0x00 面向对象 从设计模式上看,对象是...

    superw 评论0 收藏0
  • SegmentFault 技术周刊 Vol.32 - 七夕将至,你的“对象”还好吗?

    摘要:很多情况下,通常一个人类,即创建了一个具体的对象。对象就是数据,对象本身不包含方法。类是相似对象的描述,称为类的定义,是该类对象的蓝图或原型。在中,对象通过对类的实体化形成的对象。一类的对象抽取出来。注意中,对象一定是通过类的实例化来的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 马上就要到七夕了,离年底老妈老爸...

    李昌杰 评论0 收藏0
  • SegmentFault 技术周刊 Vol.32 - 七夕将至,你的“对象”还好吗?

    摘要:很多情况下,通常一个人类,即创建了一个具体的对象。对象就是数据,对象本身不包含方法。类是相似对象的描述,称为类的定义,是该类对象的蓝图或原型。在中,对象通过对类的实体化形成的对象。一类的对象抽取出来。注意中,对象一定是通过类的实例化来的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 马上就要到七夕了,离年底老妈老爸...

    Lyux 评论0 收藏0
  • SegmentFault 技术周刊 Vol.32 - 七夕将至,你的“对象”还好吗?

    摘要:很多情况下,通常一个人类,即创建了一个具体的对象。对象就是数据,对象本身不包含方法。类是相似对象的描述,称为类的定义,是该类对象的蓝图或原型。在中,对象通过对类的实体化形成的对象。一类的对象抽取出来。注意中,对象一定是通过类的实例化来的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 马上就要到七夕了,离年底老妈老爸...

    AaronYuan 评论0 收藏0
  • 前端进击的巨人(七):走进面向对象原型原型继承方式

    摘要:除了以上介绍的几种对象创建方式,此外还有寄生构造函数模式稳妥构造函数模式。 showImg(https://segmentfault.com/img/remote/1460000018196128); 面向对象 是以 对象 为中心的编程思想,它的思维方式是构造。 面向对象 编程的三大特点:封装、继承、多态: 封装:属性方法的抽象 继承:一个类继承(复制)另一个类的属性/方法 多态:方...

    wums 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<