JS面向对象篇四、原型链与继承(多种继承实现方式及其利弊分析)

本片文章内容:
1、什么是原型链;
2、利用原型链实现继承;
3、借用构造函数实现继承;
4、组合继承(最常用的继承模式);
5、原型式继承:Object.create();
6、寄生式继承、寄生组合继承(最理想的继承范式);

前言.....

由于本文篇幅较长,避免大家看了后乱了章法,还是进一步详细明确下主要内容。首先介绍了什么是原型链,理解了原型链的概念才可以进一步学习继承的知识,已经了解的可跳过。因为javascript的继承主要是依靠原型链来实现的,当然实现继承的方式有很多种,上面也有罗列,其中组合继承是最常用的方式,因为它是由原型链和借用构造函数两种方式结合而来,所以可以先简单了解借用构造函数以及原型链实现继承的方法。剩下的原型式继承和寄生式继承,都是为了最后的寄生组合式继承所做的铺垫,因为寄生组合式继承相比最常用的组合继承更加高效,所以这种方式也是最理想的继承范式。

原型链

由于前面的文章中已经详细写过关于原型的文章,那么先来回顾一下原型、构造函数和实例对象的关系,请谨记下面的规则,这是理解原型链的基础:
每一个构造函数都有一个属性叫prototype,这个属性是一个指针,它指向了一个对象,这个对象就是原型;
每一个原型对象默认会获取一个constructor属性,这个属性又指回了构造函数;
每一个实例对象都有一个内部属性[[prototype]],这个属性指向创建它的构造函数的prototype属性值,也就是原型对象;
当试图获得一个对象的某个属性时,若这个对象本身没有这个属性,那么就要去它的[[prototype]](即它的构造函数的prototype)中寻找。

由于很多浏览器中通过__proto__能够访问到对象内部的[[prototype]]属性,所以接下来讨论中我们都会使用__proto__代替[[prototype]]。

知道了以上规则,那么原型链其实就是从一个对象出发,每一个对象都有一个__proto__属性,这个属性指向这个对象的构造函数的prototype属性值,即原型对象,而原型对象本身也是一个对象,因此它本身也有一个__proto__属性,这个属性再次指向这个原型对象的构造函数的prototype属性值,以此类推,这样由对象的__proto__属性关联形成的这条链就是原型链。
这里要谨记一个问题,因为所有对象都是通过new Object()创建的,那么所有函数的默认原型都是Object的实例。

function Person(name) {
    this.name = name;
}
var p = new Person();
console.log( p.__proto__ == Person.prototype);//true,实例对象p的__proto__属性指向函数Person的prototype属性值;
console.log(p.__proto__.constructor);//结果:ƒ Person(name) {this.name = name;},p的构造函数为Person函数
console.log(Person.prototype.__proto__.constructor);//ƒ Object() { [native code] },Person.prototype这个对象的构造函数(即Person.prototype这个对象的__proto__中的constructor属性)为Object
console.log(Object.prototype == Person.prototype.__proto__);//因此Person.prototype对象的__proto__属性指向它的构造函数Object的prototype
console.log(Object.prototype.__proto__);//null,再向上一层去找Object的原型(Object.prototype)也是一个对象,它的原型为null;

注意由于上面代码中打印Person.prototype
以上代码形成了的原型链:p——>p.__proto__(Person.prototype)——>Person.prototype.__proto__(Object.prototype)——>null,如下图所示:
在这里插入图片描述
到这里如果没有理解也没有关系,继续看下面的原型链实现继承可以更加清晰的了解原型链。

原型链实现继承

javascript中的继承主要是依靠原型链来实现的,其基本思想就是利用原型,让一个引用类型继承另一个引用类型的属性和方法。
具体做法就是重写创建引用类型a的构造函数A的原型对象为另一个引用类型b。

function Father () {
    this.fatherName = 'dad'
}
Father.prototype.say = function () {
    console.log(this.fatherName)
}
function Child () {
    this.childName = 'son'
}
Child.prototype = new Father()
Child.prototype.sing = function () {
    console.log('hello dad')
}
var c = new Child()
c.say() // dad

利用原型,我们将子类型Child构造函数的原型重写为父类型的实例,这样,当在子类型的实例c上访问一个属性时,会先在实例c本上寻找,如果没有找到,则回去c的原型对象,也就是Child.prototype上去找,而这时Child.prototype指向的是Father的实例,由此实现了子类型访问父类型实例的属性和方法(包括父实例对象和原型对象上的所有属性和方法),也就实现了继承。
在这里插入图片描述
所有函数的默认原型都是Object的实例,所以Father.prototype就是Object实例,那么Father.prototype.__proto__就是Object.prototype,Object.prototype也是一个对象,在向上找Object.prototype这个对象的原型则是null。

注意事项

这种利用原型链实现继承的方式有一个特别需要注意的问题:如果子自类型的原型上需要添加自己的方法或者覆盖父类型的方法,必须将添加方法的代码写在替换原型的语句之后,否则会找不到。另外也不能在替换了自类型构造函数的原型为另一个实例后,再用字面量的方式给自类型添加方法,这样做则会切断自类型与父类型的联系,不再能实现继承。

function Father () {
    this.fatherName = 'dad'
}
Father.prototype.say = function () {
    console.log(this.fatherName)
}
function Child () {
    this.name = 'son'
}
//1、先替换原型
Child.prototype = new Father()
//2、再添加方法
Child.prototype.sing = function () {
    console.log('hello dad')
}
//3、若添加方法像下面这样使用字面量方式就不能实现继承了
//Child.prototype = {
//  sing : function () {
//      console.log('hello dad')
//  }
//}
var c = new Child()
c.say() // dad
存在的问题

