JavaScript继承方法详解

2020-04-25
8分钟阅读时长

new 操作的实现原理

以 SuperType 构造函数和 obj 实例为例。

首先明确 new 操作符的预期结果:

  • obj 具有 SuperType 所有的实例属性、方法和原型属性、方法
  • obj.constructor === SuperType, 返回 true
  • obj.__proto__ == SuperType.prototype, 返回 true
  • obj 上的原型方法与 SuperType 原型上的同一方法在内存上应该一致
  • new 操作最后返回了一个对象
  • obj instanceof SuperType, 返回 true

先给出 SuperType 构造函数的定义:

function SuperType() {
    this.prop = true;
    this.func = function () {};
}

SuperType.prototype.protofunc = function () {};

验证上述几个 new 操作符的预期结果是否正确:

var obj1 = new SuperType();

console.log(obj1.constructor === SuperType); // true
console.log(obj1.__proto__ == SuperType.prototype); // true
console.log(obj1.protofunc === SuperType.prototype.protofunc); // true
console.log(obj1 instanceof SuperType); // true

返回结果都为 true,说明上述预期是正确的。

接下来,开始复现 new 操作符的实现过程。封装在函数newinstance(Type)中,其中,Type 为对象类型,即相当于SuperType

function newinstance(Type) {
    // 首先创建一个对象实例,该对象包括一个 __proto__ 属性,需要指向 Type.prototype
    var o = {
        __proto__: Type.prototype,
    };

    // 接着,对象实例 o 需要具有 Type 的所有实例属性和原型属性
    // 即,在o对象实例上运行 Type 的构造函数,初始化 Type 的那些属性和方法
    Type.apply(o);

    // 最后,需要返回这个对象实例
    return o;
}

对该复现进行验证,验证是否满足上述预期结果。

// 首先定义函数printAllProps(),用于输出所有可枚举的实例属性、原型属性
// 不用console.log输出的原因见获得对象属性的方法的博文
function printAllProps(obj) {
    let array = [];
    for (let prop in obj) {
        array.push(prop);
        // 如果要输出属性值,使用:
        // array.push(prop + ':' + obj[prop])
    }
    console.log(array);
}
var obj = newinstance(SuperType);
printAllProps(obj);

输出['prop', 'func', 'protofunc'],说明已经满足具有所有实例属性、原型属性的预期。

console.log(obj.constructor === SuperType); // true
console.log(obj.__proto__ == SuperType.prototype); // true
console.log(obj.protofunc === SuperType.prototype.protofunc); // true
console.log(obj instanceof SuperType); // true

全部输出true,说明这一步也验证正确。

至此,new 操作符的重现已经完成。

使用原型链进行继承

继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。

本质是重写原型对象,用另一个类型的实例所代替。

function SuperType() {
    this.prop = true;
    this.func = function () {};
}

SuperType.prototype.protofunc = function () {};

function SubType() {
    this.subprop = true;
    this.subfunc = function () {};
}

SubType.prototype = new SuperType(); // !important

var subobj = new SubType();

上述代码中,SubType.prototype = new SuperType() 给 SubType 换了一个新的原型,因为是直接重写的。SubType.prototype指向的是这个新new出来的对象。因此最终结果为,subobj 中有一个指向SubType.prototype的指针,SubType.prototype中有一个指向SuperType.prototype的指针。

使用原型链实现继承时,在继承类型中,不能使用对象字面量创建原型方法或属性。

例如,不能使用以下字面量添加新方法。本质问题是错误地将原型对象重写替换成了另一个对象字面量。

SubType.prototype = new SuperType();

// 错误方法
// SubType.prototype = {
//     newfunc: function () {},
// };

// 正确方法
SubType.prototype.newfunc = function () {};

instanceof 操作符、isPrototypeOf() 方法

所有引用类型都默认继承了 Object。

instanceof

只要这个构造函数在实例的原型链中出现过,就返回 true。

console.log(subobj instanceof SubType); // true
console.log(subobj instanceof SuperType); // true
console.log(subobj instanceof Object); // true

isPrototypeOf()

只要是在原型链中出现过的原型,都可以算作该原型链所派生的实例的原型,返回 true。

console.log(SubType.prototype.isPrototypeOf(subobj)); // true
console.log(SuperType.prototype.isPrototypeOf(subobj)); // true
console.log(Object.prototype.isPrototypeOf(subobj)); // true

