引言
JavaScript 是一种面向对象的编程语言,在实现面向对象编程时,继承是一个非常重要的概念。JavaScript 中的继承方式有多种,每种方式都有其优缺点和适用场景。在本文中,我们将介绍 JavaScript 中常用的几种继承方式,帮助您更好地理解和应用面向对象编程的相关概念。
原型链继承
通过让一个对象的原型指向另一个对象来实现继承。子类的原型对象是父类的实例,因此可以继承父类的属性和方法。但是,父类的引用类型属性会被子类实例共享,容易造成修改的混淆。
- 代码举例:
function Animal() {
this.species = 'animal';
}
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype = new Animal();
let cat1 = new Cat('Kitty', 'white');
console.log(cat1.species); // 输出:'animal'
在原型链继承中,通过创建一个父类的实例,并将其赋值给子类的原型对象,实现了子类对父类原型属性和方法的继承。
在上述代码中,我们创建了一个 Animal 构造函数,并给它的原型对象添加了一个 species 属性。然后,我们创建了一个 Cat 构造函数,并将其原型对象设置为一个 Animal 的实例。这样,Cat 的实例cat1就可以通过原型链访问到 Animal 原型对象上的 species 属性,输出了 ‘animal’
优缺点
- 优点:
- 实现简单,容易理解
- 可以通过原型链访问父类的方法和属性。
- 缺点:
- 无法向父类传递参数。
- 子类实例共享父类引用类型属性,容易造成修改污染。
构造函数继承
通过在子类构造函数中调用父类构造函数来实现继承。子类的实例可以拥有自己的属性和方法,且不会共享父类的引用类型属性。但是,无法继承父类原型对象中的方法
- 代码举例:
function Animal(name) {
this.name = name;
}
function Cat(name, color) {
Animal.call(this, name);
this.color = color;
}
let cat1 = new Cat('Kitty', 'white');
console.log(cat1.name); // 输出:'Kitty'
在构造函数继承中,通过在子类的构造函数中调用父类的构造函数,并使用 call 或 apply 方法改变 this 的指向,实现了子类对父类属性的继承。
在上述代码中,我们创建了一个 Animal 构造函数,其中包含一个 name 属性,然后,我们创建了一个 Cat 构造函数,通过调用父类构造函数,将父类的 name 属性传递给子类,并添加了一个 color 属性。这样,Cat 的实例cat1就可以访问到 name 和 color 属性,输出了 ‘Kitty’。
优缺点
- 优点:
- 可以继承父类的属性。
- 不会共享父类引用类型属性。
- 缺点:
- 无法继承父类的方法。
- 子类实例无法访问父类原型上的属性和方法。
组合继承
结合原型链继承和构造函数继承的优点,既可以继承父类的属性和方法,又可以拥有自己的属性和方法,且不会共享父类的引用类型属性。
- 代码举例:
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
};
function Cat(name, color) {
Animal.call(this, name);
this.color = color;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
let cat1 = new Cat('Kitty', 'white');
console.log(cat1.name); // 输出:'Kitty'
cat1.sayName(); // 输出:'Kitty'
在组合继承中,通过在子类构造函数中调用父类构造函数,实现了子类对父类属性的继承,并将子类的原型对象设置为一个父类的实例,实现了子类对父类方法的继承。
在上述代码中,我们创建了一个 Animal 构造函数,其中包含一个 name 属性和一个 sayName 方法,然后,我们创建了一个 Cat 构造函数,通过调用父类构造函数,将父类的 name 属性传递给子类,并添加了一个 color 属性。接着,我们将 Cat 的原型对象设置为一个 Animal 的实例,并将其 constructor 属性设置为 Cat。这样,Cat 的实例cat1就可以访问到 name 和 color 属性,也可以访问到父类原型对象上的 sayName 方法,输出了 ‘Kitty’。
优缺点
- 优点:
- 可以继承父类的属性和方法。
- 不会共享父类引用类型属性。
- 可以向父类传递参数。
- 缺点:
- 会调用两次父类构造函数,一次在子类构造函数中,一次在子类原型上,造成性能浪费。
原型式继承
利用已有的对象创建一个新的对象,新对象的原型指向已有对象,新对象可以拥有已有对象的属性和方法。与原型链继承类似,会共享引用类型属性。
- 代码举例:
let animal = {
species: 'animal',
};
let cat1 = Object.create(animal, {
name: {
value: 'Kitty',
},
color: {
value: 'white',
},
});
console.log(cat1.species); // 输出:'animal'
console.log(cat1.name); // 输出:'Kitty'
console.log(cat1.color); // 输出:'white'
在原型式继承中,通过 Object.create 方法创建一个新对象,并将其原型对象设置为一个已有的对象,实现了继承。
在上述代码中,我们创建了一个 animal 对象,其中包含一个 species 属性。然后,我们通过 Object.create 方法创建了一个 cat1 对象,并将其原型对象设置为 animal 对象。接着,我们使用 Object.defineProperties 方法向 cat1 对象添加了 name 和 color 属性。这样,cat1 对象就可以访问到 species、name 和 color 属性,输出了 ‘animal’、‘Kitty’ 和 ‘white’。
优缺点
- 优点:
- 可以快速创建一个对象,该对象可以继承已有对象的属性和方法。
- 缺点:
- 与原型链继承一样,子类实例共享父类引用类型属性,容易造成修改污染。
寄生式继承
在原型式继承的基础上,通过在新对象上添加方法来实现继承。可以在不影响原有对象的基础上对其进行扩展。但是,同样会共享引用类型属性。
- 代码举例:
let animal = {
species: 'animal'
};
function createCat(obj) {
let cat = Object.create(obj);
cat.catchMouse = function() {
console.log(`${this.name} is catching a mouse`);
};
return cat;
}
let cat1 = createCat(animal);
cat1.name = 'Kitty';
cat1.catchMouse(); // 输出:'Kitty is catching a mouse'
优缺点
- 优点:
- 可以在已有对象的基础上进行扩展,同时又不希望影响到原有对象。
- 缺点:
- 与原型链继承和原型式继承一样,子类实例共享父类引用类型属性,容易造成修改污染。
寄生组合式继承
在组合继承的基础上,优化了原型链继承和构造函数继承的缺点,通过借用构造函数继承属性,同时通过原型链继承方法。可以继承父类的属性和方法,同时不会共享父类的引用类型属性。
- 代码举例:
function Animal(name) {
this.name = name;
this.colors = ['white', 'black'];
}
Animal.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
}
function Cat(name, color) {
Animal.call(this, name);
this.color = color;
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
let cat1 = new Cat('Kitty', 'white');
let cat2 = new Cat('Tom', 'black');
cat1.colors.push('brown');
console.log(cat1.colors); // 输出:['white', 'black', 'brown']
console.log(cat2.colors); // 输出:['white', 'black']
在寄生组合式继承中,通过 Object.create 方法创建一个新对象,并将其原型对象设置为父类的原型对象,实现了子类对父类方法的继承。
在上述代码中,我们创建了一个 Animal 构造函数,其中包含一个 name 属性和一个 sayName 方法,然后,我们创建了一个 Cat 构造函数,通过调用父类构造函数,将父类的 name 属性传递给子类,并添加了一个 color 属性。接着,我们将 Cat 的原型对象设置为一个 Animal.prototype 对象的副本,并将其 constructor 属性设置为 Cat。这样,Cat 的实例cat1就可以访问到 name 和 color 属性,也可以访问到父类原型对象上的 sayName 方法,输出了 ‘Kitty’。
优缺点
- 优点:
- 可以继承父类的属性和方法,也不会共享父类引用类型属性。
- 不会调用两次父类构造函数,性能优于组合继承。
- 缺点:
- 实现复杂,需要手动设置子类原型的 constructor 属性为子类构造函数。
Class继承
在 ES6 中,我们可以使用 class 关键字来定义一个类,使用 extends 关键字来实现继承。ES6 中的继承方式称为类继承,具有以下特点:
- 使用 class 关键字定义一个类:
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
- 使用 extends 关键字实现继承:
class Cat extends Animal {
constructor(name, color) {
super(name);
this.color = color;
}
}
在上述代码中,我们创建了一个 Animal 类,并定义了一个构造函数和一个方法。然后,我们创建了一个 Cat 类,并通过 extends 关键字将其继承自 Animal 类。在 Cat 的构造函数中,我们通过 super 关键字调用了父类的构造函数,并将父类的 name 属性传递给子类。接着,我们添加了一个 color 属性。这样,Cat 类就继承了 Animal 类的属性和方法,同时也添加了自己的属性。
- 使用 super 关键字调用父类的方法:
class Cat extends Animal {
constructor(name, color) {
super(name);
this.color = color;
}
sayName() {
super.sayName();
console.log(`My color is ${this.color}`);
}
}
在上述代码中,我们重写了 Cat 类中的 sayName 方法,并使用 super 关键字调用了父类的 sayName 方法。这样,在调用 Cat 类的 sayName 方法时,我们既可以输出名字,也可以输出颜色。
最后
以上就是js继承的一些方法,每种继承方式都有其优缺点和适用场景,我们需要根据具体的情况选择合适的继承方式。在实际开发中,我们通常会使用 ES6 中的类继承方式,它更加直观、易于理解和维护。通过理解和应用不同的继承方式,我们可以更好地实现面向对象编程,并编写出更加高效、简洁和可维护的代码。