问题一:原型上的引用类型属性会被所有子类型实例共享,它们之间的修改相互影响;
问题二:没办法像父类型的构造函数传参数,以使得所有实例动态设置自己的属性值;
基于以上很少单独使用原型链来实现继承。

借用构造函数

这种方式的基本思想:在自类型构造函数内部,利用call或apply方法调用父类型构造函数,传入当前作用域,即this,就可以添加每个子类型实例自己的属性了。
因为每个自实例都拥有自己的属性,所以避免了共享属性带来的相互影响的问题。另外,call方法除了可以传一个this,还可以传其他的参数给父类型构造函数,从而动态设置子类型的属性值。

function Father (name, age) {
    this.name = name
    this.age = age
    this.interest = ['sing', 'book']
}
function Child (name, age, sex) {
    //继承了Father,同时传递了参数给父类型构造函数
    Father.call(this, name, age)
}
var sister = new Father('Amy', 18)
var brother = new Father('Tom', 14)
sister.interest.push('paint')
console.log(sister.interest) // ['sing', 'book', 'paint']
console.log(brother.interest) // ['sing', 'book']

如果单独使用借用构造函数的方法实现继承,那么方法就只能定义在构造函数中了,那么就会变成创建n个实例,产生n个同样的函数,就没有函数复用可言了。所以我们也很少单独使用这种方式。

组合继承

结合原型链与借用构造函数实现继承的两种方法来实现继承叫组合继承。
基本思想就是:原型链实现原型上的可共享属性和方法的继承,借用构造函数实现父类型实例属性的继承。

function Father (name, age) {
    this.name = name
    this.age = age
    this.interest = ['sing', 'book']
}
Father.prototype.say = function () {
    console.log('hello my children')
}
function Child (name, age, sex) {
    //继承父类型属性
    Father.call(this, name, age)
    this.sex = sex
}
//继承方法
Child.prototype = new Father()
Child.prototype.constructor = Child
Child.prototype.sing = function () {
    console.log('hello dad')
}
var sister = new Father('Amy', 18, 'girl')
var brother = new Father('Tom', 14, 'boy')
sister.interest.push('paint')
console.log(sister.interest) // ['sing', 'book', 'paint']
console.log(brother.interest) // ['sing', 'book']
console.log(sister.name) // Amy
console.log(brother.name) // Tom

组合继承方式是javascript中最常用的继承模式。

原型式继承:Object.create()

基本思想:借助原型基于已有的对象创建新对象。

function object (o) {
    function F() {}
    F.prototype = o
    return new F()
}

以上代码可以看出,这个object函数的作用就是,返回一个继承给定对象的新对象。如:

var father = {
    name : 'zhangsan',
    age : 40,
    interest : ['sing', 'book'],
    sayName : function () {
        console.log(this.name)
    }
}
var child1 = object(father)
var child2 = object(father)
console.log(child1.name)
console.log(child2.name)
child1.interest.push('paint')
console.log(child1.interest) // ['sing', 'book', 'paint']
console.log(child2.interest) // ['sing', 'book', 'paint']

通过这种方式可以创建两个继承father对象的实例对象child1、child2。
ECMAScript通过新增的Object.create()方法规范化了原型式继承。
这个方法接收两个参数,使用方法如下:

var father = {
    name : 'youyang',
}
var child = Object.create(father, {
    name : {
        value : 'xiaoqu'
    }
})
console.log(child.name) // xiaoqu

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同,关于Object.defineProperties()方法的使用以及属性描述符相关问题可以阅读这篇文章:理解对象及属性特性(属性描述符)

注意事项

原型式继承实际上就是返回了一个指定对象的副本而已。
仔细想来Object.create()方法也只是一个创建对象的方法,只不过这个对象的原型为某个指定对象

存在的问题

父类型的引用类型属性在自类型实例中会共享,修改后会相互影响。

原型式继承在只想让一个对象与另一个对象类似的情况下,没有必要创建构造函数时完全可以胜任。

寄生式继承

寄生式继承是在原型式继承的基础上扩展来的,之前也强调过原型式继承只是返回了指定对象的一个副本,所以原型式继承并没有给子类型添加属于自己的属性和方法。
寄生式继承的思想是:依旧在函数内部,利用原型基于已有对象创建新的对象,不同的是要为这个新对象添加一些属于自己的属性和方法。

function createAnother (o) {
    var another = object(o) // object是原型式继承中的那个object函数
    another.sayHello = function () {
        console.log('hello world')
    }
    return another
}

上面封装的函数createAnother就是寄生式继承的实现。

var father = {
    name : 'youyang',
}
var child = createAnother(father)
child.sayHello() // hello world
存在的问题

使用这种方式来为子类型添加函数时,依然也是不能做到函数复用,没创建一个对象,就会相应创建一个方法(这里指sayHello),会降低效率。

寄生组合式继承

前面说到组合继承是最常用的继承模式,但是这种方式其实还是有不足的,它的问题就是要执行两次父类型的构造函数。可回顾一下之前组合继承的代码,分别在子类型构造函数内部通过call方法调用了一次,以及在重写自类型构造函数原型的时候又调用了一次。
所谓寄生组合式继承就是结合组合继承和寄生式继承两种方式来实现继承,具体思想是:将子类型构造函数原型重写为父类型实例改为重写为父类型构造函数原型对象的副本。

function createAnotherPrototype(Father, Child) {
    var another = object(Father.prototype)
    another.constructor = Child
    Child.prototype = another
}

以上就是寄生组合式继承的核心代码,于是只要将组合继承例子中的Child.prototype = new Father()这行代码改为调用上面封装好的函数即可:createAnotherPrototype(Father, Child);

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式!

猜你喜欢

转载自www.cnblogs.com/youyang-2018/p/11777076.html