Es5 javascript实现继承的5种方式及对应缺点(超详细)

Avatar
admin

1.原型链

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

简单理解为:将父类构造函数的实例对象作为子类构造函数的原型对象,通过修改原型的方式实现继承另一个引用类型的属性和方法。

代码演示:

function Parent(name) {

this.name = name;

this.children = ["a", "b"];

}

function Child() {}

Child.prototype = new Parent();

Child.prototype.constructor = Child;

let child = new Child();

child.children.push("c");

console.log(child.children); // ['a', 'b', 'c']

let child1 = new Child();

console.log(child1.children); // ['a', 'b', 'c']

原型链继承的主要问题:

父类构造函数自身的属性会被所有子类的实例对象共享,通过其中一个修改,那么另外的所有子类实例对象看到的都是修改之后的属性

在创建Child的实例的时候,不能向Parent传参

2.借用构造函数

为解决原型中包含引用类型值所带来的问题,人们开始用一种叫做借用构造函数的技术来实现继承。这种技术的基本思想非常简单,即在子类构造函数内部调用超类构造函数(父类)。

代码演示:

function Parent(name) {

console.log(this)

this.name = name;

this.children = ["a", "b"];

this.getName = function () {

return this.name;

};

}

function Child(name) {

Parent.call(this, name);

}

let child1 = new Child("李四");

child1.children.push("c");

console.log(child1.name, "==", child1.children);

let child2 = new Child("章三");

child2.children.push("d");

console.log(child2.name, "==", child2.children);

借用构造函数优点

避免了引用类型的属性被所有实例共享 可以直接在Child中向Parent传参

借用构造函数缺点

方法都在构造函数中定义了,每次创建实例都会创建一遍方法,浪费资源,没有实现复用

3.组合继承

组合继承就是将原型链和借用构造函数的技术结合到一起,背后思想是使用原型链实现对方法的继承,通过借用构造函数实现对属性的继承。这样,既能够保证能够通过原型定义的方法实现函数复用,又能够保证每个实例有自己的属性。但组合继承也并不是完美实现继承的方式,因为这种方式在创建子类时会调用两次超类的构造函数。(代码演示中已经标注出来了)

代码演示:

function Parent(name) {

this.name = name;

this.children = ["a", "b"];

// this.getName = function () {

// return this.name;

// };

}

Parent.prototype.getName = function () {

return this.name;

};

function Child(name) {

Parent.call(this, name); // 第一次调用父类构造函数

}

Child.prototype = new Parent(); //第二次调用父类构造函数

Child.prototype.constructor = Child;

let child1 = new Child("李四");

child1.children.push("c");

console.log(child1.name, "==", child1.children);

let child2 = new Child("章三");

child2.children.push("d");

console.log(child2.name, "==", child2.children);

console.log(child2);

组合继承优点

组合继承既具有原型链继承能够复用函数的特性,又有借用构造函数方式能够保证每个子类实例能够拥有自己的属性以及向超类传参的特性组合继承缺点

父类构造函数的重复调用执行,会造成浪费,如果父类构造函数共有属性极多,会导致运行速度减慢。

因为要执行Child.prototype = new Parent();所以会执行一遍父类的构造函数,此时并没有传递参数,当赋值之后,所以会出现无用属性在子类的原型对象上

4.寄生组合式继承

组合继承是 JavaScript最常用的继承模式,其最大的问题是不管在什么情况下都会调用两次超类构造函数:一次是在创建子类原型时,一次是在子类型构造函数内部。子类型最终会包含超类的全部实例属性。

所谓寄生组合式继承,即通过构造函数来继承属性,通过原型链继承方法,背后的基本思路是:不必为了指定子类的原型而调用父类的构造函数,我们所需要的无非就是父类原型的一个副本而已。

代码演示:

