likes
comments
collection
share

JavaScript里的七种继承

作者站长头像
站长
· 阅读数 27

前言

继承,简单来讲就是让子类的实例能够访问到父类上的属性,在JavaScript中能够实现实现继承的方式有多种,本文主要介绍到的有七种


一、原型链继承

在JavaScript里,原型链继承是基于原型对象实现的一种继承方式。每个JavaScript对象在其创建时都会有一个内部属性[[Prototype]](原型),这个属性链接到另一个对象,当尝试访问该对象上不存在的属性或方法时,JavaScript引擎会沿着这条链(原型链)向上查找,直到找到相应的属性或方法为止。

实现方式

假设我们有如下两个构造函数PersonStudent

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log(`Hello, my name is ${this.name}`);
};

function Student(name, grade) {
    this.name = name;
    this.grade = grade;
}

Student.prototype = new Person();

var student1 = new Student('Alice', 10);
var student2 = new Student('Bob', 11);

student1.sayHello()
student2.sayHello()

这里,Studentprototype被设置为new Person()的实例,所以所有的Student实例都能访问Person原型上的sayHello方法。

JavaScript里的七种继承

缺点

  1. 内存共享:由于Student.prototype是一个Person实例,所有Student实例都共享Person.prototype上的属性。

    如果Person.prototype上有可变的引用类型属性(像数组或对象),那么所有Student实例会共享这些引用,导致实例之间的数据可能互相影响。

例如:

Person.prototype.favorites = [];
student1.favorites.push('math');

console.log(student2.favorites);  // ['math'] - 这是student1喜欢的科目,不是student2

JavaScript里的七种继承

  1. 构造函数被篡改:在上面的例子中,Student.prototype.constructor现在指向的是Person,而不是Student

构造器(constructor) 用来记录这个对象是由谁创建出来的

JavaScript里的七种继承

JavaScript里的七种继承

二、构造函数继承

构造函数继承允许子类实例从父类构造函数继承属性,但不继承方法

这种方法解决了原型链继承中所有实例共享同一份原型对象的问题,从而避免了实例间的相互影响。

但是它也带来了一些限制,最主要的是子类实例不能继承到父类的原型

实现方式

function Person(name) {
    this.name = name;
    this.colors = ['red', 'blue']; // 可能导致问题的共享引用类型
}

function Student(name, grade) {
    Person.call(this, name); // 显示绑定继承属性
    this.grade = grade;
}

var student1 = new Student('Alice', 10);
var student2 = new Student('Bob', 11);

使用 call 方法将 Person 的构造函数应用到 Student 的实例上,这样 Student 的实例就可以拥有与 Person 实例相同的属性,但每个实例都有自己的一份拷贝,因此它们之间不会相互影响。

缺点

  1. 不能访问父类原型上的方法:由于没有使用原型链,子类实例无法访问父类原型上的任何方法。这意味着如果你在父类的原型上定义了一个方法,子类实例将无法使用这个方法。
Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

student1.sayHello(); // TypeError: student1.sayHello is not a function

JavaScript里的七种继承

三、组合继承

原型链继承+构造函数继承,

子类实例继承父类的属性,并且能够通过原型链访问父类的方法

实现步骤

  1. 构造函数继承父类的属性:在子类构造函数中调用父类构造函数,使用 call 或者 apply 方法,将父类的属性复制到子类实例上。
  2. 原型链继承父类的方法:设置子类的 prototype 属性为父类的一个新实例,这样子类就能够访问到父类原型上的方法。
  3. 修正构造函数指针:在第二步中,子类的 prototype 成为了父类的一个实例,子类 prototypeconstructor 指向了父类构造函数,需要手动指定子类的 constructor
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.age = age;
}

// 继承方法
Child.prototype = new Parent();
Child.prototype.constructor = Child; // 修正构造函数指针

var child1 = new Child('JiSung', 10);
child1.sayName()//调用父类原型上的方法
console.log(child1.colors)//输出继承到的父类属性

JavaScript里的七种继承

JavaScript里的七种继承

缺点

  1. 父类构造函数被调用两次:一次是在子类构造函数中,另一次是在设置子类原型时。这会产生多余的性能开销。
  2. 创建子类实例时的额外开销:父类构造函数被调用了两次,每次都会创建一个新的实例,即使第二次调用并没有真正使用这个实例,也会增加内存和CPU的负担。

四、原型式继承

原型式继承是一种直接基于对象的继承方式,不涉及构造函数或原型链,而是直接从一个对象继承属性和方法。

这种继承方式的核心是使用Object.create()方法,它接收一个对象作为参数,并返回一个新对象,这个新对象的原型就是传入的对象。

实现方式

let parent = {
  name: 'JiSung',
  age: 12
}

