JavaScript 学习之继承

Javascript 的继承的实现方法有不少种,以前虽然学习过,可是没有综合整理过,这一次就来整理整理 Javascript 语言的继承方面的知识。关于详细的Javascript 的继承方面的知识,推荐你们去看那本红宝书 ————《JavaScript高级程序设计》。javascript

虽然 ES6 推出了 class 这个概念,方便了咱们开发人员的学习和理解,可是,class 只是一个语法糖,实际上底层的实现仍是原来的那一套,利用原型链和构造函数来实现继承。所以要想 Javascript 的基本功牢实一点,仍是须要去学习这些知识的。java

在 Javascript 的继承实现里,目前有原型链继承法,构造函数继承法,组合继承法等等方法,下面我就一一对这些方法来进行说明。app

1. 原型链继承

原型链继承法是运用 Javascript 的原型来实现,在 Javascript 中任意函数都拥有 prototype__proto__ 这两个属性,而每一个对象都拥有一个 __proto__ 属性,对象里 __proto__ 属性的值是来自于构造这个对象的函数的 prototype 属性,经过 prototype__proto__ ,咱们构造出原型链,而后利用原型链来实现继承。函数

具体的代码例子以下学习

function Animal() {
    this.type = 'Cat'
    this.name = 'Nini'
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}

