一、何为继承,继承的特点
继承:
简单的说就是子类继承父类,子类可以使用父类的方法、属性
特点:
①在JS中都是通过原型实现继承的
②就近原则(如果父类和子类都有某个同名属性时,优先使用最近的属性,也就是子类的属性)
二、ES5的继承实现
1、原型链继承
实现原理:
利用原型让一个引用类型继承另一个引用类型的属性和方法。也就是让子类的原型对象等于父类的实例
优点:
- 可以继承父类及其原型的全部属性和方法
- 实例和子类和父类在一条原型链上
缺点:
- 子类构建实例时不能向父类传递参数
- 父类的引用属性会被所有子类实例共享。因为是通过原型实现的继承,所以父类的实例属性会变成子类的原型属性,会导致包含引用类型值的原型属性会被所有的实例共享(基本类型没有该问题)
注意:
给子类添加原型的属性和方法需谨慎,必须放在替换原型的语句之后,否则会被覆盖掉
function ParentType() {
// 父类
this.attr = true
}
ParentType.prototype.getAttr = function () {
// 向父类原型增加方法
return this.attr
}
function ChildType() {
// 定义子类
this.a = false
}
ChildType.prototype = new ParentType() // 定义其原型对象等于父类的实例
let instance = new ChildType() // 创建子类实例
console.log(instance.attr) // true,调用父类原型中的属性,可以继承到
console.log(instance.getAttr()) // true,调用父类原型中的方法,可以继承到
console.log(instance.a) // false,使用子类原型中的属性,可以继承到
// instance.constructor指向的ParentType,这是因为原来的childType.prototype被重写了的缘故。
console.log(instance.constructor === ParentType)
2、构造函数继承
实现原理:
在子类型构造函数的内部,调用父类型的构造函数。通过call() 和 apply() 方法的作用, 改变this的指向!
核心:将父类
构造函数
的内容复制给了子类的构造函数。这是所有继承中唯一一个不涉及到prototype
的继承。
优点:
- 和原型链继承完全反过来
- 父类的引用属性不会被共享
- 子类构建实例时可以向父类传递参数
- 因为使用的call或apply的方式,所以可以实现多继承,即一个子类继承多个父类的属性和方法
缺点:
- 因为只是“借调”了构造函数,所以无法继承父类原型中的属性和方法
- 父类的属性和方法都要在父类构造函数中定义才能继承到,每次创建子类实例都重新执行一次父类的构造函数,无法实现方法的复用。(所以在实际开发中,这种继承方式很少单独使用)
注意:
子类的私有属性,为防止被父类覆盖,应定义在call或apply之后
function ParentType(name) {
// 父类
this.attr = [1, 2]
this.name = name
}
ParentType.prototype.getAttr = function () {
// 向父类原型增加方法
return this.attr
}
function ChildType(name) {
// 定义子类
ParentType.call(this, name) // 继承了父类
}
let instance1 = new ChildType('liang') // 创建子类实例
instance1.attr.push(3)
console.log(instance1.attr) // [1, 2, 3] 修改了属性值
console.log(instance1.name) // liang, 实现了向父类传参
console.log(instance1.getAttr) // undefined,未继承父类原型中的属性和方法
let instance2 = new ChildType('zai') // 创建另一个子类实例
console.log(instance2.attr) // [1, 2] 没有被共享引用类型值
console.log(instance2.name) // zai, 实现了向父类传参
3、组合继承
实现原理:
将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承方式
优点:
- 解决了原型链的引用类型值共享的问题(父类的引用属性不会被共享)
- 解决了借用构造函数无法继承原型以及构造函数中的方法无法被复用的问题(父类的方法可以被复用)
- 子类构建实例时可以向父类传递参数
- 是 JavaScript 最常用的继承模式
缺点:
- 无论什么情况下,都会调用两次父类型的构造函数:一次是在创建子类型原型的时候(
ChildType.prototype = new ParentType();)
给子类的原型添加了父类的name, arr属性,一次是在子类型构造函数内部(ParentType.call(this, name); )又给子类的构造函数添加了父类的name, arr属性,从而覆盖了子类原型中的同名参数。
function ParentType(name) {
// 父类
this.attr = [1, 2]
this.name = name
}
ParentType.prototype.getAttr = function () {
// 父类原型的方法
return this.attr
}
function ChildType(name) {
// 定义子类
ParentType.call(this, name) // 继承属性
}
ChildType.prototype = new ParentType() // 继承方法
ChildType.prototype.constructor = ChildType // 强化继承
let instance1 = new ChildType('liang') // 创建子类实例
instance1.attr.push(3)
console.log(instance1.attr) // [1, 2, 3] 修改了属性值
console.log(instance1.name) // liang, 实现了向父类传参
console.log(instance1.getAttr()) // [1, 2, 3],获取父类原型中的属性和方法
let instance2 = new ChildType('zai') // 创建另一个子类实例
console.log(instance2.attr) // [1, 2] 没有被共享引用类型值
console.log(instance2.name) // zai, 实现了向父类传参
4、原型式继承
实现原理:
原型式继承的object方法本质上是对参数对象的一个浅复制。
优点:
- 父类方法可以复用。
缺点:
- 因为是重写了原型,所以与原型链继承方式一样,会存在父类的引用属性会被所有子类实例共享的问题。
- 子类构建实例时不能向父类传递参数
核心代码:
function object(o) {
function F() {} // 先创建一个临时的构造函数
F.prototype = o; // 将传入的对象作为这个构造函数的原型
return new F(); // 返回这个临时对象的实例
}
注意:
在ECMAScript 5 中新增了Object.create()
方法规范了原型式继承。这个方法接收两个参数::一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, Object.create()
与 object()
方法的行为相同。
即上面的object()
函数等价于:Object.create(o)
一般是在没有必要兴师动众的去创建一个子类的构造函数时,且不存在引用类型属性时,原型式继承是完全可以胜任的
let person = {
name: 'liang',
friends: ['zhao', 'qian'],
} // 用作新对象原型的对象(要浅继承的对象)
// function object(o) {
// function F() {} // 先创建一个临时的构造函数
// F.prototype = o // 将传入的对象作为这个构造函数的原型
// return new F() // 返回这个临时对象的实例
// }
// 上面的object()函数等价于:Object.create(o);
let p1 = Object.create(person) // 创建一个实例
p1.name = 'sun'
p1.friends.push('zhou')
let p2 = Object.create(person) // 创建另一个实例
p2.name = 'zheng'
p2.friends.push('wu')
console.log(p1.friends) // ["zhao", "qian", "zhou", "wu"]
console.log(p2.friends) // ["zhao", "qian", "zhou", "wu"]
console.log(person.friends) // ["zhao", "qian", "zhou", "wu"]
5、寄生式继承
实现原理:
使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。
优点:
- 仅提供一种思路,没什么优点。
缺点:
- 为对象添加函数,无法做到函数的复用,降低效率,类似于借用构造函数方式
注意:
里面的Object.create()
不是必须的,只要是能返回新对象的函数都适用于次模式(比如new)
上代码:
function createPerson(original) {
let clone = Object.create(original) // 原型式继承
clone.getName = function () {
// 为对象添加方法
return this.name
}
return clone
}
let person = {
// 新对象原型的对象
name: 'liang',
frinds: ['zhao', 'qian'],
}
let instance = createPerson(person) // 新对象既拥有person对象的属性和方法,又拥有自己的方法
console.log(instance.getName()) // liang,调用到了新对象自己的方法
6、寄生组合式继承
实现原理:
使用寄生式继承来继承父类的原型,然后将结果指定给子类型的原型(刚才说到组合继承有一个会两次调用父类的构造函数造成浪费的缺点,寄生组合继承就可以解决这个问题。)
优点:
- 这是一种完美的继承方式
- 可以实现子类实例像父类传参
- 引用类型值不会被共享
- 实现了函数的复用
- 只调用了一次父类的构造函数
- 效率高
总结:
- 解决了上述所有的缺点
- 继承组合方式是引用类型最理想的继承方式
- 解决了组合式继承存在一个最大的问题,会调用两次父类的构造函数
function ParentType(name) {
// 父类
this.attr = [1, 2]
this.name = name
}
ParentType.prototype.getAttr = function () {
// 父类原型的方法
return this.attr
}
function ChildType(name) {
// 定义子类
ParentType.call(this, name) // 继承属性
}
let prototype = Object.create(ParentType.prototype) // 创建一个等于父类原型对象的对象
prototype.constructor = ChildType // 增强对象,弥补因重写原型而失去的constructor属性
ChildType.prototype = prototype // 完成了对父类的属性和方法的继承
let man1 = new ChildType('liang')
man1.attr.push(3)
console.log(man1.name) // liang,继承到父类的属性
console.log(man1.getAttr()) // [1,2,3] 继承到了父类的方法
let man2 = new ChildType('zhao')
console.log(man2.name) // zhao
console.log(man2.getAttr()) // [1,2] 引用类型值没有被共享
三、ES6的继承实现
1、 ES6通过class的extends实现继承
实现原理:
ES6继承的结果和寄生组合继承相似,实质上是 JavaScript 现有基于原型继承的语法糖。
其内部其实也是ES5寄生组合继承的方式,通过call构造函数,在子类中继承父类的属性,通过原型链来继承父类的方法。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super
方法),然后再用子类的构造函数修改this。
优点:
- 更规范——严格模式下执行
- 可读性高
注意:
关于什么是Class,可以参考JS中Class类是什么
相比ES5的继承中,子类的__proto__
属性指向的对应的构造函数的原型。ES6的Class
定义的子类同时有prototype
属性和__proto__
属性,因此同时存在两条继承链:
1、子类的__proto__
属性,表示构造函数的继承,总是指向父类
2、子类prototype
对象的__proto__
属性,表示原型的继承,总是指向父类的prototype
对象
extends
做了两件事情,一个是通过Object.create()
把子类的原型赋值为父类的实例,实现了继承方法,子类.prototype.proto__
也自动指向父类的原型,一个是手动修改了子类的__proto
,修改为指向父类,(本来在es5 中应该是指向Function.prototype
)
extends
也可以继承ES5的构造函数
class ParentType {
// 父类
constructor(param) {
// 父类的构造函数
this.param = param
this.attr = 'haha'
}
static age = 21
static showSomething(str) {
// 父类的静态方法
console.log(str)
}
getParam() {
// 原型的方法
return this.param
}
}
class ChildType extends ParentType {
// 定义子类继承父类
// constructor(param) {
// super(param) // 使子类获取到this对象
// }
static showSth(str) {
super.showSomething(str) // 通过super调用父类的函数
}
}
let child = new ChildType('123456') // 创建实例
ParentType.showSomething('lalala!') // lalala! 静态方法的使用
// child.showSomething('child!') // Uncaught TypeError: child.showSomething is not a function 实例无法继承静态方法。
console.log('age==>', ChildType.age)
// ChildType.state.age = 23
// ChildType.showSth('yeah!') // yeah! 子类可以继承父类的静态方法。
console.log(child.getParam()) // 123456 调用到了父类的方法
console.log(ChildType.prototype.__proto__ === ParentType.prototype) // true
console.log(ChildType.__proto__ === ParentType) // true
四、ES5,ES6继承的区别
ES6的继承实现方法,实质上是 JavaScript 现有基于原型继承的语法糖,其内部其实也是ES5寄生组合继承的方式。
相比ES5的继承中,ES6继承子类的__proto__
属性指向的对应的构造函数的原型。ES6的Class
定义的子类同时有prototype
属性和__proto__
属性
五、总结
ES6继承的实现,是基于ES5而实现的,class是语法糖,使用上更加规范、严格
ES5中实现继承的方法有很多,最常用的是组合式继承、寄生组合式继承
根据不同的场景,使用不同的方法,如果支持ES6,那么尽量用ES6 class的extends实现继承