【JS核心概念】继承

一、原型链继承

1.1 实现

基本思想:重写子类的原型对象,让原型对象等于父类的实例

    function Animal(color) {
        this.kind = ['cat', 'dog'];
        this.color = color;
    }
    Animal.prototype.getKind = function () {
        return this.kind;
    }
    
    function Cat(name) {
        this.name = name;
    }
    
    // 让Cat.prototype等于Animal的实例
    // 传入超类Animal的构造函数中的参数,被Cat的所有实例共享
    Cat.prototype = new Animal('black');
    Cat.prototype.getColor = function () {
        return this.color;
    }
    
    const cat1 = new Cat('wangcai');
    let kind1 = cat1.getKind();
    console.log(kind1); // [ 'cat', 'dog' ]
    console.log(cat1.getColor()); // black
    
    const cat2 = new Cat('xiaohei');
    let kind2 = cat2.getKind();
    console.log(kind2); // [ 'cat', 'dog' ]
    console.log(cat2.getColor()); // black
    

注意:

  • Cat.prototype.constructor指向Animal,因为 Cat.prototype = new Animal('black') 重写了Cat.prototype为Animal的一个实例,实例对象中没有constructor属性,因此当访问Cat.prototype.constructor时,实际上访问的是其父类型原型上的constructor
console.log(Cat.prototype.constructor === Animal); // true
console.log(Cat.prototype.constructor === Cat); // false
  • 使用原型链继承时,如果重写原型链之前在原型链上添加了属性或者方法,重写原型链后将无法访问到刚刚添加的属性或方法
    // 先在原型上添加了方法
    Cat.prototype.getColor = function () {
        return this.color;
    }
    // 然后重新原型对象
    Cat.prototype = new Animal('black');
    
    const cat1 = new Cat('wangcai');
    // 调用原型上的方法报错
    cat1.getColor(); // TypeError: cat1.getColor is not a function
1.2 判断原型与实例之间的关系
  • instanceof:只要是派生出该实例的原型链中出现过的构造函数,都会返回true
    // 使用instanceof操作符判断原型与实例之间的关系:
    // 只要是派生出该实例的原型链中出现过的构造函数,都会返回true
    console.log(cat1 instanceof Cat); // true
    console.log(cat1 instanceof Animal); // true
    console.log(cat1 instanceof Object); // true
    
  • isPrototypeOf:只要是派生出该实例的原型链中出现过的原型,都会返回true
    // 使用isPrototypeOf方法判断原型与实例中的关系:
    // 只要是派生出该实例的原型链中出现过的原型,都会返回true
    console.log(Cat.prototype.isPrototypeOf(cat2));
    console.log(Animal.prototype.isPrototypeOf(cat2));
    console.log(Object.prototype.isPrototypeOf(cat2));
1.3 缺点
  • 父类的实例属性会被子类的所有实例共享,当通过子类型修改父类的实例属性(值为引用类型)时,会影响到子类型的其他实例
    // 通过cat1修改kind属性的值会影响到Cat的其他实例
    kind1.push('pig');
    console.log(kind1); // [ 'cat', 'dog', 'pig' ]
    console.log(kind2); // [ 'cat', 'dog', 'pig' ]
    
    // 在cat1上添加了一个同名属性kind,不会影响到其他实例
    cat1.kind = 'cat';
    console.log(cat1.getKind()); // cat
    console.log(cat2.getKind()); // [ 'cat', 'dog', 'pig' ]

分析:kind是Animal类型的实例属性,当让Cat.prototype等于Animal类型的实例时,kind会变成Cat.prototype中的一个属性,Cat类型的所有实例都会共享Cat.prototype上的kind属性,因此Cat类型的某个实例如果修改(是修改,不是添加)了kind属性的值,会影响到Cat类型其他实例的这个属性。

二、借用构造函数继承

2.1 实现

基本思想:在子类型的构造函数内部调用父类型的构造函数

借用构造函数继承解决了原型链继承的带来的一些问题:

    function Animal(color) {
        this.kind = ['cat', 'dog'];
        this.color = color;
    }
    Animal.prototype.getKind = function () {
        return this.kind;
    }
    
    function Cat(name, color) {
        this.name = name;
        // 调用父类构造函数,实现继承
        Animal.call(this, color)
    }
    
    Cat.prototype.getColor = function () {
        return this.color;
    }
    
    const cat1 = new Cat('wangcai', 'black');
    const cat2 = new Cat('xiaohei', 'white');
    
    // 修改kind属性,不会影响到其他实例
    cat1.kind.push('pig');
    console.log(cat1.kind); // [ 'cat', 'dog', 'pig' ]
    console.log(cat2.kind); // [ 'cat', 'dog' ]