function Cat() {
    this.age = '1'
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

let smallCat = new Cat()
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini

let bigCat = new Cat()
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
复制代码

从上面的例子咱们能够看到,原型链继承的优势:优化

  • 多个实例共同引用可复用的属性和方法,不是建立每个实例的时候再建立一遍这些数据

缺点:ui

  • 全部的属性都被实例所共享,这意味着若是属性是基本数据类型的话,实例是没法修改这个属性的值,由于实例会新增一个同名的属性,咱们只能对新增的属性进行操做,以刚刚的代码为例
smallCat.name = 'Kiki' // 此时 smallCat 对象上新增了 name 属性,若是访问这个属性的话,咱们获得是这个新增的属性而不是在原型上的 name 属性
console.log(smallCat.name) // 'Kiki'
console.log(bigCat.name) // 'Nini'
复制代码

若是属性是引用属性的话,修改这个属性所指向的数据里的内容将会影响全部的实例(注意不是对属性直接赋值,若是直接赋值了就像基本数据类型同样,在实例自己上新建一个同名属性),如以前的代码实例this

smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball', 'sleep' ]
复制代码

2. 构造函数继承

构造函数继承的基本原理就是利用 call, apply 这样的能够指定函数 this 值的方法,来实现子类对父类属性的继承,例子以下spa

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
function Cat(type, name) {
    Animal.call(this, type, name)
    this.age = '1'
    this.say = () => {
        console.log('type is ' + this.type + ' name is ' + this.name);
    }
}
let smallCat = new Cat('Cat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini

let bigCat = new Cat('Cat', 'Nicole')
console.log(bigCat.hobbies) // [ 'eat fish', 'play ball' ]
bigCat.say() // type is Cat name is Nicole
复制代码

从上面的例子能够看到,构造函数继承的优势是prototype

  • 全部的实例没有共享引用属性,也就是说每一个实例都独立拥有一份从父类那里继承来的属性,任一个实例修改了引用属性里的数据内容,并不会影响到其余的实例

  • 可向父函数传参

缺点:

  • 因为全部的属性和方法都再也不被全部的实例共享,所以那些公有的属性和方法就会被重复的建立,形成了内存的额外开销

3. 组合继承 (原型链继承和构造函数继承的合体)

其实经过以前的分析,能够知道,不管是原型链继承仍是构造函数继承,都存在本身的优缺点,对于咱们的开发实现而言,都是不完美的。原型链继承把全部的属性和方法都共享给了全部的实例,也就是说,咱们想要个性化的针对某一实例上所继承的引用属性的数据内容进行修改的话,这一操做将同时影响别的实例,这可能会给咱们的开发带来必定的问题。构造函数继承把全部的属性和方法都为每一个实例单独拷贝了一份,虽然实现了实例之间的数据隔离,可是对于那些原本就应该是公共的属性和方法来讲,重复而无心义的复制也无疑是增长了额外的内存开销。

所以,组合继承方法吸取了这两个方法的优势,同时避免了各自的缺点,是一种可行的实现继承的方法,实现的代码以下

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
    Animal.call(this, type, name) // 构造函数继承
    this.age = '1'
}
Cat.prototype = new Animal() // 原型链继承
Cat.prototype.constructor = Cat

let smallCat = new Cat('smallCat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini

let bigCat = new Cat('bigCat', 'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
复制代码

组合继承方法的思路是将公共的属性和方法放在父类的 prototype 上,而后利用原型链继承来实现公共的属性和方法的继承,而对于那种每一个实例均可自定义修改的属性采起构造函数继承的方法来实现每一个实例都独有一份这样的属性。

4. 原型式继承

原型式继承的实现原理就是将一个对象做为建立对象的原型传入到一个构建新对象的函数中,好比

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}
复制代码

其实原型式继承的思路也就是 Object.create() 方法的实现思路,来看看一个完整的原型式继承的实现,代码以下

let Animal = {
    type: 'Cat',
    name: 'Nini',
    hobbies: ['eat fish', 'play ball']
}

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

let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
bigCat.name = 'Nicole' // 直接在 bigCat 这个对象上新增一个 name 属性,并不是去修改原型上的 name 属性
console.log(smallCat.name); // 'Nini'
console.log(bigCat.name); // 'Nicole'
console.log(bigCat.__proto__.name); // 'Nini' 原型上的 name 属性依旧保持
复制代码

原型式继承法其实和原型链继承有点类似,都是全部的属性和方法放在了原型上,若是建立全部的实例时都用的是同一个对象做为原型的话,那么原型链继承遇到的问题,这个方法一样也有。

关于原型式继承的更多思考

在学习原型式继承的时候,我想到了若是建立每一个实例的时候,传入的父类对象都是不一样的对象,可是都是同属于一个父类的对象,那么若是咱们将公共的属性和方法放在父类的原型上,把可自定义的属性放在父类的构造函数上,那也能够实现比较合理的继承,具体代码以下

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function createCat(o) {
    function F() {}
    F.prototype = o
    return new F()
}

let smallCat = createCat(new Animal('smallCat', 'Nini'))
let bigCat = createCat(new Animal('bigCat', 'Nicole'))
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
复制代码

这个思路看起来不错,可是仔细想一想仍是有必定的问题的,相比于以前提到的组合式继承来讲,这个方法每次在建立实例的时候,咱们都会 new 一个新的父类实例,这其实形成了内存的浪费,而组合继承则保证了父类的实例只会被 new 一次,而那些能够自定义的属性都被存在每一个子类的实例中,保证了数据的互不影响,咱们能够经过下面的图片来看看具体的差别

5.寄生式继承

寄生式继承其实和原型式继承的实现有些类似,不过寄生式继承在原型式继承的基础上添加了在建立实例的函数中以某种形式来加强对象,最后返回对象。其实意思就是,在建立子实例的函数中,先经过原型式继承的方法建立一个实例,而后为这个实例添加属性和方法,最后返回这个实例,代码实例以下

function createCat(o) {
    let cloneObj = Object.create(o)
    cloneObj.say = function (){ // 为实例添加一个 say 方法
        console.log('type is ' + this.type + ' name is ' + this.name);
    }
    return cloneObj
}

let Animal = {
    type: 'Cat',
    name: 'Nini',
    hobbies: ['eat fish', 'play ball']
}

let smallCat = createCat(Animal)
let bigCat = createCat(Animal)
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is Cat name is Nini
bigCat.say() // type is Cat name is Nini
复制代码

经过上面代码咱们能够很清楚的看到,寄生式继承有原型链继承的缺点和构造函数继承的缺点,也就是说经过寄生式继承创造出来的实例,若是修改了它原型上的引用属性里的内容,其余的实例也会受影响,并且每次建立实例的时候,那些公共的属性和方法都会被建立一次。

6. 寄生组合式继承

上面咱们提到了组合式继承是一种还不错的继承实现方式,既能让每一个实例拥有继承来的可自定义的属性和方法,也能共享公共的方法和属性。可是这种方法还有可以优化的地方,这个须要优化的点在于,组合式继承时,父类的构造函数会被调用两次,结合代码看一下

function Cat(type, name) {
    Animal.call(this, type, name) // 这里调用了一次父类的构造函数
    this.age = '1'
}
Cat.prototype = new Animal() // 这里也调用了一次父类的构造函数
Cat.prototype.constructor = Cat
复制代码

实际上,子函数的 prototype 只须要指向那些公共的属性和方法就能够了,不须要指向整个父函数的实例,因为咱们把须要继承的公共的属性和方法放在了父函数prototype 上,因此咱们能够考虑让子函数的 prototype 间接访问父函数的 prototype。实现的代码例子以下

// 利用寄生式继承来让子函数的 prototype 能访问到父函数的原型
function createObj(child, parent) {
    let prototype = Object.create(parent.prototype) 
    // 这个对象相比于父实例少了那些子函数已经过parent.call 继承到的属性和方法,仅仅含有一个指向父函数原型的属性
    prototype.constructor = child
    child.prototype = prototype
}
createObj(Cat, Animal)
复制代码

最后,完整的寄生组合式继承的实现代码以下

function Animal(type, name) {
    this.type = type
    this.name = name
    this.hobbies = ['eat fish', 'play ball']
}
Animal.prototype.say = function () {
    console.log('type is ' + this.type + ' name is ' + this.name);
}
function Cat(type, name) {
    Animal.call(this, type, name)
    this.age = '1'
}

function createObj(child, parent) {
    let prototype = Object.create(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
createObj(Cat, Animal)

let smallCat = new Cat('smallCat', 'Nini')
smallCat.hobbies.push('sleep')
console.log(smallCat.hobbies); // [ 'eat fish', 'play ball', 'sleep' ]
smallCat.say() // type is smallCat name is Nini

let bigCat = new Cat('bigCat', 'Nicole')
console.log(bigCat.hobbies); // [ 'eat fish', 'play ball' ]
bigCat.say() // type is bigCat name is Nicole
复制代码

所以寄生组合式继承在吸收了组合式继承的优势上,避免了在子函数的原型上面建立没必要要的、多余的属性,而寄生组合式继承也是目前的一种理想的比较好的继承方法的实现。

总结

其实 Javascript 继承的关键点是必定要将私有的属性和方法,公有的属性和方法分别处理,私有的属性和方法须要让每一个实例都独有一份,保证数据的更改互不影响,公有的属性和方法须要放在父类的原型上,确保不重复建立。

相关文章
相关标签/搜索