JavaScript中的继承方式详细解析

什么是继承

继承是面向对象编程中的一个重要概念,它指的是一个对象(或类)可以获得另一个对象(或类)的属性和方法。在继承中,被继承的对象通常称为父类(或基类、超类),继承属性和方法的对象称为子类(或派生类、衍生类)。

继承的主要作用包括:

  1. 代码复用:通过继承,子类可以重用父类的属性和方法,避免了重复编写相似的代码,提高了代码的可维护性和复用性。

  2. 组织代码:通过将相关的属性和方法封装在一个类中,并让其他类继承它,可以更好地组织和管理代码,使代码结构更清晰。

  3. 多态:继承是实现多态的基础。子类可以重写父类的方法,从而根据实际情况执行不同的代码逻辑,实现不同的行为。

虽然JavaScript并不是真正的面向对象语言,但是它天生的灵活性,使应用场景更加丰富。

关于继承,我们举个形象的例子:

javascript 复制代码
// 父类 Animal
class Animal {
    constructor(name) {
        this.name = name;
    }

    // 父类方法
    speak() {
        console.log(`${this.name} makes a noise.`);
    }
}

// 子类 Dog 继承自 Animal
class Dog extends Animal {
    constructor(name) {
        super(name); // 调用父类的构造函数
    }

    // 子类方法
    speak() {
        console.log(`${this.name} barks.`);
    }
}

// 创建一个 Animal 实例
const animal = new Animal('Generic Animal');
animal.speak(); // 输出: Generic Animal makes a noise.

// 创建一个 Dog 实例
const dog = new Dog('Buddy');
dog.speak(); // 输出: Buddy barks.

在这个例子中,Animal定义了一个通用的动物类它有一个构造函数和一个speak()方法,Dog类通过extends关键字继承了Animal类。它也有一个构造函数和一个speak()方法,但是speak()方法被重写,输出了不同的声音。在子类的构造函数中使用super()调用了父类的构造函数,以确保在创建子类实例时父类的属性也被正确的初始化。

继承方式

  • 原型链继承
  • 构造函数继承(借助call)
  • 组合继承
  • 原型式继承
  • 寄生虫式继承
  • 寄生组合式继承

原型链继承

原型链继承是比较常用的继承方式之一,其中涉及的构造函数、原型、实例。三者之间存在一定的关系,即每个构造函数都有一个圆形对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针

javascript 复制代码
function Parent() {
  this.name = 'parent1';
  this.play = [1, 2, 3]
}
function Child() {
  this.type = 'child2';
}
Child.prototype = new Parent();

var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // [1,2,3,4]

我们发现,在改变s1的play属性的时候,s2也跟着变化了,这是因为两个实例使用的是同一个原型对象,内存空间也是共享的。

原型链继承小结:

优点:

  • 简单易懂:实现简单,易于理解
  • 可以实现函数复用:子类可以共享父类原型中的方法

缺点:

  • 共享属性:所有的实力共享父类原型中的属性,容易造成属性修改的相互影响
  • 无法向父类传参数:无法在创建子类实例的时候向父类构造函数传递参数

构造函数继承

构造函数继承是通过在子类构造函数中借助call || bind等调用父类的构造函数来实现继承的模式。这种方式主要是基于JavaScript中的函数特性。

举例1:

javascript 复制代码
// 父类
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function(){
    console.log(this.name);
}

// 子类
function Child(name, age) {
    Parent.call(this, name); // 在子类构造函数中调用父类构造函数,传入子类实例(this)和参数
    this.age = age;
}

// 创建子类实例
var child1 = new Child('John', 10);
var child2 = new Child('Jane', 12);

// 修改子类实例属性
child1.colors.push('black');

console.log(child1.name); // 输出: John
console.log(child1.age); // 输出: 10
console.log(child1.colors); // 输出: ["red", "blue", "green", "black"]

console.log(child2.name); // 输出: Jane
console.log(child2.age); // 输出: 12
console.log(child2.colors); // 输出: ["red", "blue", "green"]

child1.sayName()//报错

可以从上面代码中发现,父类原型上定义的方法,子类是无法继承这些方法的。相比于第一种原型链继承的方法,构造函数继承的方式的父类应用不会被共享,优化了第一种继承方式的弊端。但是缺点也很明显,即不能继承原型属性或者方法。

构造函数继承小结:

优点:

  • 可以向父类传递参数:子类可以通过在构造函数中调用父类构造函数来传递参数
  • 没有共享属性:每个实例都有自己的属性,不会互相影响

