共有六种继承方式:构造函数式继承、原型链式继承、组合继承、原型式继承、寄生式继承、寄生组合式继承
一、构造函数式继承
利用函数实现继承
//父类构造函数
function Father(){
this.name = "Father";
this.say = function(){
console.log(this.name);
}
}
//子类构造函数
function Son(){
Father.call(this); // 重点
}
重点:用call将父类构造函数引入子类函数(在子类函数中进行了父类的复制)
测试:
let son1 = new Son();
son1.say(); // Father
son1成功继承到了父类的name属性和say方法
但这不是传统意义上的继承,因为每一个Son子类调用父类生成的对象,都是各自独立的,也就是说,如果父类希望有一个公共的属性是所有子类实例共享的话,是没办法实现的。
优点:
- 可以继承多个构造函数(可以多个call)
- 所有基本属性独立,不会被其他实例影响
缺点:
- 无法继承父类构造函数的属性和方法(.prototype)
- 每个新实例都拷贝了一遍父构造函数,如果父类很大会占用内存
- 未实现公共方法的复用(函数复用),每次都会创建一个新函数而不是共用
二、原型链式继承
最原始的方式,通过prototype属性继承
//父级 构造函数
function Father() {
this.arr = [1,2,3];
}
//父级 原型属性
Father.prototype.name = "tingting"
//子级 构造函数
function Son() {
}
//子级 原型属性: 继承父级
Son.prototype = new Father() // 重点
//创建子级的实例对象
let son = new Son();
console.log(son.name); // tingting
重点:子类原型等于父类的实例 Son.prototype = new Father()
解释son实例如何找到name属性
如果一个对象某个属性找不到,会沿着它的原型往上去寻找,直到原型链的最后才会停止寻找
- 首先在son对象自身查找,若未找到
- 在Son.prototype中找 ( Father() ) ,若未找到
- 在上一层
Son.prototype.__proto__
(Father.prototype) - 直到找到需要的属性或方法,或达到原型链顶端Object.prototype
但所有的子实例的属性和方法,都在父类同一个实例上了,所以一旦某一个子实例修改了其中的方法,其他所有的子实例都会被影响
看下面代码
function Father() {
this.arr = [1,2,3];
}
function Son() {
}
Son.prototype = new Father()
let son1 = new Son();
let son2 = new Son();
son1.arr.push(4);
console.log(son1.arr); // 1,2,3,4
console.log(son2.arr);// 1,2,3,4
子实例son1修改arr后,son2实例的arr也被修改了
优点:
- 简单
缺点:
- 无法向父类构造函数传参
- 所有实例都会共享父类实例的属性(一个实例进行修改,那所有属性都会变化)
三、组合继承(原型链+构造函数)
将原型链继承和借用构造函数继承组合,整合了二者的优点,常用
function Father(color) {
this.color = color;
}
Father.prototype.print = function() {
console.log(this.color);
}
function Son(color) {
Father.call(this, color); // 借用构造函数继承
}
Son.prototype = new Father(); // 原型链继承
let son1 = new Son('red');
son1.print(); // red
let son2 = new Son('blue');
son2.print(); // blue
在Son子类中,使用了Father.call来调用父类构造函数,同时又将Son.prototype赋给了父类实例
使用Father.call调用父类构造函数之后,以后所有通过new Son()
创建出来的实例,单独拷贝了一份父类构造函数中定义的属性和方法;
再把父类的实例赋值给子类的原型prototype,所有子类实例就可以共享父类原型上的属性和方法;
因此,定义父类函时,将私有属性和方法放在构造函数里,将共享属性和方法放在原型上
优点:
- 可以传参
- 公用函数可以复用
缺点: - 调用了两次父类构造函数,占用内存
四、原型式继承
原型式继承本质其实就是一个浅拷贝,以一个对象为模板复制出新的对象
// 封装一个函数容器,用来承载继承的原型
function object(obj){
function F(){
}
F.prototype = obj;
return new F();
}
let father = {
name: "father",
arr: [1,2,3]
}
let son1 = object(father);
object函数中,定义一个构造函数,以obj为模板,让构造函数的原型对象(prototype)指向obj,再返回构造函数的一个实例,就可以访问到对象obj所有属性和方法。
Object.creat()
es5中新增了一个函数Object.create()直接实现了原型式继承
上面的代码可以改写为:
let father = {
name: "father",
arr: [1,2,3]
}
let son1 = Object.create(obj);
本质还是原型链继承,同样会共享父类的属性
五、寄生式继承
寄生式继承就是把原型式继承再次封装,然后在对象上扩展新方法,再把新对象返回
可以理解为在原型式继承的基础上新增一些函数或属性
function object(obj){
function F(){
}
F.prototype = obj;
return new F();
}
// 上面是原型式继承
function create(obj){
let clone = object(obj) // 或 Object.create(obj)
clone.say = function(){
console.log('123');
}
return clone;
}
// 用create函数封装了一遍,又增添了say方法
let father = {
name: "father",
arr: [1,2,3]
}
let son = create(father);
son.say(); // 123
clone给对象添加新的方法,最后返回clone,这样就继承了object返回的对象
优点:
- 不用创建自定义类型
缺点:
- 没用到原型,无法复用
六、寄生组合式继承(常用)
最后一个继承方式,是最完美的解决方案
是es6 class语法的实现原理,主要目的是为了解决组合式继承中每次都需要new Father导致执行多一次父类构造函数的缺点
function Father(color) {
this.color = color;
}
Father.prototype.print = function() {
console.log(this.color);
}
function Son(color) {
Father.call(this, color);
}
Son.prototype = Object.create(Father.prototype); // 划重点
let son1 = new Son('red');
son1.print(); // red
let son2 = new Son('blue');
son2.print(); // blue
这段和组合继承的不同之处只有一个,就是把原来的 Son.prototype = new Father();
修改为Son.prototype = Object.create(Father.prototype);
原型式中提过Object.create
方法是以传入的对象为原型,创建一个新对象;创建了这个新对象之后,又赋值给了Son.prototype
,因此Son的原型最终指向的其实就是父类的原型对象,和new Father
是一样的效果,且不会创建多余的实例占用内存,同时也可以实现多继承;