function inherit(subClass, superClass) {

// subClass 子类

// superClass 父类

// 复制父类原型

const parentPrototype = Object.create(superClass.prototype);

// 赋值给子类

subClass.prototype = parentPrototype;

// 子类 constructor 指回 subClass

subClass.prototype.constructor = subClass;

}

inherit方法接收两个参数子类构造函数和父类构造函数,通过inherit() 方法复制了父类的原型,赋值给子类,目的就是为了少调用一次超类的构造函数,从而避免在子类构造函数的原型对象上出现无用的属性

完整的继承示例:

function Parent(name) {

this.selfName = name;

this.children = ["a", "b"];

}

Parent.prototype.getName = function () {

return this.selfName;

};

function Child(name) {

Parent.call(this, name); // 为了继承父类的属性

}

function inherit(subClass, superClass) {

// subClass 子类

// superClass 父类

// 复制父类原型

const parentPrototype = Object.create(superClass.prototype);

// 改变constructor 指回 subClass

parentPrototype.constructor = subClass;

// 赋值给子类

subClass.prototype = parentPrototype;

}

inherit(Child, Parent);

let child = new Child("张三");

child.children.push("c");

console.log(child.selfName, "===", child.children);

// 张三 === (3)['a', 'b', 'c']

let child1 = new Child("李四");

child1.children.push("d");

console.log(child1.selfName, "===", child1.children);

// 李四 === (3)['a', 'b', 'd']

console.log(child1);

输出结果:

寄生组合继承的高效率在于它只调用了一次超类构造函数,同时还能够保持原型链不变,能够正常使用 instanceof 和 isPrototypeOf() 寄生组合继承被普遍认为是引用类型最理想的继承方式

5.增强型寄生组合继承

虽然寄生组合式继承能够很完美地实现继承,但也不是没有缺点。inherit() 方法中复制了父类的原型,赋值给子类构造函数的原型对象,假如子类原型上有自定的方法,直接进行赋值的话,会覆盖掉子类原型上有自定的方法,也会被覆盖。

因此可以通过Object.defineProperty的方式,将子类原型上原本定义的属性或方法添加到复制的原型对象上,如此,既可以保留子类的原型对象的完整性,又能够复制父类原型。

代码演示:

function Parent(name) {

this.selfName = name;

this.children = ["a", "b"];

}

Parent.prototype.getName = function () {

return this.selfName;

};

function Child(name) {

Parent.call(this, name); // 为了继承父类的属性

}

Child.prototype.getChild = function () {

return "child";

};

function inherit(subClass, superClass) {

// subClass 子类

// superClass 父类

// 复制父类原型

const parentPrototype = Object.create(superClass.prototype);

// 改变constructor 指回 subClass

parentPrototype.constructor = subClass;

//for...in语句迭代一个对象的所有可枚举字符串属性(除 Symbol 以外)包括继承的可枚举属性。

for (let key in subClass.prototype) {

Object.defineProperty(parentPrototype, key, {

value: subClass.prototype[key],

});

}

// 赋值给子类

subClass.prototype = parentPrototype;

//原来的写法直接赋值 subClass.prototype = parentPrototype;

}

inherit(Child, Parent);

let child = new Child("张三");

child.children.push("c");

console.log(child.selfName, "===", child.children);

// 张三 === (3)['a', 'b', 'c']

let child1 = new Child("李四");

child1.children.push("d");

console.log(child1.selfName, "===", child1.children);

// 李四 === (3)['a', 'b', 'd']

console.log(child1);

输出结果:

6.tips

isPrototypeOf() 与 instanceof 运算符不同。

在表达式 object instanceof AFunction 中,会检查 object 的原型链是否与 AFunction.prototype 匹配,而不是与 AFunction本身匹配。

Object.prototype.isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链中。

Object.defineProperty()

该静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。允许精确地添加或修改对象上的属性。

默认情况下,使用 Object.defineProperty() 添加的属性是不可写、不可枚举和不可配置的。