JavaScript里的七种继承
前言
继承,简单来讲就是让子类的实例能够访问到父类上的属性,在JavaScript中能够实现实现继承的方式有多种,本文主要介绍到的有七种
一、原型链继承
在JavaScript里,原型链继承是基于原型对象实现的一种继承方式。每个JavaScript对象在其创建时都会有一个内部属性[[Prototype]](原型),这个属性链接到另一个对象,当尝试访问该对象上不存在的属性或方法时,JavaScript引擎会沿着这条链(原型链)向上查找,直到找到相应的属性或方法为止。
实现方式
假设我们有如下两个构造函数Person
和Student
:
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()
这里,Student
的prototype
被设置为new Person()
的实例,所以所有的Student
实例都能访问Person
原型上的sayHello
方法。
缺点
-
内存共享:由于
Student.prototype
是一个Person
实例,所有Student
实例都共享Person.prototype
上的属性。如果
Person.prototype
上有可变的引用类型属性(像数组或对象),那么所有Student
实例会共享这些引用,导致实例之间的数据可能互相影响。
例如:
Person.prototype.favorites = [];
student1.favorites.push('math');
console.log(student2.favorites); // ['math'] - 这是student1喜欢的科目,不是student2
- 构造函数被篡改:在上面的例子中,
Student.prototype.constructor
现在指向的是Person
,而不是Student
。
构造器(constructor) 用来记录这个对象是由谁创建出来的
二、构造函数继承
构造函数继承允许子类实例从父类构造函数继承属性,但不继承方法。
这种方法解决了原型链继承中所有实例共享同一份原型对象的问题,从而避免了实例间的相互影响。
但是它也带来了一些限制,最主要的是子类实例不能继承到父类的原型。
实现方式
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
实例相同的属性,但每个实例都有自己的一份拷贝,因此它们之间不会相互影响。
缺点
- 不能访问父类原型上的方法:由于没有使用原型链,子类实例无法访问父类原型上的任何方法。这意味着如果你在父类的原型上定义了一个方法,子类实例将无法使用这个方法。
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
student1.sayHello(); // TypeError: student1.sayHello is not a function
三、组合继承
原型链继承+构造函数继承,
子类实例继承父类的属性,并且能够通过原型链访问父类的方法
实现步骤
- 构造函数继承父类的属性:在子类构造函数中调用父类构造函数,使用
call
或者apply
方法,将父类的属性复制到子类实例上。 - 原型链继承父类的方法:设置子类的
prototype
属性为父类的一个新实例,这样子类就能够访问到父类原型上的方法。 - 修正构造函数指针:在第二步中,子类的
prototype
成为了父类的一个实例,子类prototype
的constructor
指向了父类构造函数,需要手动指定子类的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)//输出继承到的父类属性
缺点
- 父类构造函数被调用两次:一次是在子类构造函数中,另一次是在设置子类原型时。这会产生多余的性能开销。
- 创建子类实例时的额外开销:父类构造函数被调用了两次,每次都会创建一个新的实例,即使第二次调用并没有真正使用这个实例,也会增加内存和CPU的负担。
四、原型式继承
原型式继承是一种直接基于对象的继承方式,不涉及构造函数或原型链,而是直接从一个对象继承属性和方法。
这种继承方式的核心是使用Object.create()
方法,它接收一个对象作为参数,并返回一个新对象,这个新对象的原型就是传入的对象。
实现方式
let parent = {
name: 'JiSung',
age: 12
}
let child = Object.create(parent);//凭空创建一个新对象,并让这个新对象继承到parent里的属性
子对象的隐式原型里面有父对象里的所有属性和方法
缺点
引用类型的属性会被所有实例共享:多个实例之间继承到的引用类型是相同的地址,会相互影响。
let parent = {
name: 'JiSung',
age: 12
}
let child2 = Object.create(parent);
child2.like.push(3)
console.log(child2.like);
五、寄生式继承
寄生式继承基于原型式继承的基础上进行了改进。
在原型式继承中,一个新对象是基于另一个对象创建的,但所有实例共享原型对象上的属性,尤其是引用类型属性,这可能导致数据污染。
寄生式继承则通过创建一个仅用来继承的函数,然后在返回新对象之前,对这个新对象进行增强,以此确保每个实例都有其独立的属性副本,可以让子对象默认具有自己的属性。
实现步骤
- 创建一个函数,这个函数的目的是为了创建一个新对象。
- 借助
Object.create()
方法,以某个对象作为原型创建新对象。 - 在这个新对象上添加方法或属性。
- 返回这个新对象
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);
缺点
-
和原型式继承一样,因为是浅拷贝,所以多个实例之间继承到的引用类型是相同的地址,会相互影响。
-
无法传递参数
六、寄生组合继承
寄生组合继承结合了原型链继承和构造函数继承的优点,同时避免了它们各自的缺点。
寄生组合继承的目标是避免父类构造函数的多次调用,并确保每个子类实例都能够正确地继承父类的属性和方法,而不会共��引用类型属性
实现步骤:
- 创建一个不作任何事情的临时构造函数:这个构造函数用于继承父类的属性,但不会执行父类构造函数中的任何代码。
- 使用
Object.create()
方法:使用临时构造函数的原型来创建子类的原型,这样子类就能继承父类原型上的方法。 - 修正构造函数指针:确保子类的
prototype.constructor
指向子类本身,而不是临时构造函数。 - 在子类构造函数中调用父类构造函数:使用
call
或apply
方法调用父类构造函数,以便每个子类实例都能获得父类的属性。
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())
-
子类继承了父类的属性和方法,同时还可以添加自己的属性和方法(getAge())。
-
通过调用
super(name)
,子类的构造函数确保了父类的构造函数被正确调用,从而正确初始化了继承的属性。 -
子类类可以自由地添加或覆盖属性和方法,以扩展或修改从父类继承的行为。
结语
以上就是本文的全部内容,希望对你了解继承的这几种方式有所帮助,感谢你的阅读!
转载自:https://juejin.cn/post/7393606971928051727