前言
继承是JavaScript的三座大山之一。
优点:可以更好地复用以前的开发代码;缩短开发周期、提升开发效率
对象相关知识请参考文章:【JavaScript】【对象】Object
在面向对象的编程语言中,都有一个对象(子类)可以从另外一个对象(父类)继承属性和方法,从而减少重复的代码。
在js中继承是通过原型和原型链的方式实现的,每个对象都有一个指向原型对象的链接,被称为原型链。当我们访问对象的属性或者方法时,如果该对象没有定义这个属性或者方法时,js就会一直沿着原型链往上查找,直到找到为止。
一、继承种类
1.1 原型链继承
-
实现方式:将子类的原型链指向父类的对象实例。通俗讲,就是直接在原型链上写继承
function Animal (){ this.species = 'animal'; } // 原型上的方法 Animal.prototype.eat = function (){ console.log('eating') } function Cat(name,color){ this.name = name; this.color = color; } // 将 Animal 的实例挂载到 Cat 的原型链上 Cat.prototype = new Animal(); var cat1 = new Cat('福来','乳白色'); console.log(cat1.species); // animal cat1.eat(); // eating var cat2 = new Cat('包子','蓝猫');cat1的打印值截图:
cat2的打印值截图:
-
优点:可以继承构造函数的属性/方法、父类构造函数的属性/方法,父类原型的属性/方法
-
缺点:
- 子类实例共享父类引用类型属性
- 在创建子类型的实例时,不能向父类型的构造函数中传递参数
- 无法实现继承多个
- 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
function Animal (){ this.species = 'animal'; this.xingge = ["乖巧"] } Animal.prototype.eat = function (){ console.log('eating') } function Cat(name,color){ this.name = name; this.color = color; } Cat.prototype = new Animal(); var cat1 = new Cat('福来','乳白色'); var cat2 = new Cat('包子','蓝猫'); cat1.xingge.push("可爱"); console.log(cat1.xingge, cat2.xingge) // 都是:['乖巧', '可爱'] -
原理:
子类实例
cat1的__proto__指向Cat的原型链prototype,而Cat.prototype指向Animal类的对象实例,该父类对象实例的__proto__指向Animal.prototype,所以Cat可继承Animal的构造函数属性、方法和原型链属性、方法
1.2 构造函数继承
-
实现方式:在子类构造函数中使用
call或apply劫持父类构造函数方法,并传入参数function Parent(name, id){ this.id = id; this.name = name; this.printName = function(){ console.log(this.name); } } Parent.prototype.sayName = function(){ console.log(this.name); }; function Child(name, id){ Parent.call(this, name, id); // Parent.apply(this, arguments); } var child = new Child("福来", "1"); child.printName(); // 福来 child.sayName() // Error => Uncaught TypeError: child.sayName is not a function console.log(child instanceof Child) // true console.log(child instanceof Parent) // false -
优点:
- 创建子类实例时,可以向父类传递参数(可以解决原型链继承的缺点)
- 可以实现多继承(call多个父类对象)
-
缺点:不可继承父类的原型链方法,构造函数不可复用
-
原理:使用
call或apply更改子类函数的作用域,使this执行父类构造函数,子类因此可以继承父类共有属性
1.3 组合继承
function MaoMao(name, breed) {
this.name = name || "";
this.breed = breed || "";
this.sleep = function () {
console.log(this.name + "在睡觉!");
};
this.xingge = ["乖巧"]
}
MaoMao.prototype = {
eat: function() {
console.log(this.name + "在吃饭")
},
play: function () {
console.log(this.name + "在玩!");
}
};
function YingDuan(name) {
MaoMao.call(this, name, "英短长毛");
// MaoMao.apply(this, arguments);
}
YingDuan.prototype = new MaoMao()
var fulai = new YingDuan("福来")
var baozi = new YingDuan("包子")
修改baozi的属性xingge: baozi 和 fulai的xingge属性值不一样
- 优点:
- 可以继承父类原型上的属性,且可传参
- 每个新实例引入的构造函数是私有的
- 缺点:
- 会执行两次父类的构造函数(一次是在创建子级原型的时候;另一次是在自己构造函数内部)。消耗较大内存,子类的构造函数会代替原型上的那个父类构造函数
- 原理:综合使用构造函数继承和原型链继承
- 利用原型链继承,实现原型对象方法的继承
- 利用构造函数继承,实现属性的继承,且可传参
1.4 原型式继承
function maomao(obj) {
function YingDuan() {};
YingDuan.prototype = obj;
return new YingDuan();
}
const options = { name: "福来", aihao: { aihai1: "逗猫棒" } };
var fulai = maomao(options)
var baozi = maomao(options)
console.log(fulai, baozi)
// 修改fulai的aihao属性值
fulai.aihao.aihai1 = "滚球球"
console.log(fulai, baozi)
- 优点:
- 缺点:(与原型链继承的缺点相同)
- 子类实例共享父类引用类型属性
- 在创建子类型的实例时,不能向父类型的构造函数中传递参数
- 原理:通过借助原型,基于已有对象创建新的对象,是对参数对象的一种浅拷贝。
1.5 寄成式继承
function maomao(obj) {
function YingDuan() {};
YingDuan.prototype = obj;
return new YingDuan();
}
function setYingDuanInfo(obj) {
const yingduan = maomao(obj);
yingduan.eatting = function() {
console.log("猫猫在吃饭...")
}
return yingduan;
}
const options = { name: "福来", aihao: { aihai1: "逗猫棒", aihao2: "猫抓板" } };
var fulai = setYingDuanInfo(options)
var baozi = setYingDuanInfo(options)
console.log(fulai, baozi)
// 修改fulai的aihao属性值
fulai.aihao.aihai3 = "滚球球"
console.log(fulai, baozi)
- 优点:
- 缺点:(同原型式继承)寄生式继承为构造函数新增属性和方法,增强了函数属性。并且新添加函数方法无法实现函数复用
- 原理:是创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
- 核心:在原型式继承的继承之上,增强对象,返回构造函数
- 函数的主要作用是为构造函数新增属性和方法,以增强函数
1.6 寄生组合式继承
function inherit(children, parent) {
// 创建对象 - 创建 父类原型的一个副本
let prototype = Object.create(parent.prototype);
// 增强对象 - 弥补因重写原型而失去的默认的constructor属性
prototype.constructor = children;
// 指定对象 - 将新创建的对象复制给子类的原型
children.prototype = prototype;
}
// 父类初始化实例属性和原型属性
function Parent(name) {
this.name = name;
this.num = [0, 1, 2];
}
Parent.prototype.sayName = function() {
console.log(this.name, "说,今天也很幸运的呢")
};
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// 将父类原型指向子类
inherit(Child, Parent);
// 新增子类原型属性
Child.prototype.sayAge = function() {
console.log(this.age);
};
var fulai = new Child("福来", 1)
console.log(fulai instanceof Child); // true
console.log(fulai instanceof Parent); // true
console.log(fulai.sayName()); // 福来 说,今天也很幸运的呢
- 优点:
- 只调用了一次
Child构造函数 - 避免了在
Child.prototype上创建不要的、多余的属性 - 原型链保持不变
- 只调用了一次
- 缺点:
- 原理:构造函数继承 + 组合继承
- 借用构造函数来继承不可共享的属性
- 通过寄生式来继承方法和可共享的属性