let child = Object.create(parent);//凭空创建一个新对象,并让这个新对象继承到parent里的属性

JavaScript里的七种继承

子对象的隐式原型里面有父对象里的所有属性和方法

缺点

引用类型的属性会被所有实例共享:多个实例之间继承到的引用类型是相同的地址,会相互影响。

let parent = {
  name: 'JiSung',
  age: 12
}
let child2 = Object.create(parent);
child2.like.push(3)

console.log(child2.like);

JavaScript里的七种继承

五、寄生式继承

寄生式继承基于原型式继承的基础上进行了改进。

在原型式继承中,一个新对象是基于另一个对象创建的,但所有实例共享原型对象上的属性,尤其是引用类型属性,这可能导致数据污染。

寄生式继承则通过创建一个仅用来继承的函数,然后在返回新对象之前对这个新对象进行增强,以此确保每个实例都有其独立的属性副本,可以让子对象默认具有自己的属性。

实现步骤

  1. 创建一个函数,这个函数的目的是为了创建一个新对象。
  2. 借助Object.create()方法,以某个对象作为原型创建新对象。
  3. 在这个新对象上添加方法或属性。
  4. 返回这个新对象
let parent = {
  name: 'JiSung',
  age: 22,
  like: [1, 2]
}

function clone(obj) {
  let clone = Object.create(obj)//// 通过调用Object.create() 函数创建一个新对象
  clone.getLike = function () {//增强对象
    return this.like
  }
  clone.sex = '男'//子对象新添加的属性
  return clone
}

let child = clone(parent)
console.log(child);

JavaScript里的七种继承

缺点

  • 和原型式继承一样,因为是浅拷贝,所以多个实例之间继承到的引用类型是相同的地址,会相互影响。

  • 无法传递参数

六、寄生组合继承

寄生组合继承结合了原型链继承和构造函数继承的优点,同时避免了它们各自的缺点。

寄生组合继承的目标是避免父类构造函数的多次调用,并确保每个子类实例都能够正确地继承父类的属性和方法,而不会共��引用类型属性

实现步骤:

  1. 创建一个不作任何事情的临时构造函数:这个构造函数用于继承父类的属性,但不会执行父类构造函数中的任何代码。
  2. 使用Object.create()方法:使用临时构造函数的原型来创建子类的原型,这样子类就能继承父类原型上的方法。
  3. 修正构造函数指针:确保子类的prototype.constructor指向子类本身,而不是临时构造函数。
  4. 在子类构造函数中调用父类构造函数:使用callapply方法调用父类构造函数,以便每个子类实例都能获得父类的属性。
function Parent() {
    this.name = 'JiSung';
    this.like = [1, 2, 3];
}

Parent.prototype.getName = function () {
    return this.name;
}

function Child() {
    Parent.call(this);
    this.type = 'children';
}


// 临时构造函数
function F() {}

// 设置临时构造函数的原型
F.prototype = Parent.prototype;

// 设置子类的原型
Child.prototype = new F();

// 确保构造函数指针正确
Child.prototype.constructor = Child;

也可以将临时构造函数改为下列代码,使用 Object.create(),避免创建一个临时构造函数,使得代码更加简洁:

Child.prototype = Object.create(Parent.prototype)  //创建了一个新对象,跟原来的Parent.prototype不是同一个对象
Child.prototype.constructor = Child

优点:

  • 避免了父类构造函数的多次调用,提高了性能。
  • 每个子类实例都有独立的引用类型属性副本,避免了数据污染。
  • 子类实例能够访问父类原型上的方法,实现了完整的继承效果。

七、ES6类继承 extends

ES6(ECMAScript 2015)引入了类的概念,为JavaScript带来了更接近传统面向对象语言的语法糖。

实现方法

在ES6中,类继承通过extends关键字实现,子类可以继承父类的属性和方法,并且可以使用super关键字来调用父类的构造函数和方法。

class Parent {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

class Child extends Parent {
  constructor(name) {//继承父类属性
    super(name);//super里不传参数的话就是把父类里的属性都继承过来
    this.age = 20;
  }
  getAge() {//子类扩展的方法
    return this.age
  }
}

let c = new Child('JiSung');//创建Child实例并
console.log(c.getName())//调用getName方法
console.log(c.getAge())

JavaScript里的七种继承

  • 子类继承了父类的属性和方法,同时还可以添加自己的属性和方法(getAge())。

  • 通过调用super(name),子类的构造函数确保了父类的构造函数被正确调用,从而正确初始化了继承的属性。

  • 子类类可以自由地添加或覆盖属性和方法,以扩展或修改从父类继承的行为。


结语

以上就是本文的全部内容,希望对你了解继承的这几种方式有所帮助,感谢你的阅读!

转载自:https://juejin.cn/post/7393606971928051727
评论
请登录