缺点:

  • 无法继承方法:子类无法继承父类原型中的方法,导致方法无法复用
  • 造成内存浪费:每个实例都会单独拥有一份方法的副本,造成内存浪费
  • 无法形成真正的原型链,导致无法使用原型链上的一些方法和属性

组合继承

组合继承结合了原型链继承和构造函数继承的优点,两者结合,避免了两者的缺点。通过组合继承,我们可以实现实例属性的独立性,同时实现方法的共享,提高了代码的可维护性和复用性。

javascript 复制代码
// 父类
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function(){
    console.log(this.name);
}
// 子类
function Child(name, age) {
    Parent.call(this, name); // 在子类构造函数中调用父类构造函数,传入子类实例(this)和参数
    this.age = age;
}

Child.prototype = new Parent(); // 子类的原型指向父类的实例
Child.prototype.constructor = Child; // 修正子类的构造函数为自己

var s1 = new Child('hzz11', 18);
var s2 = new Child('hzz22',22);
console.log(s1)
console.log(s1.name); // hzz11
s1.colors.push('black');

console.log(s1.colors); // ["red", "blue", "green", "black"]
console.log(s2.colors); // ["red", "blue", "green"]
s1.sayName(); // hzz11
s2.sayName(); // hzz22

这种方式看上去觉得还行,似乎没什么问题的样子。但是仔细观察你会发现,上面代码中的Parent执行了两次(Parent.call(this, name)、new Parent()),造成了多构造一次的性能开销。

组合式继承小结:

优点:

  • 结合了构造函数和原型链继承的优点:通过构造函数实现实例属性,通过原型链继承共享方法,既够传递参数,又能实现方法的复用
  • 方法共享:子类实例共享父类原型中的方法,节省内存

缺点:

  • 重复的调用构造函数:在创建子类实例时,会调用两次父类构造函数,一次是在原型链继承时,一次是在构造函数继承时,可能会导致一些不必要的性能开销。

原型式继承

原型式继承是通过浅复制一个对象来创建一个新的对象,并将新对象的原型指向这个被复制的对象。这样新对象就能够继承被复制对象的属性和方法

javascript 复制代码
// 原型对象
var person = {
    name: 'John',
    age: 30,
    sayHello: function() {
        console.log('Hello, my name is ' + this.name);
    }
};

// 创建一个继承自 person 的新对象
var john = Object.create(person);

console.log(john.name); // 输出: John
console.log(john.age); // 输出: 30
john.sayHello(); // 输出: Hello, my name is John

Object.create()方法说明:Object.create()的作用是创建一个新的对象,并将该对象的原型链指向另外一个对象或者null

  1. 创建新对象Object.create() 可以创建一个新的对象,该对象继承了指定的原型对象的属性和方法。

  2. 指定原型链:通过指定一个对象作为参数,新创建的对象将会继承该对象的属性和方法,并且其原型链将指向这个对象。

  3. 继承属性和方法:新创建的对象将会继承指定对象的属性和方法,包括原型链上的属性和方法。

  4. 属性描述符继承:新创建的对象会继承指定对象的属性的属性描述符(比如可枚举、可写、可配置等)。

  5. 创建原型链终点:当传入 null 作为参数时,将创建一个没有原型链的对象,这个对象将不会继承任何属性和方法,相当于创建了原型链的终点。

语法:

javascript 复制代码
Object.create(proto[, propertiesObject])

其中,proto 参数是新对象的原型对象,可以是 null 或者一个对象。propertiesObject 是可选参数,用于定义额外的属性,该参数的对象属性将被添加到新创建的对象中,属性描述符则将与 Object.defineProperty() 方法的功能一致。

下面是模拟Object.create()方法的实现:

javascript 复制代码
function createObject (parent) {
  function F () { } // 创建一个临时构造函数
  F.prototype = parent; // 将临时构造函数的原型指向 parent 对象
  return new F(); // 返回新对象
}

原型式继承小结:

优点:

  • 简单灵活:使用方便,可以快速创建对象
  • 可以通过原型链并进行属性和方法的复用

缺点:

  • 共享引用类型属性:如果一个对象的引用类型属性被修改,会影响到所有继承自同一个原型的对象

寄生式继承

寄生式继承是在原型式继承的基础上增加了对象,返回一个增强后的对象。这种模式在原有对象的基础上添加额外的方法或者属性,从而实现继承

  • 原型式继承的基础:在原型式继承中,我们通过浅复制一个对象来创建一个新对象,新对象的原型链指向被复制的对象。这样,新对象就继承了被复制对象的属性和方法。
  • 增强对象:在寄生式继承中,我们不仅创建了一个新对象,还对这个新对象进行了增强。这个增强可以包括添加新的方法、修改已有方法等。
  • 返回增强后的对象:最后,我们将这个增强后的对象返回,以供使用者使用。