2.2 缺点
  • 只能继承父类的实例属性和方法,不能继承原型属性和方法
 console.log(cat1.getKind()); // TypeError: cat1.getKind is not a function
  • 无法实现复用,子类型的每个实例都包含父类函数的副本,影响性能

三、组合继承(最常用的)

3.1 实现

基本思想:通过原型链实现对父类的原型属性和方法的继承,通过借用构造函数实现对父类的实例属性和方法的继承

    function Animal(color) {
        this.kind = ['cat', 'dog'];
        this.color = color;
    }
    Animal.prototype.getKind = function () {
        return this.kind;
    }
    
    function Cat(name, color) {
        this.name = name;
        // 调用父类构造函数,实现继承(第二次调用)
        Animal.call(this, color)
    }
    
    // 第一次调用Animal(),使Cat.prototype为Animal类型的实例对象
    Cat.prototype = new Animal();
    // 在Cat.prototype上添加constructor属性,屏蔽了其原型对象Animal.prototype上的constructor属性,能更准确的判断出Cat实例的类型
    Cat.prototype.constructor = Cat;
    Cat.prototype.getColor = function () {
        return this.color;
    }
    
    const cat1 = new Cat('wangcai', 'black');
    const cat2 = new Cat('xiaohei', 'white');
    
    // 修改kind属性,不会影响到其他实例
    cat1.kind.push('pig');
    console.log(cat1.kind); // [ 'cat', 'dog', 'pig' ]
    console.log(cat2.kind); // [ 'cat', 'dog' ]
    
    // 可以访问父类原型上的方法
    console.log(cat1.getKind()); // [ 'cat', 'dog', 'pig' ]
    console.log(cat2.getKind()); // [ 'cat', 'dog' ]
    
    // 子类的实例可以直接通过constructor属性判断其类型
    console.log(cat1.constructor === Cat); // true
    console.log(cat2.constructor === Cat); // true

分析:

  • 第一次调用Animal()时,在Cat.prototype上添加了kind、color属性;
  • 第二次调用Animal()时,在Cat实例上添加了kind、color属性;
  • 实例对象cat1/cat2上的kind、color属性屏蔽了原型上的kind、color属性。
3.2 缺点
  • 使用子类创建实例时,实例与原型对象上会有一组同名属性或方法

四、原型式继承

4.1 实现

基本思想:借助原型可以基于已有对象创建新对象,同时还不必创建自定义类型。

    function object(o) {
        // 创建一个临时性的构造函数F
        function F() {}
        // 将传入的对象作为这个F类型的原型
        F.prototype = o;
        // 返回一个F类型的实例
        return new F();
    }
    
    const person = {
        name: 'Lily',
        interests: ['music', 'book']
    }
    
    const person1 = object(person);
    const person2 = object(person);
    
    // person1、person2的类型与person一样为Object,不需要创建一个新的自定义类型
    console.log(person1.constructor); // [Function: Object]
    console.log(person2.constructor); // [Function: Object]

注意:

  • object方法对传入的对象进行了一次浅复制,将构造函数F的原型指向传入的对象,因此实例对象与传入的对象共享属性。
    console.log(person1.interests); // [ 'music', 'book' ]
    console.log(person2.interests); // [ 'music', 'book' ]
    
    // 通过一个实例对象修改某个引用类型的属性的值后,会影响其他实例,以及传入的基本对象
    person1.interests.push('play');
    console.log(person1.interests); // [ 'music', 'book', 'play' ]
    console.log(person2.interests); // [ 'music', 'book', 'play' ]
    console.log(person.interests); // [ 'music', 'book', 'play' ]
  • ES5中的Object.create()方法规范了原型式继承。这个方法接收两个参数,第一个参数表示的是用作新对象原型的对象,第二个参数是一个为新对象定义额外属性的对象,这个对象中的每个属性都是通过自己的描述符定义的。用这种方法定义的属性会覆盖原型对象上的同名属性。
    let person = {
    name: 'Lily',
    interests: ['music', 'book'],
    friends: ['Lucy', 'Jack']
    }
    
    let newPerson = Object.create(person, {
        name: {
            value: 'Lucy'
        },
        age: {
            value: 18
        },
        friends: {
            value: ['Lily', 'Jack']
        }
    })
    
    // 属性都在原型对象上
    console.log(newPerson); // {}
    // 覆盖了person上的name属性值
    console.log(newPerson.name); // Lucy
    console.log(newPerson.age); // 18
    
    console.log(newPerson.interests); // [ 'music', 'book' ]
    
    // 与person共享interests属性
    newPerson.interests.push('sing');
    console.log(newPerson.interests); // [ 'music', 'book', 'sing' ]
    console.log(person.interests); // [ 'music', 'book', 'sing' ]
    
    // 覆盖了person上的frineds,修改frineds不会影响person上的同名属性
    console.log(newPerson.friends); // [ 'Lily', 'Jack' ]
    newPerson.friends.push('Mary');
    console.log(newPerson.friends); // [ 'Lily', 'Jack', 'Mary' ]
    console.log(person.friends); // [ 'Lucy', 'Jack' ]

