最近在面试时有两次被问到JS的继承方式,然而只答出了原型链继承和借用构造函数来实现继承,是时候总结一番了(╥╯^╰╥)。
JS中主要有六种方式来实现继承,原型链实现继承、借用构造函数实现继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。下面介绍这几种方式:
1、 原型链继承
原型链继承基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法;即每一个构造函数都有一个prototype属性,指向原型对象,每个实例都有一个内部指针_proto_指向原型对象,让其子类构造函数的原型对象指向超类的实例,从而实现原型链继承。
function SuperType() {
this.property = true;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subProperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
return this.subProperty;
}
let instance1 = new SubType();
let instance2 = new SubType();
instance1.colors.push("black");
console.log(instance2);//["red", "blue", "green", "black"]
实现本质:重写子类构造函数的原型对象,使其原型对象指向超类的实例。
缺点:
-
若超类中有引用类型属性,由于所有属性会共享该属性,则会导致修改引用类型值后会导致其他实例也感知到变化,从而影响其他实例属性值。例如以上代码colors属性为引用类型值,当instance1改变colors的值时,导致instance2.colors也被改变。
-
不能在影响所有实例的情况下,向超类构造函数中传参。
在使用原型链继承时需注意以下几点:
-
给原型添加方法和属性需要在替换原型队形语句之后,否则重写原型对象会导致方法和属性不生效。
-
原型链继承实现中,不能通过对象字面量创建原型方法
那如何确定实例与原型之间的方法呢?
a、使用instanceof操作符来判断原型和实例之间的关系。
console.log(instance1 instanceof Object);//true
console.log(instance1 instanceof SuperType);//true
console.log(instance1 instanceof SubType);//true
b、 使用isPrototypeOf,只要实例的原型链中出现过的原型,则返回true
console.log(Object.prototype.isPrototypeOf(instance1));//true
2、 借用构造函数实现继承
为了解决原型链继承中解决原型中包含引用类型值带来的问题,开发人员开始使用一种借用构造函数来实现继承,即在子类型构造函数中调用超类型构造函数。代码如下:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
function SubType(name) {
SuperType.call(this, name);
this.age = 18;
}
let instance = new SubType("Nicholas");
console.log(instance.name);//Nicholas
console.log(instance.age);//18
优点:
- 可以解决原型链中超类型中包含引用类型值带来的问题。
- 可以向子类型构造函数中向超类型构造函数中传递参数。 缺点:
- 在超类型构造函数原型中定义的属性和方法对子类不可见。
- 由于超类原型上的方法对子类不可见,故方法都在构造函数中定义,则函数复用就无从谈起。
3、组合继承
组合继承指的是将原型链和借用构造函数技术组合在一起,发挥二者之长的一种继承模式。组合继承思想是使用原型链实现对原型的属性和方法的继承,通过借用构造函数实现对实例属性的继承。代码如下:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
优点:
- 避免了原型链继承和借用构造函数的缺陷,既能实现超类原型上方法和属性的继承也能像超类构造函数中传参。 缺点:
- 会调用两次构造函数,一次是构造函数中,一次是原型对象赋值时。
4、原型式继承
原型式继承是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。从本质上讲,对传入的对象做了一次浅复制。且必须有一个对象作为另一个对象的基础。代码如下:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
es5通过新增Object.create(参数1, 参数2)方法规范了原型式继承;第一个参数是用作新对象原型的对象,参数2是一个可为新对象定义额外属性的对象。例如:
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"];
}
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
})
console.log(anotherPerson.name);//Greg
缺点:包含引用类型值得属性始终都会共享相应的值。
5、寄生式继承
寄生式继承的思路是创建一个仅用于封装继承过程的函数,在函数内部增强对象,最后再返回对象。代码如下:
function createAnother(original) {
let clone = object(original);
clone.sayHi = function() {
console.log("hi");
}
return clone;
}
6、寄生组合式继承
组合继承会调用两次超类型构造函数,一次是创建子类型原型的时候,一次是子类型构造函数内部,寄生组合式继承就是为了解决这一问题而出现。代码如下:
function inheritPrototype(SubType, SuperType) {
let prototype = Object(SuperType.prototype);//创建对象
prototype.constructor = SubType;//增强对象
SubType.prototype = prototype;//指定对象
}
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name, age) {
SuperType.call(this.name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
优点:集寄生式继承和组合继承优点于一体,是实现基于类型继承的最有效方式。