面向对象 面向你(二)

464 阅读8分钟

这一篇来聊聊 JavaScript 中的继承

继承 是面向对象语言中一个最为人津津乐道的概念。许多面向对象语言都支持两种继承方式:接口继承实现继承接口继承 只继承方法签名,而 实现继承 则继承实际的方法。因为在 JavaScript 中,函数没有签名(签名可以用来实现类型检查、函数重载、接口等等,而这些东西 js 没有),所以无法实现接口继承,只支持实现继承,且主要是依靠原型链来实现。

原型链

将原型链作为实现继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

我们先简单看下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

构造函数:拥有一个原型对象;

原型对象:包含一个指向构造函数的指针;

实例:包含一个指向原型对象的内部指针。

那么,假如我们让原型对象等于另一个类型的实例,会发生什么呢?很明显,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例 😂,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条 ⛓️,这就是所谓原型链的基本概念。

实现原型链有一种基本模式,大致如下:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function () {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

// 继承了 SuperType;
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue()); // true;

上面代码实现了 SubType 继承 SuperType,继承的方式是通过创建 SuperType 的实例,并将该实例赋给 SubType.prototype 实现的。

实现的本质是重写原型对象,代之以一个新类型的实例。

换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。

这个例子里的实例及构造函数和原型之间的关系如下图所示:

另外值得注意的是,instance.constructor 现在指向的是 SuperType,这是因为 SubType 的原型指向了另一个对象 —— SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType。

通过实现原型链,本质上扩展了上一篇介绍的原型搜索机制。

  • 确定原型和实例的关系

    可以通过两种方法来确定原型和实例的关系:instanceof 操作符isPrototypeOf() 方法

    alert(instacne instanceof SuperType); // true;
    alert(instance instanceof SubType); // true;
    
    alert(SuperType.prototype.isPrototypeOf(instance)); // true;
    alert(SubType.prototype.isPrototypeOf(instance)); // true;
    
  • 莫用对象字面量创建原型方法

    因为这样会重写原型链,有代码为证 😁:

    function SuperType() {
        this.property = true;
    }
    
    SuperType.prototype.getSuperValue = function () {
        return this.property;
    };
    
    function SubType() {
        this.subproperty = false;
    }
    
    // 继承了 SuperType;
    SubType.prototype = new SuperType();
    
    // 使用字面量添加新方法,会导致上一行代码无效;
    SubType.prototype = {
        getSubValue: function () {
            return this.subproperty;
        }
    };
    
    var instacne = new SubType();
    
    alert(instance.getSuperValue()); // error!
    

    之所以报错是因为现在的原型包含的是一个 Object 的实例,而非 SuperType 的实例,因而我们设想中的原型链已经被切断 —— SubType 和 SuperType 之间没有关系了。

  • 原型链的问题

    原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。

    最主要的问题来自包含引用类型值的原型。

    在上一篇里说过,包含引用类型值的原型属性会被所有实例共享;( 这也是为什么要在构造函数中,而不是在原型对象中定义属性的原因 )

    再通过原型来实现继承时,原型实际上会变成另一个类型的实例,so 原先的实例属性也就顺理成章地变成了现在的原型属性了:

    function SuperType() {
        this.colors = ['red', 'green', 'blue'];
    }
    
    function SubType() {}
    
    // 继承了 SuperType;
    SubType.prototype = new SuperType();
    
    var instance1 = new SubType();
    var instance2 = new SubType();
    
    instance1.colors.push('orange');
    
    alert(instance1.colors); // 'red, green, blue, orange';
    alert(instance2.colors); // 'red, green, blue, orange';
    

    结果 SubType 的所有实例都会共享这一个 colors 属性。

    原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。( 实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数 ),所以实际中很少会单独使用原型链。

借用构造函数

为解决原型中包含引用类型值所带来的问题,出现一种叫 借用构造函数 的技术,也称做 伪造对象经典继承

这种技术的基本思想是 在子类型构造函数的内部调用父类型构造函数 ,别忘了函数只不过是在特定环境中执行代码的对象,因为可以通过使用 apply()call() 方法在新创建的对象上执行构造函数:

