JS 当中如何实现继承?(原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承法、class 继承)
前言
JavaScript 在语言设计上采用了原型继承的方式,而不是传统的基于类的继承模型。在 JavaScript 中,并没有类(class)的概念,而是通过原型(prototype)来实现对象之间的继承关系。每个对象都有一个原型对象,它包含了对象的属性和方法。当试图访问一个对象的属性或方法时,JavaScript 引擎会沿着原型链向上查找,直到找到对应的属性或方法为止。
这篇文章让我们了解继承的那些方法!!!!
一、原型链继承
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型对象和实例,三者之间存在着一定的关系:
- 每个构造函数都有一个原型对象,通过构造函数的
prototype
属性访问。 - 每个实例都包含一个指向构造函数原型对象的指针(
__proto__
或[[Prototype]]
)。 - 构造函数的原型对象包含着共享给所有实例的属性和方法。
- 实例通过原型链继承构造函数原型对象中定义的属性和方法。
总之,构造函数负责创建对象,原型对象负责存储共享的属性和方法,而实例则通过原型链继承构造函数原型对象的属性和方法。
function parent() {
this.name = 'Tom'
this.like = [1, 2]
}
Child.prototype = new parent()
function Child() {
this.age = 18
}
let child1 = new Child()
console.log(child1);
这段代码虽然实现了继承,但存在潜在的问题。例如:
let child1 = new Child()
let child2 = new Child()
child1.like.push(3)
console.log(child1.like); // [ 1, 2, 3 ]
console.log(child2.like); // [ 1, 2, 3 ]
在这里我们只是更改了child1
的like
属性,为什么child2
的也一起改变了呢?
这是因为所有通过原型链继承的对象都会共享同一个原型对象的属性和方法。这意味着,如果一个对象修改了原型对象的属性或方法,会影响到所有继承自该原型对象的对象。
缺点:
- 多个实例之间共用了同一个原型,属性会相互影响
- 子类无法给父类传参
二、构造函数继承
构造函数继承是另一种实现继承的方法,它通过在子类构造函数中调用父类构造函数来实现继承父类属性的目的。
function parent(name) {
this.name = name
}
function Child(name) {
parent.call(this,name)
this.age = 18
}
let c = new Child('Tom')
console.log(c.name); // Tom
在这里,通过调用父类的构造函数 parent.call(this, name)
,子类 Child
继承了父类 parent
的属性 name
。这样虽然解决了第一种继承方法的弊端(共享属性和方法、无法传参),但是它只继承了父类构造函数的属性,没有继承父类原型的属性。
parent.prototype.getName = function() {
return this.name
}
一旦父类原型对象中存在父类之前自己定义的方法,那么子类将无法继承这些方法。当我们调用这个方法时会报错。
console.log(c.getName());
TypeError: c.getName is not a function
缺点:
- 无法继承到父类原型上的属性
- 构造函数复用困难
三、组合继承(经典继承)
既然组合继承弥补了原型链继承的缺点,那么将二者结合起来使用岂不是起到了 1 + 1 > 2 的效果,于是组合继承诞生了。
parent.prototype.getName = function() {
return this.name
}
function parent(name) {
this.name = name
this.like = [1, 2]
}
// 设置子类原型为父类实例,实现继承
Child.prototype = new parent()
// 修正constructor的指向,指向构造函数Child
Child.prototype.constuctor = Child
function Child(name) {
// 调用父类构造函数,继承父类属性
parent.call(this, name) // this.name = 'Tom'
this.age = 18
}
let c1 = new Child('Tom')
let c2 = new Child('Jerry')
c1.like.push(3)
console.log(c1.like); // [ 1, 2, 3 ]
console.log(c2.like); // [ 1, 2 ]
console.log(c1.getName()); // Tom
貌似这样就是非常完美的继承方法了,但从代码中我们可以看见 parent
执行了两次:第一次是改变 Child
的 prototype
的时候,第二次是通过 call
方法调用 parent
的时候,那么 parent
多构造一次就多进行了一次性能开销,这是我们不愿看到的,我们可以通过寄生组合继承法
解决这个问题。
四、原型式继承
原型式继承是一种通过使用已有对象作为模板来创建新对象的继承方式,我们可以通过 Object.create()
方法来实现原型式继承,Object.create()
方法接受两个参数,第一个参数是用作新创建对象的原型对象,第二个参数是一个可选的对象,用于定义新对象的属性。
let obj = {
name: 'Tom',
age: '18',
like: [1,2,3]
}
let obj2 = Object.create(obj)
obj2.like.push(4)
console.log(obj2); // {}
console.log(obj2.__proto__); // { name: 'Tom', age: '18', like: [ 1, 2, 3, 4 ] }
我们可以看出这是利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。所以 Object.create()
的实现为:
function object(obj){
function Fn(){}
Fn.prototype = obj;
return new Fn();
}
缺点:
- 多个对象之间共用了同一个原型,属性会相互影响
- 子类无法给父类传参
五、寄生式继承
寄生式继承与原型式继承类似,它核心思想是在一个已有的对象上添加新的属性或方法,然后返回这个对象作为子类的原型。这样子类就能够继承原型对象上的属性和方法,同时还可以拥有自己定义的属性和方法。
// 原型对象
var parent = {
name: "Parent",
sayName: function() {
console.log("My name is " + this.name);
}
};
// 寄生式继承函数
function createChild(original) {
// 通过 Object.create() 方法创建一个新对象,该对象的原型链指向 original
var child = Object.create(original);
// 添加子类的新属性或方法
child.age = 18;
// 返回新对象
return child;
}
// 创建子类对象
var child = createChild(parent);
// 调用子类对象的方法
child.sayName(); // 输出 "My name is Parent"
console.log(child.age); // 输出 18
其中createChild
函数的主要作用就是为构造函数新增属性和方法,以增强函数。
缺点:
- 多个实例之间共用了同一个原型,属性会相互影响
- 子类无法给父类传参
六、寄生组合继承法(优雅)
寄生组合继承是一种结合了寄生式继承和组合继承的继承方式,它解决了组合继承中重复调用父类构造函数的问题,从而提高了性能。在寄生组合继承中,通过借用构造函数来继承父类的属性,并通过原型链的方式来继承父类的方法。
parent.prototype.getName = function() {
return this.name
}
function parent(name) {
this.name = name
}
// 使用 Object.create() 减少一次构造,优化了性能。
Child.prototype = Object.create(parent.prototype)
Child.prototype.constuctor = Child
function Child(name) {
parent.call(this, name) // 构造函数继承
this.age = 18
}
let c1 = new Child('Tom')
console.log(c1); // { name: 'Tom', age: 18 }
console.log(c1.getName()); // Tom
目前 寄生组合继承法
是最优雅的实现继承的解决方案。
七、ES6 的 extends 关键字实现继承
在 ES6 中,extends
关键字用于实现类的继承。当一个类通过 extends
关键字继承另一个类时,它实际上在原型链上建立了与父类的连接。这意味着子类的原型对象指向了父类的原型对象,从而继承了父类的属性和方法。
class Parent{
constructor(name){
this.name = name;
}
getName(){
return this.name;
}
}
class Child extends Parent{
constructor(name){
super(name)
this.age = 18;
}
}
const c = new Child('Tom')
console.log(c); // { name: 'Tom', age: 18 }
console.log(c.getName()); // Tom
extends
关键字的基本实现逻辑:
- 建立原型链关系:子类通过
extends
关键字继承父类时,它的原型对象会指向父类的原型对象,从而建立了原型链关系。这样子类就可以访问父类原型对象中定义的属性和方法。 - 调用父类构造函数:子类的构造函数中可以通过
super()
方法调用父类的构造函数,并传递参数。这样可以在子类中初始化继承自父类的属性。 - 子类特有方法:子类可以定义自己特有的方法,这些方法会被添加到子类的原型对象上。
前面我们提到:目前 寄生组合继承法
是最优雅的实现继承的解决方案,所以我们不难猜出 ES6 中类的继承实现原理就是寄生组合继承
想要了解ES6中 Class继承 的戳这里:《ECMAScript 6 入门教程》对于Class的继承介绍
总结 🌸🌸🌸
看到这里,恭喜你彻底了解了JS 中实现继承的方法。在实际开发中,选择合适的继承方式取决于项目的需求和开发团队的偏好,合理的运用各种知识点才是我们学习的意义了😜😜😜
转载自:https://juejin.cn/post/7346987289931382810