继承与扩展
是什么
JavaScript 中的继承是一种允许一个对象获取另一个对象的属性和方法的机制。这是实现代码重用和创建基于现有对象的新对象的一种方式。在 ES5 及之前的版本中,继承通常是通过原型链实现的,而在 ES6 中,引入了 class 关键字,让基于类的继承看起来更像是传统面向对象编程语言中的继承。
原型链继承(ES5 及之前)
在 ES5 及之前的版本中,所有的 JavaScript 对象都有一个内置的属性,叫做原型(prototype)。每个函数都有一个 prototype 属性,这个属性是一个指向对象的指针,而这个对象包含可以由特定类型的所有实例共享的属性和方法。当试图访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,解释器就会去其原型对象上查找,如果原型对象上也没有,就会继续查找原型对象的原型,依此类推,形成了所谓的“原型链”。
function Parent() {
this.parentAttribute = "I am the parent";
}
Parent.prototype.parentMethod = function () {
return "This method is on the parent!";
};
function Child() {
Parent.call(this); // 继承属性
}
Child.prototype = Object.create(Parent.prototype); // 继承方法
Child.prototype.constructor = Child; // 修复构造函数指向
var childInstance = new Child();
console.log(childInstance.parentAttribute); // 输出: I am the parent
console.log(childInstance.parentMethod()); // 输出: This method is on the parent!
类继承(ES6+)
ES6 引入了 class 语法,使得基于类的继承更加直观和易于理解。尽管在底层,JavaScript 仍然使用原型链来实现继承,但新的语法提供了一种更清晰、更传统的方式来创建和继承对象。
class Parent {
constructor() {
this.parentAttribute = "I am the parent";
}
parentMethod() {
return "This method is on the parent!";
}
}
class Child extends Parent {
constructor() {
super(); // 调用父类的 constructor
}
}
let childInstance = new Child();
console.log(childInstance.parentAttribute); // 输出: I am the parent
console.log(childInstance.parentMethod()); // 输出: This method is on the parent!
在 ES6+ 的类继承中,关键字 extends
用于创建一个子类,super()
被用来调用父类的构造函数。
继承是面向对象编程的一个核心概念,它在 JavaScript 中同样是一个重要的概念,无论是采用原型链还是类继承的方式。
知识点
JavaScript 中继承的知识点可以从多个角度来理解,包括原型链继承的基本概念、ES6 类继承的语法,以及与继承相关的高级概念和模式。下面是一些关键的知识点:
原型链
原型对象(Prototype):
- 每个 JavaScript 对象都有一个原型对象,从中继承方法和属性。
- 函数的
prototype
属性是一个对象,通过new
调用函数时,新对象会继承这个prototype
对象上的属性。
原型链(Prototype Chain):
- 查找属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找。
- 原型链的末端通常是
Object.prototype
,所有对象默认继承自Object.prototype
。
构造函数(Constructor):
- 用来创建对象的特殊函数。
- 通过
new
关键字调用时,构造函数内的this
关键字会指向新创建的对象。
ES6 类继承
类定义(Class Definition):
class
关键字用来定义一个类。- 类中的
constructor
方法是创建和初始化类创建的对象的特殊方法。
继承和扩展(Extends and Super):
extends
关键字用于在类声明或类表达式中创建一个类作为另一个类的子类。super
关键字用于访问和调用一个对象的父对象上的函数。
静态方法和属性(Static Methods and Properties):
static
关键字用于定义类的静态方法。- 静态方法通常用于实现不特定于实例的功能。
- 静态属性(ES2022 引入)是直接绑定到类构造函数的属性,而不是绑定到原型对象。
高级概念和模式
原型继承模式:
- 使用
Object.create()
创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。 - 可以创建一个纯粹的继承链,没有构造函数的复杂性。
- 使用
组合继承:
- 结合了原型链继承和构造函数继承。
- 属性通常在构造函数中定义,方法则继承自原型链。
寄生组合继承:
- 优化了组合继承,解决了两次调用父类构造函数的问题(一次在创建子类原型时,一次在子类构造函数中)。
类的多态性:
- 子类可以重写继承自父类的方法,这是多态的一种表现。
- 通过
super
关键字可以调用父类被重写的方法。
封装和私有属性:
- 类可以使用封装来限制对其内部属性和方法的直接访问。
- ES2022 引入的私有字段(使用
#
前缀)和私有方法提供了更好的封装。
Mixin:
- 一种向类添加多个行为的模式,而不是通过传统的继承链。
- 可以通过将方法添加到类的原型,或者通过类表达式创建混入。
理解和掌握这些知识点对于成为一个熟练的 JavaScript 开发者至关重要。它们不仅涉及到基本的语法和概念,还包括了更深层次的设计模式和最佳实践。
继承方式
JavaScript 中实现继承的方式有多种,以下是一些常见的继承方式及示例:
- 原型链继承
使用原型链实现继承,子类型的原型是父类型的一个实例。
- 优点:
- 简单易懂。
- 可以实现方法和属性的共享,节省内存。
- 缺点:
- 来自原型的所有属性都是共享的,一个实例对属性的修改会影响所有实例。
- 创建子类实例时,不能向父类构造函数传递参数。
function Parent() {
this.parentProperty = true;
}
Parent.prototype.getParentProperty = function () {
return this.parentProperty;
};
function Child() {
this.childProperty = false;
}
// 继承父类型
Child.prototype = new Parent();
var child = new Child();
console.log(child.getParentProperty()); // true
- 构造函数继承
通过在子类型构造函数的内部调用父类型构造函数实现继承,可以传递参数。
- 优点:
- 子类可以向父类构造函数传递参数。
- 解决了原型链继承中父类引用属性被所有实例共享的问题。
- 缺点:
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法实现函数复用。
- 只能继承父类的实例属性和方法,不能继承原型属性/方法。
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}
var child = new Child("Child Name");
console.log(child.name); // Child Name
- 组合继承
结合原型链继承和构造函数继承,即在原型上定义方法实现重用,在构造函数中定义属性。
- 优点:
- 结合了两种模式的优点,可以传递参数,且方法可以被复用。
- 既继承了父类的实例属性和方法,也继承了父类原型上的属性和方法。
- 缺点:
- 父类构造函数会被调用两次,一次是设置子类原型的时候,一次是在子类构造函数内部调用,可能导致效率低下和冗余。
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; // 修正 constructor 指向
Child.prototype.sayAge = function () {
console.log(this.age);
};
var child = new Child("Child Name", 18);
child.sayName(); // Child Name
child.sayAge(); // 18
- 原型式继承
利用 Object.create
创建一个新对象,以一个现有对象作为新创建对象的原型。
var parent = {
name: "Parent Name",
colors: ["red", "blue", "green"],
};
var child = Object.create(parent);
child.name = "Child Name";
console.log(child.name); // Child Name
- 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回对象。
- 优点:
- 灵活,可以对继承来的对象进行增强。
- 缺点:
- 不能实现函数复用,每个实例都有自己的方法。
function createAnother(original) {
var clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function () {
// 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
var person = {
name: "Person Name",
friends: ["Shelby", "Court", "Van"],
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
- 寄生组合式继承
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
- 优点:
- 解决了组合继承调用两次父类构造函数的问题,更高效。
- 可以实现函数复用。
- 既继承了父类的实例属性和方法,也继承了父类原型上的属性和方法。
- 缺点:
- 实现较为复杂。
function inheritPrototype(childObj, parentObj) {
var prototype = Object.create(parentObj.prototype); // 创建对象
prototype.constructor = childObj; // 增强对象
childObj.prototype = prototype; // 指定对象
}
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;
}
inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
console.log(this.age);
};
var child = new Child("Child Name", 18);
child.sayName(); // Child Name
child.sayAge(); // 18
- ES6 类继承
使用 class
和 extends
关键字实现继承。
- 优点
- 语法简洁直观,更接近传统面向对象编程的写法。
- 提供了 class、extends、super 等关键字,使得结构清晰,继承机制更加明确。
- 支持静态方法。
- 缺点
- ES6 类实际上是原型继承的语法糖,底层仍然使用原型链,需要理解原型链的工作原理。
- 在旧版本浏览器中需要通过编译转换才能使用
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的 constructor(name)
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
const child = new Child("Child Name", 18);
child.sayName(); // Child Name
child.sayAge(); // 18
这些是实现继承的一些基本方式,每种方式都有其用途和适用场景。在实际开发中,根据具体需求选择合适的继承方式是非常重要的。
使用场景
继承是面向对象编程中的一个核心概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。JavaScript 中的继承主要用于以下场景:
- 代码重用
当多个类共享相同的属性或方法时,可以将这些共通的部分提取到一个父类中,然后通过继承来重用这些代码。
示例:定义一个 Vehicle
类作为所有交通工具的基类,然后通过继承来创建 Car
和 Bike
类。
class Vehicle {
constructor(brand) {
this.brand = brand;
}
start() {
console.log("Starting the vehicle...");
}
}
class Car extends Vehicle {
constructor(brand, model) {
super(brand);
this.model = model;
}
display() {
console.log(`Car: ${this.brand} ${this.model}`);
}
}
class Bike extends Vehicle {
constructor(brand, type) {
super(brand);
this.type = type;
}
display() {
console.log(`Bike: ${this.brand} ${this.type}`);
}
}
let myCar = new Car("Toyota", "Corolla");
myCar.start(); // Starting the vehicle...
myCar.display(); // Car: Toyota Corolla
let myBike = new Bike("Trek", "Mountain Bike");
myBike.start(); // Starting the vehicle...
myBike.display(); // Bike: Trek Mountain Bike
- 多态性
继承允许子类重写父类的方法,这意味着同一个方法可以根据对象的不同而表现出不同的行为。
示例:Animal
类有一个 speak
方法,不同的动物类(如 Dog
和 Cat
)继承 Animal
类并重写 speak
方法。
class Animal {
speak() {
console.log("The animal makes a sound.");
}
}
class Dog extends Animal {
speak() {
console.log("The dog barks.");
}
}
class Cat extends Animal {
speak() {
console.log("The cat meows.");
}
}
let dog = new Dog();
dog.speak(); // The dog barks.
let cat = new Cat();
cat.speak(); // The cat meows.
- 抽象和封装
通过继承,可以创建抽象的基类,它定义了子类必须实现的方法。这有助于封装复杂性,并且只公开必要的接口。
示例:Shape
类定义了一个抽象方法 draw
,它必须在派生类中实现。
class Shape {
draw() {
throw new Error("This method should be implemented by subclasses.");
}
}
class Circle extends Shape {
draw() {
console.log("Drawing a circle...");
}
}
class Square extends Shape {
draw() {
console.log("Drawing a square...");
}
}
let circle = new Circle();
circle.draw(); // Drawing a circle...
let square = new Square();
square.draw(); // Drawing a square...
- 创建框架或库时的扩展性
当创建一个框架或库时,可以通过继承提供一个基本的实现,允许用户扩展和定制功能。
示例:一个简单的 UI 组件库,允许用户通过继承基本组件来创建自定义组件。
class Component {
constructor(id) {
this.id = id;
}
render() {
console.log(`Rendering component with id: ${this.id}`);
}
}
class Button extends Component {
constructor(id, label) {
super(id);
this.label = label;
}
render() {
super.render();
console.log(`Rendering a button with label: ${this.label}`);
}
}
let button = new Button("btn-1", "Submit");
button.render();
// Rendering component with id: btn-1
// Rendering a button with label: Submit
继承在 JavaScript 中的这些使用场景展示了它如何帮助组织和结构化代码,同时提高了代码的可维护性和可扩展性。
当然,除了上述场景外,继承还有其他的使用场景,如:
- 处理类似对象的集合
当需要对一组具有共同属性和方法的对象进行操作时,继承可以确保所有对象都遵循相同的接口。
示例:假设你有一个游戏,其中有多种类型的游戏角色,每个角色都有共同的属性和方法。
class GameCharacter {
constructor(name, health) {
this.name = name;
this.health = health;
}
attack(target) {
console.log(`${this.name} attacks ${target.name}!`);
}
}
class Knight extends GameCharacter {
constructor(name, health, armor) {
super(name, health);
this.armor = armor;
}
}
class Archer extends GameCharacter {
constructor(name, health, range) {
super(name, health);
this.range = range;
}
}
let knight = new Knight("Arthur", 100, 50);
let archer = new Archer("Robin", 80, 100);
knight.attack(archer); // Arthur attacks Robin!
archer.attack(knight); // Robin attacks Arthur!
- 创建插件式架构
在设计可扩展的系统时,继承可以用来定义插件的基本行为,允许第三方开发者通过继承来创建新的插件。
示例:一个文本编辑器,可以通过插件来扩展其功能。
class Plugin {
constructor(editorInstance) {
this.editorInstance = editorInstance;
}
apply() {
// Default implementation
}
}
class SpellCheckerPlugin extends Plugin {
apply() {
console.log("Checking spelling...");
// Specific implementation for spell checking
}
}
class AutoSavePlugin extends Plugin {
apply() {
console.log("Saving document...");
// Specific implementation for auto-saving
}
}
// Assuming `editor` is an instance of some editor that the plugins are augmenting
let spellChecker = new SpellCheckerPlugin(editor);
let autoSaver = new AutoSavePlugin(editor);
spellChecker.apply(); // Checking spelling...
autoSaver.apply(); // Saving document...
- 原型继承和动态语言特性
在 JavaScript 这样的动态语言中,继承可以用来动态地改变对象的行为,通过改变对象的原型链来引入新的行为或者覆盖现有的行为。
示例:动态地改变对象的行为。
let animal = {
eat() {
console.log("The animal is eating.");
},
};
let rabbit = {
jump() {
console.log("The rabbit is jumping.");
},
};
// rabbit 继承自 animal
Object.setPrototypeOf(rabbit, animal);
rabbit.eat(); // The animal is eating.
rabbit.jump(); // The rabbit is jumping.
// 动态改变 rabbit 的行为
rabbit.eat = function () {
console.log("The rabbit is eating a carrot.");
};
rabbit.eat(); // The rabbit is eating a carrot.
- 遵循设计模式
许多设计模式,如工厂模式、建造者模式、策略模式等,都涉及到继承。这些模式利用继承来实现对象的创建、行为的定义和算法的封装。
示例:使用工厂模式来创建不同类型的 UI 控件。
class UIControl {
render() {
throw new Error("You must implement the render method.");
}
}
class ButtonControl extends UIControl {
render() {
console.log("Rendering a button");
}
}
class CheckBoxControl extends UIControl {
render() {
console.log("Rendering a checkbox");
}
}
class UIControlFactory {
createControl(type) {
switch (type) {
case "button":
return new ButtonControl();
case "checkbox":
return new CheckBoxControl();
default:
throw new Error("Unknown control type");
}
}
}
let factory = new UIControlFactory();
let button = factory.createControl("button");
let checkbox = factory.createControl("checkbox");
button.render(); // Rendering a button
checkbox.render(); // Rendering a checkbox
继承的使用场景非常广泛,它在不同的编程范式和模式中都有着重要的作用。以上示例只是冰山一角,实际应用时,继承的使用会更加多样化和复杂。