Skip to content

继承与扩展

是什么

JavaScript 中的继承是一种允许一个对象获取另一个对象的属性和方法的机制。这是实现代码重用和创建基于现有对象的新对象的一种方式。在 ES5 及之前的版本中,继承通常是通过原型链实现的,而在 ES6 中,引入了 class 关键字,让基于类的继承看起来更像是传统面向对象编程语言中的继承。

原型链继承(ES5 及之前)

在 ES5 及之前的版本中,所有的 JavaScript 对象都有一个内置的属性,叫做原型(prototype)。每个函数都有一个 prototype 属性,这个属性是一个指向对象的指针,而这个对象包含可以由特定类型的所有实例共享的属性和方法。当试图访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,解释器就会去其原型对象上查找,如果原型对象上也没有,就会继续查找原型对象的原型,依此类推,形成了所谓的“原型链”。

javascript
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 仍然使用原型链来实现继承,但新的语法提供了一种更清晰、更传统的方式来创建和继承对象。

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 类继承的语法,以及与继承相关的高级概念和模式。下面是一些关键的知识点:

原型链

  1. 原型对象(Prototype)

    • 每个 JavaScript 对象都有一个原型对象,从中继承方法和属性。
    • 函数的 prototype 属性是一个对象,通过 new 调用函数时,新对象会继承这个 prototype 对象上的属性。
  2. 原型链(Prototype Chain)

    • 查找属性时,如果对象本身没有这个属性,JavaScript 引擎会沿着原型链向上查找。
    • 原型链的末端通常是 Object.prototype,所有对象默认继承自 Object.prototype
  3. 构造函数(Constructor)

    • 用来创建对象的特殊函数。
    • 通过 new 关键字调用时,构造函数内的 this 关键字会指向新创建的对象。

ES6 类继承

  1. 类定义(Class Definition)

    • class 关键字用来定义一个类。
    • 类中的 constructor 方法是创建和初始化类创建的对象的特殊方法。
  2. 继承和扩展(Extends and Super)

    • extends 关键字用于在类声明或类表达式中创建一个类作为另一个类的子类。
    • super 关键字用于访问和调用一个对象的父对象上的函数。
  3. 静态方法和属性(Static Methods and Properties)

    • static 关键字用于定义类的静态方法。
    • 静态方法通常用于实现不特定于实例的功能。
    • 静态属性(ES2022 引入)是直接绑定到类构造函数的属性,而不是绑定到原型对象。

