继承是什么?怎么实现继承呢?

145 阅读6分钟

继承是什么?

继承可以将重复的代码逻辑抽取到父类中,子类只需要直接继承过来使用即可。

怎么实现继承?

原型链实现继承(组合继承)

首先回顾一下原型,原型链是什么?

原型

  1. JavaScript的所有对象中都包含了一个 [_ _ proto _ _ ]内部属性,这个属性所对应的就是该对象的原型
  2. JavaScript的函数对象,除了原型 [_ _ proto _ _ ] 之外,还预置了 prototype 属性
  3. 当函数对象作为构造函数创建实例时,该 prototype 属性值将被作为实例对象的原型[_ _ proto _ _]
  4. 函数的原型上有一个属性constructor,指向函数本身

原型链

  1. 当一个对象调用的属性/方法在自身不存在时,就会去自己 [_ _ proto_ _ ] 关联的前辈prototype 对象上去找
  2. 如果没找到,就会去该 prototype 原型对象的 [_ _ proto_ _ ] 关联的前辈 prototype 去找。依次类推,直到找到属性/方法或 undefined 为止,从而形成了所谓的“原型链”

实现继承

  1. 创建两个构造函数,往里面添加属性和方法,这时创建出来的stu对象并不能使用Person里面的属性和方法
// 父类:公共的属性和方法
function Person(){
    this.name = 'bx',
    this.friends = []
}

Person.prototype.eating = function(){
    console.log(this.name + '正在吃饭')
}

// 子类:特有的属性和方法
function Student(){
    this.sno = 111
}

Student.prototype.studying = function(){
    console.log(this.name + '正在学习')
}
var stu = new Student()

// 2.创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()
  1. 往Student原型添加方法之前,让Student的原型prototype指向Person创建出来的那个对象p,这样就形成了原型链,当在stu对象上找不到某个属性或方法时,就沿着原型链去p对象身上找,p对象上找不到就去p的原型prototype上找,这样就实现了继承
// 父类:公共的属性和方法
function Person(){
    this.name = 'bx',
    this.friends = []
}

Person.prototype.eating = function(){
    console.log(this.name + '正在吃饭')
}

// 子类:特有的属性和方法
function Student(){
    this.sno = 111
}

var p = new Person()
Student.prototype = p
// 合并为一行代码  Student.prototype = new Person()

Student.prototype.studying = function(){
    console.log(this.name + '正在学习')
}

var stu = new Student()

// 2.创建出来两个stu对象
var stu1 = new Student()
var stu2 = new Student()

这时stu1和stu2身上都有了name和friends属性,有了eating方法,相当于继承了父类的属性和方法

但是这种实现继承的方法有几个弊端,开发中不使用这种方法:

  1. 打印stu对象,继承的属性是看不到的,而且体现不出来自己类的名字,而是父类的名字,后面寄生组合式继承会解释到
打印stu1出来的是  Person { sno: 111 }
  1. 获取引用,修改引用中的值,会互相影响

如果给stu1的friends添加数据,会影响到stu2的friends,因为它们指向的都是p对象

stu1.friends.push('bx')
console.log(stu1.friends)  // ['bx']
console.log(stu2.friends)  // ['bx']

如果stu1.friends = []就是给本对象添加新的属性了,不会影响到stu2

  1. 前面实现类的过程中都没有传递参数

构造函数实现继承

在Person里传入三个参数name,age,friends,在Student构造函数里用call调用Person函数,并把this和name,age,friends参数传进去,解决了以下问题:

  1. 打印stu对象可以看到继承的属性,因为这些属性都加在自己身上了
  2. 获取引用,修改引用中的值,不会互相影响
  3. 在创建stu对象的时候就能传入参数,解决上面原型实现继承的第三个弊端
  4. 这里是借用构造函数实现继承,调用的是子类函数,但实际上是借用父类构造函数来给子类身上加属性 代码如下:
// 父类:公共的属性和方法
function Person(name,age,friends){
    // 现在的情况下this就是Student对象了
    this.name = name,
    this.age = age,
    this.friends = friends
}

Person.prototype.eating = function(){
    console.log(this.name + '正在吃饭')
}

// 子类:特有的属性和方法
function Student(name,age,friends,sno){
    // 传入this
    Person.call(this,name,age,friends)
    this.sno = 111
}

var p = new Person()
Student.prototype = p
// 合并为一行代码  Student.prototype = new Person()

Student.prototype.studying = function(){
    console.log(this.name + '正在学习')
}

var stu = new Student()

// 2.创建出来两个stu对象
var stu1 = new Student('bx',21,['yjt'],11)
var stu2 = new Student('lucy',22,['wje'],12)

