JS中的五种继承方式

192 阅读4分钟

继承的概念

通过某种方式,一个对象可以访问到另一个对象的属性和方法,这个方式就是继承

并不是所谓的‘xxx extends yyy’

为什么要有继承

function Person(){
   this.say = function(){}
}
let p1 = new Person()
let p2 = new Person()
console.log(p1.say === p2.say)//false

打印false,说明p1和p2的say()方法不是同一个方法。say()方法功能一样,但是没有指向同一块内存,会造成内存浪费。

解决:在某个构造函数的prototype对象(即原型对象)中添加方法,这样,添加的方法能被所有的构造实例通过原型链访问到,这种方法即原型链继承

function Person(){
}
Person.prototype.say = function(){console.log('say')}
let p1 = new Person()
let p2 = new Person()
console.log(p1.say === p2.say)//true

这时候,p1.say和p2.say指向了同一块内存。

Person.prototype是Person构造函数的实例的原型对象,或者说p1,p2的原型对象是Person.prototype

Person的原型对象是什么呢?首先找Person的构造函数Function,然后就知道了Person的原型对象是Function.prototype

原型链继承

上面提到过的解决方案就是原型链继承,缺点:多个方法这样添加,造成代码冗余。

原型链继承改进版

一般的,对于新原型,要添加一个constructor属性,指向原构造函数,目的是不破坏原型对象的结构

Person.prototype={
  constructor:Person,
  say:function(){console.log('say方法')},
  sing:function(){}
}
let p1 = new Person()
p1.say()//打印say()方法

注意:一般的,先改变原型对象,再创建对象。因为,对象在创建的时候已经有了一个确定的原型对象,就是旧的Person.prototype,由于Person.prototype后面被重新赋值,但是p1对象的原型对象却没有改变,所以P1对象访问不到say()方法(如下)

let p1 = new Person()
Person.prototype={
  constructor:Person,
  say:function(){console.log('say方法')},
  sing:function(){}
}
p1.say()//打印undefined

拷贝继承

有时候使用某个对象中的属性和方法,但是不能直接修改它,可以创建一个该对象的拷贝。拷贝这种方式能够访问到其他对象的属性和方法,也算一种继承了。

模拟jquery中的extend方法实现拷贝
let $ = {
   extend:function(source,target){
    for(let key in source){
        target[key] = source[key]
    }
   }
}
let p1 = {'name':'jack'}
let p2 = {}
$.extend(p1,p2)
p2.name = 'rose'
console.log(p1.name)//jack
console.log(p2.name)//rose
浅拷贝和深拷贝

浅拷贝就只拷贝了一层属性,

深拷贝利用递归,把对象的所有层属性都拷贝出来;深拷贝和浅拷贝的区别就是前者有递归。

浅拷贝Object.assign
let p1 = {'name':'jack'}
let p2 = Object.assign({},p1)
p2.name = 'rose'
//修改p2不会影响p1
console.log(p1.name)//jack
console.log(p2.name)//rose
浅拷贝扩展运算符...
let p1 = {'name':'jack'}
let p2 = {...p1}
p2.name = 'rose'
//修改p2不会影响p1
console.log(p1.name)//jack
console.log(p2.name)//rose
JSON.parse(JSON.stringify(object)) 深拷贝
let a = {
    name:'jack',
    jobs:{
        first:'boss'
    }
 }
let b = JSON.parse(JSON.stringify(a))
b.jobs.first = 'worker'
console.log(a.jobs.first)//boss

但是该方法也是有局限性的:会忽略 undefined;会忽略 symbol;不能序列化函数;不能解决循环引用的对象

原型式继承

Object.create()

使用场景1:创建一个纯洁的对象,对象内部什么属性都没有,而使用字面量let a = {}并不是纯洁的

let a = Object.create(null)

使用场景2:继承

let p1 = {'name':'jack'}
let p2 = Object.create(p1)
p2.name = 'rose'
console.log(p1.name)//jack
console.log(p2.name)//rose

组合继承

最常用的继承

核心是使用Parent.call(this.name)继承属性,Child.prototype = new Parent()继承方法

function Parent(name) {
        this.name = name
    }

    Parent.prototype = {
        constructor: Parent,
        say: function() {
            console.log(this.name + '在说话');
        },
        sing: function() {
            console.log(this.name + '在唱歌')
        }
    }

    function Child(name, age) {
        //继承父类的属性
        Parent.call(this, name)
    }
        //改变子类的原型,来继承父类的函数
    Child.prototype = new Parent()
    
    let child = new Child('jack')
    child.say()//jack在说话

优点:这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数

缺点:在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

寄生组合继承

为了解决上面的问题,引入了寄生组合继承

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

这部分代码,现在还不太理解

function Parent(name) {
        this.name = name
    }
    Parent.prototype = {
        constructor: Parent,
        say: function() {
            console.log(this.name + '在说话');
        },
        sing: function() {
            console.log(this.name + '在唱歌')
        }
    }
    function Child(name, age) {
        //继承父类的属性
        Parent.call(this, name)
    }
    Child.prototype = Object.create(Parent.prototype, {
        constructor: {
            value: Child,
            enumerable: false,
            writable: true,
            configurable: true
        }
    })
    let child = new Child('jack')
    child.say()

class继承

js中并不存在class,class的本质是函数

class的核心是使用extends关键字继承哪个父类,constructor里写super(name),相当于Parent.call(this.name)

  class Parent {
        constructor(name) {
            this.name = name
        }
        say() {
            console.log(this.name + '在说话')
        }
        sing() {
            console.log(this.name + '在唱歌')
        }
    }
    class Child extends Parent {
        constructor(name, age) {
            super(name)
            this.age = age
        }
    }
    let child = new Child('jack', 25)
    child.sing()
    console.log(child instanceof Parent)//true