全面解析JavaScript的继承、构造函数、原型与原型链

150 阅读5分钟

前言

JavaScript是一种基于原型的语言,这意味着对象可以直接从其他对象继承属性和方法,而不是通过类继承。理解JavaScript的继承、构造函数、原型、原型对象和原型链是掌握JavaScript面向对象编程的关键。本文将深入讲解这些概念,并通过示例代码和图示帮助你更好地理解它们。

构造函数

在JavaScript中,构造函数是一种特殊的函数,用于创建和初始化对象。构造函数通常以大写字母开头,以区别于普通函数。使用new关键字调用构造函数时,会创建一个新的对象,并将其this指向该对象。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

const alice = new Person('Alice', 25);
console.log(alice.name); // 输出:Alice
console.log(alice.age);  // 输出:25

在这个例子中,Person是一个构造函数,用于创建Person对象。通过new Person('Alice', 25)创建了一个新的Person实例alice

构造函数的工作原理

当我们使用new关键字调用构造函数时,实际上发生了以下几步:

  1. 创建一个新的空对象。
  2. 将构造函数的prototype属性赋值给新对象的__proto__属性。
  3. 将构造函数内部的this指向新对象。
  4. 执行构造函数的代码。
  5. 返回新对象(除非构造函数显式返回一个对象)。
function Person(name, age) {
  this.name = name;
  this.age = age;
}

const alice = new Person('Alice', 25);

// 等价于
const alice = {};
alice.__proto__ = Person.prototype;
Person.call(alice, 'Alice', 25);

原型和原型对象

每个JavaScript函数都有一个prototype属性,该属性指向一个对象,这个对象称为原型对象。原型对象用于定义所有实例共享的属性和方法。当我们创建一个新的对象实例时,该实例会继承原型对象上的属性和方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 在原型对象上定义方法
Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const alice = new Person('Alice', 25);
alice.sayHello(); // 输出:Hello, my name is Alice

在这个例子中,我们在Person.prototype上定义了一个方法sayHello。所有Person实例都可以访问这个方法,因为它们继承了Person.prototype上的属性和方法。

原型对象的结构

原型对象本身也是一个对象,因此它也有自己的原型。所有对象的最终原型是Object.prototype,而Object.prototype的原型是null

console.log(Person.prototype.__proto__ === Object.prototype); // 输出:true
console.log(Object.prototype.__proto__); // 输出:null

原型链的示意图

以下是一个简单的原型链示意图:

alice -> Person.prototype -> Object.prototype -> null

原型链

原型链是由对象及其原型组成的链条。当我们访问一个对象的属性或方法时,JavaScript引擎会首先查找该对象自身的属性或方法。如果找不到,它会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(即null)。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, my name is ${this.name}`);
};

const alice = new Person('Alice', 25);
alice.sayHello(); // 输出:Hello, my name is Alice

console.log(alice.toString()); // 输出:[object Object]

在这个例子中,alice对象没有toString方法,因此JavaScript引擎会沿着原型链向上查找,最终找到Object.prototype上的toString方法。

原型链的工作原理

当我们访问一个对象的属性或方法时,JavaScript引擎会执行以下步骤:

  1. 查找对象自身是否有该属性或方法。
  2. 如果没有,查找对象的原型(即__proto__)是否有该属性或方法。
  3. 如果还没有,继续沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(即null)。

原型链的示意图

以下是一个原型链的详细示意图:

alice
  |
  v
Person.prototype
  |
  v
Object.prototype
  |
  v
null

继承

JavaScript中的继承可以通过原型链实现。我们可以通过设置一个对象的原型来实现继承。

原型继承

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

// 在Animal的原型上定义方法
Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

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

// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
// 修正构造函数指向
Dog.prototype.constructor = Dog;

// 在Dog的原型上定义方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking`);
};

const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat();  // 输出:Buddy is eating
dog.bark(); // 输出:Buddy is barking

