• 【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
    

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





  • 相关阅读:
    简单对拍
    搜索感想
    L1434滑雪
    记忆化搜索
    L3956棋盘
    USACO 数字三角形
    枚举顺序
    蓝桥计算
    用户态和内核态IO过程
    Mybatis的结果集中的Do要不要有setter
  • 原文地址:https://www.cnblogs.com/jiafifteen/p/12201328.html
Copyright © 2020-2023  润新知