这种方法虽然解决了原型链实现继承的弊端,但是还是有不足之处:

  1. Person函数至少被调用了两次,创建p对象一次,创建stu对象一次
  2. stu的原型对象上会多出一些属性,这些属性没有必要存在。p对象上没有必要存在name,age,friends这些属性,因为这些属性在stu对象创建的时候及已经存在在stu身上了,p里面存在这些属性没用

注意:如果为了不给p对象添加额外没用的属性,不可以这样做:Student.prototype = Person.prototype,这样的话,两个构造函数的原型都是Person的原型对象,给Student的prototype加属性和方法也会加到Person的原型对象上,会让Person的原型对象越来越大,不符合面向对象的思想

原型式继承

这里obj使用字面量方式创建,想让obj作为info的原型,创建了createObject函数,Object.setPrototypeOf(newObj,o)方法意思是把o作为newObj的原型,createObject2里面的实现和createObject意思一样

var obj = {
    name:'bx',
    age:18
}

// 原型式继承函数
function createObject(o){
    var newObj = {}
    Object.setPrototypeOf(newObj,o)
    return newObj
}

function createObject2(o){
    function Fn(){}
    Fn.prototype = o
    var newObj = new Fn()
    return newObj
}

var info = createObject(obj)
// var info = createObject2(obj)

console.log(info)             //{}
console.log(info.__proto__)   //{ name: 'bx', age: 18 }

createObject和createObject2方法可以用一个方法代替:Object.create()

var info = Object.create(obj)

这样也实现了info的原型是obj,就可以继承obj的属性

寄生式继承

思想:寄生式继承是与原型式继承紧密相关的一种思想,思路是结合原型式继承和工厂函数的一种方式,创建一个封装继承过程的函数,在该函数内部以某种方式来增强(扩展)对象,最后再将这个对象返回

var personObj = {
    running:function(){
        console.log('running')
    }
}
// 工厂函数(需要把创建的对象明确的return出去)
function createStudent(name){
    var stu = Object.create(personObj)
    stu.name = name
    stu.studing = function(){
        console.log('studying')
    }
    return stu
}

var stu1 = createStudent('bx')
var stu2 = createStudent('yjt')
var stu3 = createStudent('lhw')

寄生组合式继承

即通过借用构造函数来继承属性,通过原型链继承方法

function Person(name,age,friends){
    this.name = name,
    this.age = age,
    this.friends = friends
}

Person.prototype.eating = function(){
    console.log('eating~')
}

function Student(name,age,friends,sno,score){
    //借用构造函数来继承属性
    Person.call(this,name,age,friends)
    this.sno = sno,
    this.score = score
}

//通过原型链继承方法
Student.prototype = Object.create(Person.prototype)

Student.prototype.studying = function(){
    console.log('studying~')
}

var stu1 = new Student('bx',21,'wyy',123,100)

console.log(stu1)
stu1.eating()    //eating~
stu1.studying()  //studying~

这里打印出来的stu1是Person { name: 'bx', age: 21, friends: 'wyy', sno: 123, score: 100 }

为什么名字是Person呢?

Student.prototype = Object.create(Person.prototype)这行代码执行时,Object.create()创建出来的新对象取代了Student.prototype一开始指向的对象,这个新创建出来的对象没有constructor属性,但是名字要从constructor的name取,在自身找不到就沿着原型链找,所以就找到了Person.prototype,因为Person.prototype身上有constructor属性,并且指向Person函数,所以这里的名字是Person。

这里也解释了原型链继承的弊端1

这里封装了一个实现继承的函数,包括修改constructor名字

// 封装一个继承方法,SubType子类,SuperType父类
function inheritPrototype(SubType,SuperType){
    SubType.prototype = Object.create(SuperType.prototype)
    // 修改constructor的名字
    Object.defineProperty(SubType.prototype,"constructor",{
        enumerable:false,    //可枚举
        configurable:true,   //可配置
        writable:true,       //可写
        value:SubType        //名字
    })
}

也可以这么写:

function createObject(o){
    function Fn(){}
    Fn.prototype = o
    return new Fn()
}

// 封装一个继承方法,SubType子类,SuperType父类
function inheritPrototype(SubType,SuperType){
    // SubType.prototype = Object.create(SuperType.prototype)
    SubType.prototype = createObject(SuperType.prototype)
    // 修改constructor的名字
    Object.defineProperty(SubType.prototype,"constructor",{
        enumerable:false,
        configurable:true,
        writable:true,
        value:SubType
    })
}

后面在往Student的prototype上添加方法之前调用这个方法inheritPrototype(Student,Person)

image.png