在这个例子中,我们定义了一个Animal构造函数和一个Dog构造函数。通过Object.create方法,我们将Dog.prototype设置为Animal.prototype的一个新对象,从而实现了继承。Dog实例可以访问Animal原型上的方法。

ES6类继承

ES6引入了class关键字,使得继承变得更加简洁和直观。

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

  eat() {
    console.log(`${this.name} is eating`);
  }
}

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

  bark() {
    console.log(`${this.name} is barking`);
  }
}

const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat();  // 输出:Buddy is eating
dog.bark(); // 输出:Buddy is barking

在这个例子中,我们使用class关键字定义了Animal类和Dog类。通过extends关键字,Dog类继承了Animal类。super关键字用于调用父类的构造函数。

继承的示意图

以下是一个继承示意图:

Dog.prototype -> Animal.prototype -> Object.prototype -> null

原型链的性能

在JavaScript中,访问对象属性时会沿着原型链查找,这可能会影响性能。为了提高性能,建议尽量将常用的方法和属性定义在对象自身或其直接原型上,而不是深层次的原型链上。

深入理解原型链

多层继承

我们可以通过多层继承来实现更复杂的对象关系。

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

// 在Animal的原型上定义方法
Animal.prototype.eat = function() {
  console.log(`${this.name} is eating`);
};

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

// 设置原型链
Mammal.prototype = Object.create(Animal.prototype);
// 修正构造函数指向
Mammal.prototype.constructor = Mammal;

// 在Mammal的原型上定义方法
Mammal.prototype.walk = function() {
  console.log(`${this.name} is walking`);
};

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

// 设置原型链
Dog.prototype = Object.create(Mammal.prototype);
// 修正构造函数指向
Dog.prototype.constructor = Dog;

// 在Dog的原型上定义方法
Dog.prototype.bark = function() {
  console.log(`${this.name} is barking`);
};

const dog = new Dog('Buddy', 'Golden Retriever');
dog.eat();  // 输出:Buddy is eating
dog.walk(); // 输出:Buddy is walking
dog.bark(); // 输出:Buddy is barking

在这个例子中,我们通过多层继承实现了更复杂的对象关系。Dog继承自MammalMammal继承自AnimalDog实例可以访问AnimalMammal原型上的方法。

多层继承的示意图

以下是一个多层继承的示意图:

dog
  |
  v
Dog.prototype
  |
  v
Mammal.prototype
  |
  v
Animal.prototype
  |
  v
Object.prototype
  |
  v
null

使用Object.create实现继承

Object.create方法可以创建一个新对象,并将其原型设置为指定的对象。我们可以使用Object.create来实现继承。

const animal = {
  eat() {
    console.log('Eating');
  }
};

const dog = Object.create(animal);
dog.bark = function() {
  console.log('Barking');
};

dog.eat();  // 输出:Eating
dog.bark(); // 输出:Barking

在这个例子中,dog对象的原型是animal,因此dog可以访问animal上的eat方法。

Object.create的示意图

以下是一个使用Object.create实现继承的示意图:

dog
  |
  v
animal
  |
  v
Object.prototype
  |
  v
null

总结

理解JavaScript的继承、构造函数、原型、原型对象和原型链是掌握这门语言的关键。通过原型,JavaScript实现了对象的继承和代码的重用。原型链则是对象查找属性和方法的机制。希望通过本文的讲解,你对JavaScript的这些概念有了更深入的理解。

关键点回顾

  • 构造函数:用于创建和初始化对象。
  • 原型:每个函数都有一个prototype属性,指向一个对象,包含所有实例共享的属性和方法。
  • 原型链:由对象及其原型组成的链条,用于查找属性和方法。
  • 继承:通过原型链实现对象的继承,可以使用原型继承或ES6类继承。

通过这些知识,你可以更好地理解和使用JavaScript的面向对象编程特性。希望你在编写JavaScript代码时,能够像魔法师一样,轻松驾驭继承、构造函数、原型、原型对象和原型链的魔法!

找到具有 2 个许可证类型的类似代码