继承
实例上定义属性包新(保证每次实例化属性都是新的) + 原型上定义方法包共享(保证每次实例化方法不可每次都创建,仅是共享即可):这个很关键,是所有继承的前提
要点速览
shell
# JS 继承
在 JS 中,继承是基于原型(prototype) 实现的(ES6 新增了 class 语法糖,但底层依然是原型)。
# 七种继承方法:五种核心继承,两种非关键继承(了解即可)
原型链继承
借用构造函数继承
组合继承
寄生组合式继承
Es6 extends 继承
寄生式继承(了解,几乎不用)
原型式继承(了解,几乎不用)
# 必备:属性和方法分布在两个区域,一个是构造函数的实例上,一个是构造函数的原型上
父类实例属性 / 方法:构造函数内部 this.xxx
父类原型属性 / 方法:Parent.prototype.xxx
# 但是:在继承上属性必须放在实例上,方法必须放在原型上,重要的话说三遍:必须!必须!必须!,原因是实例上的实例化后必变,原型上的实例化后共享
# 也就是:下面拓展的继承方法的方向已经明确,实例属性——原型方法,具体看下方拓展
# 最后
定义在原型上的属性和方法不会占用实例的内存原型链继承
shell
原理:把父类的实例复制给子类的原型,让子类可以访问父类实例属性和父类原型上的方法
实现:Child.prototype = new Parent()
缺点:
父类的引用类型属性会被所有子类实例共享以至于一个实例修改会影响其他实例;
创建子类实例时,无法向父类构造函数传参(因为代码的执行顺序,Child.prototype = new Parent() 代码在子类实例化之前执行,所以子类无法在实例化的时候给父类构造函数传参)
注意:原型链继承需要修正 constructor 指针,让原型链保持完整构造函数继承
shell
原理:在子类构造函数中通过 call/apply 调用父类构造函数,实现父类属性的私有化(给子类重新挂载一遍父类实例上的属性)。
实现:Parent.call(子类this, name 参数);
缺点:
不能继承父类原型上的方法,只能继承父类实例上的属性。
没有原型链,失去原型优势(因为 Parent.call(this) 只会执行父类构造函数,不会建立原型链关系,原型链断裂)组合继承(原型链继承 + 构造函数继承)
- 原理:结合原型链继承(继承方法)和构造函数继承(继承属性),是最常用的传统继承方式。
- 优点:既继承了父类原型的方法(复用),又私有化了父类属性,支持传参,解决了前两种方式的核心问题(可传参、不共享属性、可复用原型方法,修正原型链)
- 缺点:调用了两次父类构造函数(一次是实例化,一次是在子类内部.call),存在轻微性能损耗
ts
function Parent(name) {
this.name = name;
}
Parent.prototype.sayName = function () {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
// 继承方法(改变了 prototype 原型链关系)
Child.prototype = new Parent();
// 修复构造函数指向(恢复到原来)
Child.prototype.constructor = Child;
Child.prototype.sayAge = function () {
console.log(this.age);
};
const child = new Child('Tom', 10);
child.sayName(); // "Tom"
child.sayAge(); // 10寄生组合式继承(ES6 class 继承的底层就是它)
- 实现:寄生式继承 + 组合式继承
- 实现思路:寄生式+原型链式继承=子类只继承父类原型上的方法,构造函数继承 = 子类只继承父类的属性,结合:安静无污染
- 优点:避免了组合继承中调用两次父类构造函数的问题、避免了原型链继承中共享引用属性问题、保持了原型链的完整性
- 缺点:没有
- 相比于组合式继承:只是在原型链继承那一步,将父类的实例,改成了父类的原型,这样就可以在不执行父类构造函数的前提下还能共享父类原型上的方法
- Object.create() 原理:Object.create(Parent.prototype) 创建一个{}空对象,空对象的原型指向 Parent.prototype
ts
// 父类
function Parent(name){ this.name = name }
Parent.prototype.say = function(){}
// 子类
function Child(name){
Parent.call(this, name) // 构造函数继承:只继承父类属性
}
// 原型链继承:只继承原型方法,不 new Parent!不创建父类实例!
Child.prototype = Object.create(Parent.prototype)
// 修正构造函数指向
Child.prototype.constructor = ChildES6 Class 继承
- ES6 的 class 语法实际上是语法糖,通过 extends 和 super 实现更简洁的继承。底层还是基于寄生组合式继承实现的。
- 优点:实现了面向对象类继承的概念,更易理解
- 缺点:仍然是基于原型的语法糖,旧的浏览器需要Babel等工具转译
ts
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的constructor
this.age = age;
}
sayAge() {
console.log(this.age);
}
}原型式继承(克隆)
shell
实现:使用Object.create()克隆父类实例对象作为子类实例对象(非构造函数操作)const child = Object.create(parent);(也可以通过手写函数实现)
缺点:共享父类引用类型属性,不能直接添加新的属性和方法寄生式继承(原型式继承 + 工厂模式)
shell
实现:创建一个工厂函数,在原型式继承的基础上添加额外的方法或属性,最终返回增强对象
缺点:共享父类引用类型属性,
相比原型式继承:可以添加新的属性和方法,相当于原型式继承的增强版拓展
shell
# 为什么要把属性定义在实例上,把方法定义在原型上
因为实例上的属性或方法每次实例化的时候都是新的,而原型上的属性和方法确是在实例化后各个实例共享的
因此这很符合我们的预期:属性不可共享只能实例化,方法只能共享不可实例化
# 进一步解释方法在原型上的元婴
方法挂载在实例上 = 每 new 一个对象,就新建一个函数;
方法挂载在原型上 = 所有实例共用同一个函数,内存复用。
1. 极大节省内存(最核心原因)
实例上:1000 个实例 = 1000 个一模一样的函数
原型上:1000 个实例 = 1 个函数
页面开越多、实例越多,差距越恐怖。
2. 性能更好
不用每次构造函数执行都创建新函数,初始化更快。
3. 最主要的符合原型设计思想
属性每个实例都不一样(放实例),每次实例化的时候都是新的
方法所有实例都一样(放原型),每次实例化的时候统一用一个
姓名、年龄每个人不同 → 放 this 实例
说话、吃饭逻辑所有人一样 → 放 prototype 原型
# 同理,原型可以挂载属性吗?
不可,如果共享那么属性讲毫无意义总结
- 原型链继承把父类的实例化对象 赋值给 子类构造函数的原型对象把子类构造函数的原型对象的 constructor 指针重新指向子类构造函数
- 借用构造函数继承借用父类构造函数的 call 或者 apply 方法,让子类的 this 在父类构造函数中重新挂载一遍父类构造函数属性
- 组合继承原型链继承 + 借用构造函数继承
- 原型式继承创建一个空对象,把父类的实例赋值给空对象的原型对象,然后把这个空对象作为子类实例
- 寄生式继承创建一个工厂函数,在原型式继承的基础上添加额外的方法或属性,最终返回增强对象
- 寄生组合式继承(最佳)实现思路:寄生式+原型链式继承=子类只继承父类原型对象上的方法,构造函数继承 = 子类只继承父类的属性,结合:安静无污染保留了构造函数继承,用寄生式继承改进了原型链继承(原型链保持干净,只调用一次父类构造函数)(寄生式继承 --- 纯继承父类方法,构造函数继承 --- 纯继承父类属性)
- ES6 extends 继承 ES6 extends 继承本身是个语法糖,是基于寄生组合继承实现的
- 对于现代 JavaScript 开发,推荐使用 ES6 的 class 语法,如果需要兼容旧环境,寄生组合式继承是最佳选择