function SuperType() {
    this.colors = ['red', 'green', 'blue'];
}

function SubType() {
    // 继承了 SuperType;
    SuperType.call(this);
}

var instance1 = new SubType();
var instance2 = new SubType();

instance1.colors.push('orange');

alert(instance1.colors); // 'red, green, blue, orange';
alert(instance2.colors); // 'red, green, blue';

通过使用 call () ( 或 apply () 方法也可以 ),我们实际上是在新创建的 SubType 实例的环境下调用了 SuperType 构造函数,这样一来,就会在新 SubType 对象上执行 SuperType () 函数中定义的所有对象初始化代码,然后 SubType 的每个实例就都会有自己的 colors 属性的副本了。

  • 传递参数

    相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向父类型构造函数 传递参数

    function SuperType(name) {
        this.name = name;
    }
    
    function SubType() {
        // 继承 SuperType 的同时还传递了参数;
        SuperType.call(this, 'Fly_001');
        // 实例属性;
        this.age = 22;
    }
    
    var instance = new SubType();
    
    alert(instance.name); // 'Fly_001';
    alert(instance.age); // 22;
    
  • 借用构造函数的问题

    构造函数的问题在于 —— 方法都在构造函数中定义,因此函数复用也就无从谈起。

    而且,在父类型的原型中定义的方法,对子类型而言也是不可见的。

    so。。。借用构造函数的技术也是很少单独使用的。

    所以 组合继承 即将闪亮登场 ✨。

组合继承

组合继承,也称为 伪经典继承 。指的是将原型链和借用构造函数的技术组合到一块,使用原型链实现对原型属性和方法对继承、而通过借用构造函数来实现对实例属性对继承,从而发挥二者之长的一种继承模式。

来看下面一个栗子 🌰 :

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    // 继承属性;
    SuperType.call(this, name);
    
    this.age = age;
}

// 继承方法;
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = funciton() {
    alert(this.age);
};

var instance1 = new SubType('Fly_001', 22);
var instance2 = new SubType('juejin', 24);

instance1.colors.push('orange');

alert(instance1.colors); 'red, green, blue, orange';
alert(instance2.colors); 'red, green, blue';

instance1.sayName(); // 'Fly_001';
instance2.sayAge(); // 24;

组合继承避免了原型链和借用构造函数对缺陷,融合了它们的优点,成为 JavaScript 中最常用对继承模式,而且,instanceof() 和 isPrototypeOf() 也能够用于识别基于组合继承创建的对象。

寄生组合式继承

上面说过 👆,组合继承是 JavaScript 最常用的继承模式;

but,它也有自己的不足。

组合继承最大的问题就是无论在什么情况下,都会调用两次父类型构造函数:

一次是在创建子类型原型的时候;

一次是在子类型构造函数内部。

再来看下组合继承的模式:

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name); // 第二次调用 SuperType();
    
    this.age = age;
}

SubType.prototype = new SuperType(); // 第一次调用 SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = funciton() {
    alert(this.age);
};

在上述代码里,会有两组 name 和 colors 属性:一组在实例上,一组在 SubType 原型中。 这就是调用两次 SuperType 构造函数的结果。

好在我们有解决问题的方法 —— 铛铛铛铛 ~ 寄生组合式继承 应运而生。

所谓 寄生组合式继承 ,就是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

其基本思路是:不必为了指定子类型的原型而调用父类型的构造函数,只需父类型原型的一个副本就够了。

寄生组合式继承 的基本模式如下:

function inheritPrototype(subType, superType) {
    var prototype = onject(superType.prototype); // 创建对象;
    prototype.constructor = subType; // 增强对象;
    subType.prototype = prototype; // 制定对象;
}
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue'];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this. name);
    
    this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = funciton() {
    alert(this.age);
};

这个栗子 🌰 的高效率在于它只调用了一次 SuperType 构造函数,并避免了在 SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变,因此还能正常地使用 instanceof()isPrototypeOf() 方法。

关于 JavaScript 的继承就讲到这里, 如有不对,还望指出

觉得不错就点个喜欢 ❤️ 再走呗~