自学内容网 自学内容网

JavaScript 原型链的工作原理及如何实现继承

在 JavaScript 中,原型链继承是对象系统的核心概念。它们通过原型机制让对象之间能够共享属性和方法,从而实现代码复用和对象的灵活扩展。

1. 什么是原型(Prototype)?

每个 JavaScript 对象在创建时都会拥有一个内部链接,指向另一个对象(称为原型对象)。这个原型对象包含了该对象可以继承的属性和方法。原型的存在使得对象可以共享一些通用的行为。

在 JavaScript 中,可以通过 __proto__(或在 ES6 中的 Object.getPrototypeOfObject.setPrototypeOf)来访问或设置一个对象的原型。

2. 什么是原型链?

JavaScript 中的对象通过原型链来实现继承。当我们访问一个对象的属性或方法时,如果该对象本身没有该属性或方法,JavaScript 引擎会沿着该对象的原型链向上查找,直到找到该属性或方法,或到达原型链的顶端(即 null)为止。

原型链的工作过程如下:

  1. 当访问一个对象属性时,JavaScript 引擎会先查找对象自身的属性。
  2. 如果对象本身没有该属性,则会沿着原型链逐级向上查找。
  3. 如果在整个原型链中都找不到该属性,JavaScript 会返回 undefined
function Animal() {}
Animal.prototype.sayHello = function() {
  console.log("Hello from Animal");
};

const animal = new Animal();
animal.sayHello(); // 输出 "Hello from Animal"

// 查找过程:animal -> Animal.prototype -> Object.prototype -> null

在上例中,animal 的原型是 Animal.prototype,而 Animal.prototype 的原型是 Object.prototype,这形成了一条原型链 animal -> Animal.prototype -> Object.prototype -> null

3. 实现继承的方式

JavaScript 提供了多种实现继承的方式。以下是几种常见的方法:

3.1 构造函数继承(组合继承)

在传统的构造函数继承中,我们通过在子类的构造函数中调用父类构造函数来实现继承。

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

Animal.prototype.sayHello = function() {
  console.log("Hello, I am " + this.name);
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数
  this.breed = breed;
}

// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 重新设置构造函数引用

const dog = new Dog("Buddy", "Golden Retriever");
dog.sayHello(); // 输出 "Hello, I am Buddy"

这里的步骤如下:

  1. Animal.call(this, name); 在子类构造函数中调用父类构造函数,使得 name 被绑定在 Dog 实例上。
  2. Dog.prototype = Object.create(Animal.prototype); 创建 Dog.prototype 并将其链接到 Animal.prototype,从而实现继承。
  3. Dog.prototype.constructor = Dog;constructor 指向子类构造函数 Dog,避免指向 Animal
3.2 ES6 的 class 继承(语法糖)

ES6 引入了 class 语法,使得继承的实现更加直观。它其实是对原型链和构造函数继承的一层封装,依然基于原型链。

class Animal {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log("Hello, I am " + this.name);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }

  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.sayHello(); // 输出 "Hello, I am Buddy"
dog.bark(); // 输出 "Woof! Woof!"

Dog 类中,super(name); 调用了父类 Animal 的构造函数,保证了 name 被正确赋值。ES6 的继承方式更简洁,推荐在现代项目中使用。

3.3 使用 Object.create() 实现原型继承

Object.create() 可以基于一个现有对象创建新的对象,并将该对象作为新对象的原型。这种方法简单灵活,但不支持构造函数传参。

const animal = {
  sayHello() {
    console.log("Hello from animal");
  }
};

const dog = Object.create(animal);
dog.bark = function() {
  console.log("Woof! Woof!");
};

dog.sayHello(); // 输出 "Hello from animal"
dog.bark(); // 输出 "Woof! Woof!"

4. 原型链继承的缺点

原型链继承虽然简单,但存在一些缺点:

  1. 引用类型共享:所有实例共享父类原型的属性和方法。如果原型包含引用类型属性,任何实例修改该属性都会影响其他实例。
  2. 构造函数无法传参:无法在实例化子类时传递参数给父类构造函数。
  3. 效率问题:随着原型链层级增加,查找属性的性能会有所降低。

示例:共享引用类型的属性(第一个缺点)

我们先来看一个演示,展示原型链继承会导致实例共享引用类型属性的问题。

// 基类 Animal
function Animal(name) {
  this.name = name;
  this.friends = []; // 引用类型属性
}

Animal.prototype.addFriend = function(friend) {
  this.friends.push(friend);
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数
  this.breed = breed;
}