javascript 复制代码
// 寄生式继承函数
function createChild(parent) {
    var child = Object.create(parent); // 基于原型式继承创建一个对象
    child.sayHello = function() {      // 增加额外的方法
        console.log('Hello from Child');
    };
    return child;                      // 返回增强后的对象
}

// 原型对象
var parent = {
    name: 'Parent',
    sayName: function() {
        console.log('My name is ' + this.name);
    }
};

// 创建一个继承自 parent 的新对象
var child = createChild(parent);

console.log(child.name); // 输出: Parent
child.sayName();         // 输出: My name is Parent
child.sayHello();        // 输出: Hello from Child

其优缺点也很明显,跟上面讲的原型式继承一样

寄生组合式继承

寄生组合式继承是JavaScript中一种高效的继承模式,它结合了寄生式和组合式继承的优点。寄生组合式继承的核心思想是在子类构造函数中调用父类构造函数,通过寄生式继承来继承父类的原型,以实现方法的共享和实力属性的对立性

javascript 复制代码
// 父类
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

// 父类原型方法
Parent.prototype.sayHello = function() {
    console.log('Hello from ' + this.name);
};

// 子类
function Child(name, age) {
    Parent.call(this, name); // 构造函数继承,继承实例属性
    this.age = age;
}

// 使用寄生式继承继承父类原型
function inheritPrototype(child, parent) {
    var prototype = Object.create(parent.prototype); // 创建父类原型的副本
    prototype.constructor = child; // 修正构造函数指向
    child.prototype = prototype; // 将子类原型指向父类原型的副本
}

// 将子类原型继承父类
inheritPrototype(Child, Parent);

// 创建子类实例
var child1 = new Child('John', 10);
var child2 = new Child('Jane', 12);

// 修改子类实例属性
child1.colors.push('black');

console.log(child1.name); // 输出: John
console.log(child1.age); // 输出: 10
console.log(child1.colors); // 输出: ["red", "blue", "green", "black"]

console.log(child2.name); // 输出: Jane
console.log(child2.age); // 输出: 12
console.log(child2.colors); // 输出: ["red", "blue", "green"]

child1.sayHello(); // 输出: Hello from John
child2.sayHello(); // 输出: Hello from Jane

在这个示例中,Parent 是父类构造函数,Child 是子类构造函数。通过 Parent.call(this, name) 实现了构造函数继承,继承了父类的实例属性。然后,通过 inheritPrototype(Child, Parent) 函数实现了寄生式继承,将子类原型继承父类原型的副本,从而实现了方法的共享。最后,我们创建了两个子类实例 child1child2,它们分别拥有独立的实例属性和共享的方法。

寄生组合式继承小结

优点:

  • 实现了方法的共享和实力属性的独立性,避免了构造函数继承和原型链继承的缺点
  • 避免了构造函数继承是调用两次父类构造函数的性能开销

extends继承

文章一开头,我们是使用ES6 中的extends关键字直接实现 JavaScript的继承,我们利用babel工具进行转换也可以发现,extends实际采用的也是寄生组合继承方法,因此也证明了这种方式是较优的解决继承的方式。

相关推荐
hongkid7 分钟前
React Native 如何打包正式apk
javascript·react native·react.js
李少兄9 分钟前
简单讲讲 SVG:前端开发中的矢量图形
前端·svg
前端小万11 分钟前
告别 CJS 库加载兼容坑
前端·前端工程化
恋猫de小郭11 分钟前
Flutter 3.38.1 之后,因为某些框架低级错误导致提交 Store 被拒
android·前端·flutter
JarvanMo15 分钟前
Flutter 需要 Hooks 吗?
前端
光影少年25 分钟前
前端如何虚拟列表优化?
前端·react native·react.js
Moment26 分钟前
一杯茶时间带你基于 Yjs 和 reactflow 构建协同流程图编辑器 😍😍😍
前端·后端·面试
菩提祖师_40 分钟前
量子机器学习在时间序列预测中的应用
开发语言·javascript·爬虫·flutter
invicinble44 分钟前
对于前端数据的生命周期的认识
前端
PieroPc1 小时前
用FastAPI 后端 和 HTML/CSS/JavaScript 前端写一个博客系统 例
前端·html·fastapi