小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
继承作为 js 的高级技能在开发过程中也会经常遇到,但是很多小伙伴表示对这个知识点还是有些不太清晰,今天就来把这个知识给汇总一下。
继承是面向对象编程中的一个重要概念,通过继承可以使子类的实例使用在父类中定义的属性和方法。
1 原型链继承
原型链继承是ECMAScript的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。什么是原型链?每个构造函数都会有一个原型对象,调用构造函数创建的实例会有一个指针__proto__指向原型对象,这个原型可能是另一个类型的实例,所以内部可能也有一个指针指向另一个原型,然后就这样形成了一条原型链。
//代码:
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;
};
let instance = ``new` `SubType();
console.log(instance.getSuperValue()); // true
特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 要想为子类新增属性和方法,必须要在
new SuperType()这样的语句之后执行,不能放到构造器中 - 无法实现多继承
- 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码:)
- 创建子类实例时,无法向父类构造函数传参
推荐指数:★★(3、4两大致命缺陷)
2 借用构造函数继承
为了解决原型中包含引用类型值的问题,开始使用借用构造函数,也叫伪造对象或经典继承。
//构造函数继承
function Teacher (name, age) {
Person.call(this, name, age);
}
var t1 = new Teacher('B', 22);
var t2 = new Teacher('C', 30);
console.log(t1.name);//B
console.log(t2.name);//C
console.log(t1 instanceof Person);//false
console.log(t1 instanceof Teacher);//true
t1.sayHi();//TypeError: t1.sayHi is not a function
t2.sayHi();//TypeError: t1.sayHi is not a function
特点:
- 解决了1中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
推荐指数:★★(缺点3)
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.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //["red,blue,green,black"]
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // ["red,blue,green"]
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
//可以继承父类原型上的属性,可以传参,可复用。 每个新实例引入的构造函数属性是私有的
重点:
结合了两种模式的优点,传参和复用
特点:
- 可以继承父类原型上的属性,可以传参,可复用。
- 每个新实例引入的构造函数属性是私有的。
缺点:
调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
4 原型式继承
不自定义类型的情况下,临时创建一个构造函数,借助已有的对象作为临时构造函数的原型,然后在此基础实例化对象,并返回。本质上是object()对传入其中的对象执行了一次浅复制。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
重点:
用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:
类似于复制一个对象,用函数来包装。
缺点:
-
所有实例都会继承原型上的属性。
-
无法实现复用。(新实例属性都是后面添加的)
-
效率较低,内存占用高(因为要拷贝父类的属性)
5 寄生式继承
与原型式继承比较接近的一种继承方式是寄生式继承,类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景
function object(person) {
function F() {}
F.prototype = person
return new F()
}
function createAnother(original){
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
重点:
就是给原型式继承外面套了个壳子。
优点:
没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:
没用到原型,无法复用。
6 寄生组合式继承(常用)
寄生:在函数内返回对象然后调用
组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参
最常用的继承方式,也是最佳的,组合继承会调用两次父类构造函数,存在效率问题。其实本质上子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
//核心代码
function object(person) {
function F(params) {}
F.prototype = person
return new F()
}
function inheritPrototype(SubType,SuperType) {
let prototype = object(SuperType.prototype) //生成一个父类原型的副本
//重写这个实例的constructor
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函数给子类原型赋值,修复了组合继承的问题
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
};
重点:修复了组合继承的问题