高级概念和模式

  1. 原型继承模式

    • 使用 Object.create() 创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__
    • 可以创建一个纯粹的继承链,没有构造函数的复杂性。
  2. 组合继承

    • 结合了原型链继承和构造函数继承。
    • 属性通常在构造函数中定义,方法则继承自原型链。
  3. 寄生组合继承

    • 优化了组合继承,解决了两次调用父类构造函数的问题(一次在创建子类原型时,一次在子类构造函数中)。
  4. 类的多态性

    • 子类可以重写继承自父类的方法,这是多态的一种表现。
    • 通过 super 关键字可以调用父类被重写的方法。
  5. 封装和私有属性

    • 类可以使用封装来限制对其内部属性和方法的直接访问。
    • ES2022 引入的私有字段(使用 # 前缀)和私有方法提供了更好的封装。
  6. Mixin

    • 一种向类添加多个行为的模式,而不是通过传统的继承链。
    • 可以通过将方法添加到类的原型,或者通过类表达式创建混入。

理解和掌握这些知识点对于成为一个熟练的 JavaScript 开发者至关重要。它们不仅涉及到基本的语法和概念,还包括了更深层次的设计模式和最佳实践。

继承方式

JavaScript 中实现继承的方式有多种,以下是一些常见的继承方式及示例:

  1. 原型链继承

使用原型链实现继承,子类型的原型是父类型的一个实例。

  • 优点:
    • 简单易懂。
    • 可以实现方法和属性的共享,节省内存。
  • 缺点:
    • 来自原型的所有属性都是共享的,一个实例对属性的修改会影响所有实例。
    • 创建子类实例时,不能向父类构造函数传递参数。
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
  1. 构造函数继承

通过在子类型构造函数的内部调用父类型构造函数实现继承,可以传递参数。

  • 优点:
    • 子类可以向父类构造函数传递参数。
    • 解决了原型链继承中父类引用属性被所有实例共享的问题。
  • 缺点:
    • 方法都在构造函数中定义,每次创建实例都会创建一遍方法,无法实现函数复用。
    • 只能继承父类的实例属性和方法,不能继承原型属性/方法。
javascript
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
  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.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
  1. 原型式继承

利用 Object.create 创建一个新对象,以一个现有对象作为新创建对象的原型。

javascript
var parent = {
  name: "Parent Name",
  colors: ["red", "blue", "green"],
};

var child = Object.create(parent);
child.name = "Child Name";
console.log(child.name); // Child Name
  1. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式增强对象,最后返回对象。

  • 优点:
    • 灵活,可以对继承来的对象进行增强。
  • 缺点:
    • 不能实现函数复用,每个实例都有自己的方法。
javascript
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
  1. 寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

  • 优点:
    • 解决了组合继承调用两次父类构造函数的问题,更高效。
    • 可以实现函数复用。
    • 既继承了父类的实例属性和方法,也继承了父类原型上的属性和方法。
  • 缺点:
    • 实现较为复杂。
javascript
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
  1. ES6 类继承

使用 classextends 关键字实现继承。

  • 优点
    • 语法简洁直观,更接近传统面向对象编程的写法。
    • 提供了 class、extends、super 等关键字,使得结构清晰,继承机制更加明确。
    • 支持静态方法。
  • 缺点
    • ES6 类实际上是原型继承的语法糖,底层仍然使用原型链,需要理解原型链的工作原理。
    • 在旧版本浏览器中需要通过编译转换才能使用
javascript
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 中的继承主要用于以下场景:

  1. 代码重用

当多个类共享相同的属性或方法时,可以将这些共通的部分提取到一个父类中,然后通过继承来重用这些代码。

示例:定义一个 Vehicle 类作为所有交通工具的基类,然后通过继承来创建 CarBike 类。

javascript
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
  1. 多态性

继承允许子类重写父类的方法,这意味着同一个方法可以根据对象的不同而表现出不同的行为。

示例Animal 类有一个 speak 方法,不同的动物类(如 DogCat)继承 Animal 类并重写 speak 方法。

javascript
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.
  1. 抽象和封装

通过继承,可以创建抽象的基类,它定义了子类必须实现的方法。这有助于封装复杂性,并且只公开必要的接口。

示例Shape 类定义了一个抽象方法 draw,它必须在派生类中实现。

javascript
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...
  1. 创建框架或库时的扩展性

当创建一个框架或库时,可以通过继承提供一个基本的实现,允许用户扩展和定制功能。

示例:一个简单的 UI 组件库,允许用户通过继承基本组件来创建自定义组件。

javascript
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 中的这些使用场景展示了它如何帮助组织和结构化代码,同时提高了代码的可维护性和可扩展性。

当然,除了上述场景外,继承还有其他的使用场景,如:

  1. 处理类似对象的集合

当需要对一组具有共同属性和方法的对象进行操作时,继承可以确保所有对象都遵循相同的接口。

示例:假设你有一个游戏,其中有多种类型的游戏角色,每个角色都有共同的属性和方法。

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!
  1. 创建插件式架构

在设计可扩展的系统时,继承可以用来定义插件的基本行为,允许第三方开发者通过继承来创建新的插件。

示例:一个文本编辑器,可以通过插件来扩展其功能。

javascript
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...
  1. 原型继承和动态语言特性

在 JavaScript 这样的动态语言中,继承可以用来动态地改变对象的行为,通过改变对象的原型链来引入新的行为或者覆盖现有的行为。

示例:动态地改变对象的行为。

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.
  1. 遵循设计模式

许多设计模式,如工厂模式、建造者模式、策略模式等,都涉及到继承。这些模式利用继承来实现对象的创建、行为的定义和算法的封装。

示例:使用工厂模式来创建不同类型的 UI 控件。

javascript
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

继承的使用场景非常广泛,它在不同的编程范式和模式中都有着重要的作用。以上示例只是冰山一角,实际应用时,继承的使用会更加多样化和复杂。

hancenter808@outlook.com