问题

问题一:对于包含引用类型值的子类型原型,即超类型实例属性、原型属性中包含引用类型的时候,子类型对象公用这些引用类型内存,对引用类型值的更改会反映在所有对象上。

问题二:创建子类型对象时,不能在不影响所有对象实例的情况下向超类型构造函数传递参数。

借用构造函数

也称伪造对象继承经典继承

本质是,在子类型构造函数内部调用超类型构造函数,同时使用call()apply()方法在新创建的子类型对象上执行构造函数。

可以解决使用原型链实现继承的问题一和问题二。

function SuperType(supername) {
    this.name = supername;
    this.arrays = ["1", "2"];
    this.func = function () {};
}

function SubType(subname, supername) {
    SuperType.call(this, supername); // !important
    // 为了确保SuperType构造函数不会重写子类型的属性
    // 应先调用超类型构造函数,再进行子类型属性定义
    this.subname = subname;
}

var subobj1 = new SubType("sub", "super");

subobj1.arrays.push("3");

var subobj2 = new SubType("sub", "super");
var superobj = new SuperType("super");

使用可以输出可枚举属性名、属性值的printAllProps()函数,验证对引用类型值的更改是否影响到了所有对象实例。

printAllProps(subobj1);
// [ 'subname:sub', 'name:super', 'arrays:1,2,3', 'func:function () {}' ]
printAllProps(subobj2);
// [ 'subname:sub', 'name:super', 'arrays:1,2', 'func:function () {}' ]
printAllProps(superobj);
// [ 'name:super', 'arrays:1,2', 'func:function () {}']

原因: 使用call()方法,即在新要创建的子类型对象上执行了SuperType()函数中定义的对象初始化代码,每个SubType类型实例都具有自己的arrays副本。

但这同时也带来了借用构造函数继承的问题。

问题

  • 方法都在构造函数中定义,无法进行函数复用。
console.log(subobj1.func === subobj2.func); // false
  • 在超类型原型中定义的方法,对子类型是不可见的。

如果给SuperType类型添加原型函数protofunc()如下:

SuperType.prototype.protofunc = function () {};

SubType对象实例subobj1subobj2中将不会包含这个超类型的原型方法,printAllProps()结果将如下:

printAllProps(subobj1); // [ 'subname:sub', 'name:super', 'arrays:1,2,3', 'func:function () {}' ]
printAllProps(subobj2); // [ 'subname:sub', 'name:super', 'arrays:1,2', 'func:function () {}' ]
printAllProps(superobj); // [ 'name:super', 'arrays:1,2', 'func:function () {}', 'protofunc:function () {}']

可以看到,只有superobj中包含了该方法。原因:注意上一段中的加粗字体:执行了SuperType()函数中定义的对象初始化代码——原型上的属性方法不是定义在SuperType()构造函数中的,构造函数中仅包含实例属性方法,因此子类型SubType的实例中将只会初始化这些实例属性方法。

导致的结果是,所有类型都只能使用构造函数模式。因此借用构造函数很少单独使用。

组合继承

也称伪经典继承, 将原型链和借用构造函数组合在一起,融合了二者的优点:

  • 使用借用构造函数实现对实例属性的继承
  • 使用原型链实现对原型属性的继承
// 定义超类型的实例属性方法
function SuperType(supername) {
    this.name = supername;
    this.arrays = ["1", "2"];
}

// 定义超类型的原型属性方法
SuperType.prototype.protofunc = function () {};
SuperType.prototype.protonames = ["a", "b"];

// 子类型开始继承
// 1. 使用借用构造函数实现对实例属性的继承
function SubType(subname, supername) {
    SuperType.call(this, supername); // !important

    this.subname = subname;
}

// 2. 使用原型链实现对原型属性的继承
SubType.prototype = new SuperType(); // !important
SubType.prototype.constructor = SubType; // !important

// 创建子类型对象实例
var subobj1 = new SubType("sub", "super");
var subobj2 = new SubType("sub", "super");

虽然组合继承目前被使用得很多,但仍有不足。

不足:会调用两次超类型构造函数

无论在什么情况下,使用组合继承都会调用两次超类型构造函数:第一次,在重写SubType的原型时;第二次,在子类型SubType构造函数内部。如下:

function SuperType(supername) {
    this.name = supername;
}