Object.create()方法的实现原理:

    // 模拟create的实现
    function object(o, attr) {
        function F() {}
        F.prototype = o;
        let f = new F();
        if (attr && typeof attr === 'object') {
            Object.defineProperties(f, attr)
        }
    
        return f;
    }
    
    const newPerson = object(person, {
        name: {
            value: 'Lucy'
        },
        age: {
            value: 18
        },
        friends: {
            value: ['Lily', 'Jack']
        }
    })
    // 属性都在原型对象上
    console.log(newPerson); // {}
    // 覆盖了person上的name属性值
    console.log(newPerson.name); // Lucy
    console.log(newPerson.age); // 18
    
    console.log(newPerson.interests); // [ 'music', 'book' ]
    
    // 与person共享interests属性
    newPerson.interests.push('sing');
    console.log(newPerson.interests); // [ 'music', 'book', 'sing' ]
    console.log(person.interests); // [ 'music', 'book', 'sing' ]
    
    // 覆盖了person上的frineds,修改frineds不会影响person上的同名属性
    console.log(newPerson.friends); // [ 'Lily', 'Jack' ]
    newPerson.friends.push('Mary');
    console.log(newPerson.friends); // [ 'Lily', 'Jack', 'Mary' ]
    console.log(person.friends); // [ 'Lucy', 'Jack' ]
4.2 缺点
  • 当基本对象属性的值为引用类型时,会被所有实例对象共享,就跟使用原型模式一样。
4.3 使用场景

当仅仅想让一个对象与另一个对象保存类似,而不想创建新的类型时,可以使用该模式

五、寄生式继承

5.1 实现原理:寄生式继承与原型式继承的思路很相似,区别在于寄生式继承增强了对象
    function object(o) {
        function F() {}
        F.prototype = o;
        return new F()
    }
    
    function createObject(original) {
        // 调用object创建一个新对象
        let clone = object(original);
        // 为新对象添加方法
        clone.sayName = function () {
            console.log(this.name)
        }
        return clone;
    }
    
    let person = {
        name: 'Lily',
        interests: ['music', 'book'],
        friends: ['Lucy', 'Jack']
    }
    
    let otherPerson = createObject(person);
    otherPerson.sayName(); // Lily
    
    // 每个实例上的sayName方法指向不同的引用,虽然功能一样
    let otherPerson2 = createObject(person);
    console.log(otherPerson.sayName === otherPerson2.sayName);
5.2 缺点
  • 与原型式继承一样,如果基本对象属性的值为引用类型,那么基于该对象创建出来的对象将共享这个属性;
  • 使用组合式继承为对象添加函数时,无法实现函数复用,降低效率。

六、寄生组合式继承

6.1 基本思想:通过借用构造函数来继承父类的实例属性,通过寄生式继承来继承父类的原型属性和方法
    // 原型式:将一个空对象的原型指向一个已有对象,最后返回这个空对象
    function object(base) {
        function F() {}
        F.prototype = base;
        return new F;
    }
    
    // 组合式:对原型式中的返回的对象进行增强
    // 利用组合式继承父类的原型
    function createObject(subType, superType) {
        // 创建对象:返回父类原型的副本
        let prototype = object(superType.prototype);
        // 增强对象:指定子类类型
        prototype.constructor = subType;
        // 继承父类的原型
        subType.prototype = prototype;
    }
    
    function Animal(kind) {
        this.kind = kind;
    }
    Animal.prototype.getKind = function () {
        console.log(this.kind);
    }
    
    function Cat(name) {
        this.name = name;
        // 借用构造函数:继承父类的实例属性
        Animal.call(this, 'cat');
    }
    
    // 继承父类的原型
    createObject(Cat, Animal);
    Cat.prototype.getName = function () {
        console.log(this.name);
    }
    
    const cat1 = new Cat('xiaohei');
    cat1.getName(); // xiaohei
    cat1.getKind(); // cat
    
    console.log(cat1 instanceof Cat); // true
    console.log(Cat.prototype.isPrototypeOf(cat1)); // true
    
    console.log(cat1 instanceof Animal); // true
    console.log(Animal.prototype.isPrototypeOf(cat1)); // true

寄生组合式继承比组合式继承效率高,因为寄生组合式继承只调用了一次父类的构造函数,并且也因此避免了在子类原型上创建不必要的、多余的属性,与此同时原型链还能保持不变。因此寄生组合式继承是引用类型最理想的继承范式。





posted @ 2020-01-16 15:13  嘉平十五  阅读(163)  评论(0编辑  收藏  举报