JavaScript继承详解

174 阅读5分钟

什么是面向对象?

传统意义上,面向对象有三大特性:封装,继承,多态
封装:是一种对象功能内聚的表现形式,使模块之间耦合度变低,更具有维护性。
继承:使子类能够继承父类,获得父类的部分属性和行为,使模块更具有复用性
多态:模块在复用性基础上更具有扩展性,使系统运行具有想象空间

继承

两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承,ECMAScript只支持实现实现继承,而且其实现继承主要是依靠原型链来实现的。

原型链继承

    function Parent() {
        this.parent_name = 'hello'
    }
    Parent.prototype.getParentName = function() {
        return this.parent_name
    }

    function Child() {
        this.child_name = 'world'
    }

    // 继承Parent
    Child.prototype = new Parent();

    Child.prototype.getChildName = function() {
        return this.child_name
    }

    // 不可使用字面量直接往原型中添加方法,这样会覆盖掉整个Child原型对象
    // Child.prototype = {
    //     getChildName: function() {
    //         return this.child_name
    //     }
    // }

    let instance = new Child();
    console.log(instance.getParentName()); // hello
    console.log(instance.getChildName()) //  world

    // 上述中继承是通过创建Parent实例,并将该实例赋给Child.prototype 实现的。实现的本质是重写原型对象,代之的是一个新类型的实例。
    // 注意:instance.constructor 现在指向的是Parent,因为此时Child的原型指向了另一个对象 -- Parent的原型,而这个原型对象的constructor属性指向Parent
    // 子类型如果需要覆盖超类型中的某个方法,或者添加超类型中不存在的某个方法,给原型添加方法的代码一定要在替换原型对象语句之后。

原型链继承的问题:

1、引用数据的类型,所有实例都会共享。

        function ParentType() {
            this.colors = ['red', 'blue'] // 引用数据类型
        }
        function ChildType() {

        }

        ChildType.prototype = new ParentType()

        let child1 = new ChildType();
        child1.colors.push('black');
        console.log(child1.colors) // ['red', 'blue', 'black']

        let child2 = new ChildType();
        console.log(child2.colors) // ['red', 'blue', 'black']

2、在创建子类的实例时,不能够向父构造函数中传递参数。(不能够在不影响所有对象实例的情况下,给父构造函数传递参数)

借用构造函数(经典继承)

    function Parent(name) {
        this.parent_name = name;
        this.colors = ['red', 'blue']
    }
    function Child(name) {
        // 继承Parent
        // Child的每个实例都会具有自己的colors的副本
        Parent.call(this, name);
        this.child_name = 'child-name'
    }

    let child1 = new Child('parent-name')
    child1.colors.push('black');
    console.log(child1.parent_name) // parent-name
    console.log(child1.colors); // ['red', 'blue', 'black']

    let child2 = new ChildType();
    console.log(child2.colors) // ['red', 'blue']

    // Parent.call(this); 通过call()或apply()方法,我们在新创建的Child实例的环境下调用了Parent构造函数,这样就会在Child对象上执行Parent函数中定义的所有对象初始化代码。
    // 相比较原型链继承,借用构造函数解决了不能够传递参数和引用数据在所有实例中共享的问题

借用构造函数(经典继承)问题:

1、方法都在构造函数中定义,函数的复用就不存在了。

组合继承

使用原型链实现对原型和属性的继承,使用借用构造函数实现对实例属性的继承。

    function Parent(name) {
        this.parent_name = name;
        this.colors = ['red', 'blue']
    }
    Parent.prototype.getParentName = function() {
        return this.parent_name
    }
    function Child(name) {
        Parent.call(this, name);
        this.child_name = 'child-name'
    }

    Child.prototype = new Parent();
    Child.prototype.constructor = Child;

    Child.prototype.getChildName = function() {
        return this.child_name
    }

    let child1 = new Child('parent-name');
    child1.colors.push('black');
    console.log(child1.getChildName()) // child-name
    console.log(child1.getParentName()) // parent-name
    console.log(child1.parent_name) // parent-name
    console.log(child1.colors); // ['red', 'blue', 'black']

    let child2 = new Child();
    console.log(child2.colors) // ['red', 'blue']

    // 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。instanceof也能够识别基于组合继承创建的对象

原型式继承

借助原型可以基于已有的对象创建新对象

    function createObject(o) {
        function Func() {}
        Func.prototype = o;

        return new Func()
    }

    // 在createObject函数内部,先创建了一个临时构造函数,然后将传入的对象作为构造函数的原型,最后返回这个临时类型的一个新实例。从本质上将createObject对传入其中的对象执行了一次浅复制

    let test = {
        name: '原型式',
        colors: ['red', 'blue']
    }
    let test1 = createObject(test);
    test1.name = '原型式1';
    test1.colors.push('black');

    let test2 = createObject(test);
    test2.name = '原型式2';
    test2.colors.push('yellow');

    console.log(test)
    // test = { name: '原型式', ['red', 'blue', 'black', 'yellow'] }

    // 在es5中 新增的Object.create(),在只传入一个参数的情况下和createObject()行为相同

原型式继承的问题

1、和原型链继承一样,包含引用数据类型的值始终都会共享

寄生式继承

    function createObject(o) {
        let clone = Object.create(o)
        clone.sayName = function() {
            return this.name
        }

        return clone;
    }

    let test = {
        name: '寄生式',
        colors: ['red', 'blue']
    }

    let person1 = createObject(test);
    person1.colors.push('black');
    console.log(person1.sayName())

    console.log(test)
    // test = { name: '寄生式', ['red', 'blue', 'black'] }

寄生式继承缺点

1、使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这点和经典继承类型

为什么需要使用寄生式继承:

在主要考虑对象不是自定义类型和构造函数的情况下。

寄生组合式继承

组合式继承模式,不论在什么情况下都会调用两次父类的构造函数。第一次是在创建子类型原型的时候,另一次是在子类型构造函数内部;

寄生组合式继承,通过构造函数来继承属性,通过原型链混成形式来继承方法。基本思路:不必为了指定子类型的原型对象而调用父类型的构造函数。

    function prototype(child, parent) {
        let _prototype = Object.create(parent.prototype)
        _prototype.constructor = child
        child.prototype = _prototype
    }

    function Parent(name) {
        this.parent_name = name;
        this.colors = ['red', 'blue']
    }
    Parent.prototype.getParentName = function() {
        return this.parent_name
    }
    function Child(name) {
        Parent.call(this, name);
        this.child_name = 'child-name'
    }

    prototype(Child, Parent);

    Child.prototype.getChildName = function() {
        return this.child_name
    }

    let child1 = new Child('parent-name');
    child1.colors.push('black');
    console.log(child1.getChildName()) // child-name
    console.log(child1.getParentName()) // parent-name
    console.log(child1.parent_name) // parent-name
    console.log(child1.colors); // ['red', 'blue', 'black']

    let child2 = new Child();
    console.log(child2.colors) // ['red', 'blue']