function SubType(subname, supername) {
    SuperType.call(this, supername); // 第二次调用

    this.subname = subname;
}

SubType.prototype = new SuperType(); // 第一次调用
SubType.prototype.constructor = SubType;

var subobj1 = new SubType("sub", "super"); // 第二次调用入口

实际过程如下:

  • 第一次调用时,SubType.prototype中会存在属性name,来自于SuperType的实例属性。
  • 第二次调用时,子类型对象实例上会存在实例属性namesubname,由于实例属性会屏蔽原型属性中的同名属性,真实使用的是实例属性name,而原型属性name会被屏蔽。

这也就是组合继承中效率较低的地方。解决方法见寄生组合式继承

原型式继承

适用情况:只是想让一个对象与另一个对象保持类似的情况下,添加新的属性和方法。

不涉及到类型(function),从头到尾使用的都是对象(var)。

// 注意这是个对象,而不是类型构造函数
// 使用字面量对象、对象实例都可以
var obj = {
    name: "objname",
    arrays: ["1", "2"],
    func: function () {},
};

// Object.create()方法的两种参数形式
var anotherobj1 = Object.create(obj);
var anotherobj2 = Object.create(obj, {
    name: {
        enumerable: true,
        value: "newname",
    },
});

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

Object.create()方法有两种参数形式:

  • 第一个参数:一个作为新对象原型的对象

  • 第二个参数(可选):为新对象定义新属性的对象,形式与 Object.defineProperties()的第二个参数格式相同

特别注意第二个参数中,属性name的那四种数据属性,如果没有指定enumerable: true,在使用for..inprintAllProps时,属性不会被枚举到。

修改anotherobj1arrays的值,如下:

anotherobj1.arrays.push("3");

printAllProps(anotherobj1); // [ 'name:objname', 'arrays:1,2,3', 'func:function () {}' ]
printAllProps(anotherobj2); // [ 'name:newname', 'arrays:1,2,3', 'func:function () {}' ]

可以看到,使用原型式继承,同样也会存在使用原型链进行继承时的引用类型值的问题——包含引用类型值的属性始终会共享相应的值。

可通过以下代码验证:

console.log(anotherobj1.arrays === anotherobj2.arrays); // true
console.log(anotherobj1.func === anotherobj2.func); // true```

寄生式继承

适用情况:在主要考虑的是对象,而不是自定义类型或构造函数时。

function createAnother(original) {
    // 该Object()函数不是必须,任何返回一个对象的函数都可以
    var clone = Object(original);

    clone.newfunc = function () {};
    return clone;
}

var original = {
    name: "originalName",
};

var another = createAnother(original);

printAllProps(another); // [ 'name:originalName', 'newfunc:function () {}' ]

缺点:与构造函数继承类似地,函数不能复用。

寄生组合式继承

优点:高效率——弥补组合继承的不足,只调用一次超类型SuperType构造函数,避免了在子类型SubType的原型prototype上创建不必要的多余的属性和方法。

是目前引用类型最理想的继承范式。

本质:使用寄生式继承来继承超类型的原型(即只继承了超类型的原型属性,并没有在子类型原型上添加超类型实例属性),然后再将结果指定给子类型的原型。即,将组合继承中第一次调用超类型构造函数的地方进行了替换。

function SuperType(supername) {
    this.name = supername;
}

SuperType.prototype.protofunc = function () {};

function SubType(subname, supername) {
    this.subname = subname;

    SuperType.call(this, supername);
}

// 用以下方式替换组合继承中的第一次调用
// 即,替换掉 SubType.prototype = new SuperType()

// *************************************
function inheritPrototype(subType, superType) {
    var prototype = superType.prototype; // 创建原型对象
    prototype.constructor = subType; // 增强对象
    subType.prototype = prototype; // 指定原型对象
}

inheritPrototype(SubType, SuperType);
// *************************************

SubType.prototype.constructor = SubType;

var subobj = new SubType("sub", "super");

printAllProps(subobj); // [ 'subname:sub', 'name:super', 'protofunc:function () {}' ]

为创建的超类型原型副本prototype添加constructor属性的目的:弥补因重写而失去的默认的constructor属性,让prototype副本有正确的constructor属性指向。

Avatar
Ruiqi 有梦想,有追求,有吃的,有喝的,有玩的,有爱的