// 通过原型链继承 Animal
Dog.prototype = new Animal(); 
Dog.prototype.constructor = Dog;

// 创建 Dog 类的实例
const dog1 = new Dog("Buddy", "Golden Retriever");
const dog2 = new Dog("Max", "Bulldog");

// 向 dog1 添加朋友
dog1.addFriend("Charlie");

console.log(dog1.friends); // 输出 ["Charlie"]
console.log(dog2.friends); // 输出 ["Charlie"],因为它们共享同一个 friends 数组

5. 原型链和继承的应用场景

原型链和继承在 JavaScript 中被广泛应用于:

  • 代码复用:在不同对象间共享通用功能,避免重复编写代码。
  • 面向对象设计:将功能封装在类或对象中,通过继承扩展功能。
  • DOM 原型链:JavaScript 中的 DOM 对象如 HTMLElementNode 等都具有原型链关系,通过原型链进行方法继承。

下边给一个demo 来描述使用场景

假设我们有一个动物园系统,我们需要管理不同类型的动物。所有动物有一些共通的属性和方法(如名称、叫声),但是不同种类的动物可能有不同的行为。我们可以通过继承来实现这一点。

// 基类 Animal
function Animal(name) {
  this.name = name; // 动物的名字
}

Animal.prototype.makeSound = function() {
  console.log(this.name + " makes a sound");
};

// 派生类 Dog,继承自 Animal
function Dog(name, breed) {
  Animal.call(this, name); // 调用父类构造函数,初始化 Dog 的 name 属性
  this.breed = breed; // 狗的品种
}

// 通过 Object.create 创建一个继承自 Animal.prototype 的 Dog.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修正构造函数的指向

// 添加 Dog 特有的方法
Dog.prototype.bark = function() {
  console.log(this.name + " barks: Woof! Woof!");
};

// 派生类 Cat,继承自 Animal
function Cat(name, color) {
  Animal.call(this, name); // 调用父类构造函数,初始化 Cat 的 name 属性
  this.color = color; // 猫的颜色
}

// 通过 Object.create 创建一个继承自 Animal.prototype 的 Cat.prototype
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat; // 修正构造函数的指向

// 添加 Cat 特有的方法
Cat.prototype.meow = function() {
  console.log(this.name + " meows: Meow! Meow!");
};

// 创建 Dog 和 Cat 的实例
const dog = new Dog("Buddy", "Golden Retriever");
const cat = new Cat("Whiskers", "Tabby");

// 使用父类和子类的方法
dog.makeSound(); // 输出 "Buddy makes a sound"
dog.bark(); // 输出 "Buddy barks: Woof! Woof!"

cat.makeSound(); // 输出 "Whiskers makes a sound"
cat.meow(); // 输出 "Whiskers meows: Meow! Meow!"

工作原理:

  • DogCat 类都通过 Object.create(Animal.prototype) 来继承 Animal 类的方法。这种方式确保了每个实例都可以访问父类的方法,并且通过在子类中添加特有的方法,实现多态和行为扩展。
  • 每个动物实例(如 dogcat)都能调用 makeSound 方法,这是从父类 Animal 继承来的。同时,DogCat 类还各自有各自的特有方法,如 barkmeow

优点:

  1. 代码复用:通过继承,DogCat 类共享了 Animal 类的 makeSound 方法,避免了重复编写相同的功能。
  2. 扩展性:如果将来需要添加新的动物类型(如 Bird),可以继承自 Animal 类并添加特有的行为,代码维护和扩展变得非常简便。
  3. 多态性:虽然 DogCat 类继承了 makeSound 方法,但是它们仍然可以各自拥有不同的行为方法(如 barkmeow),体现了多态的特性。

总结:

通过原型链和继承,我们可以实现一个灵活的面向对象系统,使得不同对象共享通用功能的同时,又能根据需求拓展各自的特性。这种设计不仅提高了代码的复用性,还让程序更容易扩展和维护。

总结

  • 原型链是 JavaScript 中实现继承的基础,沿着原型链查找属性和方法。
  • 继承实现有多种方式,经典的有组合继承、ES6 class 继承和 Object.create()
  • 注意原型链的缺点,在实际开发中选择合适的继承方式,现代项目中通常优先使用 class 语法糖。

通过掌握这些知识,可以深入理解 JavaScript 的对象系统,合理地实现代码复用和扩展。


原文地址:https://blog.csdn.net/yq_dxp1218/article/details/143771392

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!