详细阐述 JavaScript 原型 (Prototype) 和原型链 (Prototype Chain) 的工作原理,并说明如何基于原型实现继承。

各位观众老爷,大家好!今天咱们不聊风花雪月,就聊聊 JavaScript 里一个既神秘又强大的概念:原型和原型链。 别害怕,听起来高大上,其实理解了之后,你会发现它们就像你家楼下的小卖部一样亲切。 准备好了吗?咱们这就开始!

1. 什么是原型 (Prototype)?

想象一下,你想要创建一个“人”的对象。每个人都有名字、年龄,还会说话。如果你每次都手动写一遍这些属性和方法,那简直就是程序员的噩梦。 这时候,原型就闪亮登场了。

原型,简单来说,就是 JavaScript 函数(包括构造函数)自带的一个属性。 这个属性本身也是一个对象,就像一个模板或者蓝图,它定义了所有由这个函数创建的对象可以共享的属性和方法。

我们可以用一张表格来形象地表示一下:

属性/方法 说明
prototype 每个函数(Function)都有的属性,它指向一个对象。这个对象就是该函数的原型对象。
__proto__ 每个对象都有的属性(注意,是对象,不是函数!),它指向创建该对象的构造函数的原型对象。 注意:这个属性虽然广泛存在,但并不推荐直接使用它。建议使用 Object.getPrototypeOf()Object.setPrototypeOf() 方法。
constructor 原型对象 (prototype) 默认自带的属性,它指向创建该原型对象的构造函数。 比如 Person.prototype.constructor === Person

举个例子:

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

Person.prototype.sayHello = function() {
  console.log("Hello, my name is " + this.name + ", and I'm " + this.age + " years old.");
};

let person1 = new Person("Alice", 30);
let person2 = new Person("Bob", 25);

person1.sayHello(); // 输出: Hello, my name is Alice, and I'm 30 years old.
person2.sayHello(); // 输出: Hello, my name is Bob, and I'm 25 years old.

在这个例子中,Person.prototype 就是 Person 构造函数的原型对象。 sayHello 方法被添加到 Person.prototype 上。 这意味着 person1person2 这两个对象都可以访问 sayHello 方法,而不需要在每个对象实例上都定义一遍。 这就像共享单车一样,大家都用,但不需要每个人都自己造一辆。

2. 原型链 (Prototype Chain): 寻根溯源的旅程

现在,我们已经知道了原型是什么,那么原型链又是什么呢? 想象一下,你要找你的爷爷,但是你只知道你爸爸的电话。 你先打电话给你爸爸,问你爷爷的电话,然后你爸爸再告诉你。 这就是原型链的思想。

当你在一个对象上访问一个属性或方法时,JavaScript 引擎会按照一定的顺序进行查找:

  1. 首先,它会在对象自身的属性中查找。 如果找到了,就返回该属性的值或执行该方法。

  2. 如果没有找到,它会沿着 __proto__ 属性(或者使用 Object.getPrototypeOf() 方法)去该对象的原型对象中查找。

  3. 如果在原型对象中找到了,就返回该属性的值或执行该方法。

  4. 如果仍然没有找到,它会继续沿着原型对象的 __proto__ 属性去原型对象的原型对象中查找,以此类推,直到找到 null 为止。 null 是原型链的终点,意味着再往上就没有原型对象了。

这个查找的过程,就像一条链子一样,连接着对象和它的原型对象,以及原型对象的原型对象,直到 null。 这条链子,就是原型链

让我们用代码来演示一下:

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

Animal.prototype.eat = function() {
  console.log(this.name + " is eating.");
};

function Dog(name, breed) {
  Animal.call(this, name); // 调用 Animal 构造函数,继承 name 属性
  this.breed = breed;
}

// 设置 Dog 的原型为 Animal 的实例
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修正 constructor 指向

Dog.prototype.bark = function() {
  console.log(this.name + " is barking.");
};

let dog1 = new Dog("Buddy", "Golden Retriever");

dog1.bark(); // 输出: Buddy is barking.  (Dog.prototype)
dog1.eat();  // 输出: Buddy is eating. (Animal.prototype)
console.log(dog1.name); // 输出: Buddy (dog1 自身属性)

// 沿着原型链查找:
// dog1.__proto__  === Dog.prototype
// Dog.prototype.__proto__ === Animal.prototype
// Animal.prototype.__proto__ === Object.prototype
// Object.prototype.__proto__ === null

在这个例子中,dog1 对象可以访问 bark 方法(定义在 Dog.prototype 上)和 eat 方法(定义在 Animal.prototype 上),以及 name 属性(定义在 Animal 构造函数中并通过 call 方法继承)。 这就是原型链的威力。

3. 基于原型实现继承

原型链是实现继承的关键。 JavaScript 中没有像 Java 或 C++ 那样的 class 关键字(虽然 ES6 引入了 class,但它本质上还是基于原型的语法糖)。 JavaScript 的继承是通过原型链来实现的。

简单来说,要实现 A 继承 B,我们需要做以下几件事:

  1. 创建一个新的对象,并将该对象的原型设置为 B 的实例。 这样做,A 的原型链上就有了 B,A 就可以访问 B 的属性和方法了。

  2. 修正 constructor 指向。 因为我们把 A 的原型指向了 B 的实例,所以 A 的原型对象的 constructor 属性会指向 B。 我们需要把它修正回指向 A。

  3. 在 A 的原型上添加 A 特有的属性和方法。

让我们用代码来演示一下:

// 父类
function Animal(name) {
  this.name = name;
}

Animal.prototype.eat = function() {
  console.log(this.name + " is eating.");
};

// 子类
function Dog(name, breed) {
  Animal.call(this, name); // 继承 Animal 的属性
  this.breed = breed;
}

// 关键步骤:设置 Dog 的原型为 Animal 的实例
Dog.prototype = Object.create(Animal.prototype);  // 使用 Object.create 避免调用 Animal 构造函数
Dog.prototype.constructor = Dog; // 修正 constructor 指向

Dog.prototype.bark = function() {
  console.log(this.name + " is barking.");
};

let dog1 = new Dog("Buddy", "Golden Retriever");

dog1.bark(); // 输出: Buddy is barking.
dog1.eat();  // 输出: Buddy is eating.
console.log(dog1 instanceof Dog);   // true
console.log(dog1 instanceof Animal);  // true
console.log(dog1 instanceof Object);  // true

在这个例子中,我们使用 Object.create(Animal.prototype) 来创建一个新的对象,并将该对象的原型设置为 Animal.prototype。 这样做的好处是,避免了调用 Animal 构造函数,从而避免了在 Dog.prototype 上创建不必要的属性。

4. 几种常见的继承方式

除了上面演示的原型链继承,还有一些其他的继承方式,各有优缺点:

继承方式 优点 缺点 代码示例
原型链继承 简单易懂 共享原型对象的属性,修改子类的原型会影响到父类;创建子类实例时无法向父类构造函数传递参数。 javascript function Animal(name) { this.name = name; } Animal.prototype.eat = function() { console.log(this.name + " is eating."); }; function Dog(name, breed) { this.breed = breed; } Dog.prototype = new Animal(); // 继承 Animal Dog.prototype.constructor = Dog; // 修正 constructor let dog1 = new Dog("Buddy", "Golden Retriever"); dog1.eat(); // Buddy is eating.
构造函数继承 可以向父类构造函数传递参数;解决了原型链继承中共享原型对象的问题。 只能继承父类的属性,不能继承父类原型对象的属性和方法;每次创建子类实例时,都要调用父类构造函数,效率较低。 javascript function Animal(name) { this.name = name; } Animal.prototype.eat = function() { console.log(this.name + " is eating."); }; function Dog(name, breed) { Animal.call(this, name); // 继承 Animal 的属性 this.breed = breed; } let dog1 = new Dog("Buddy", "Golden Retriever"); console.log(dog1.name); // Buddy // dog1.eat(); // TypeError: dog1.eat is not a function
组合继承 结合了原型链继承和构造函数继承的优点,既可以继承父类的属性,又可以继承父类原型对象的属性和方法;可以向父类构造函数传递参数。 需要调用两次父类构造函数,一次是在设置原型时,一次是在创建子类实例时,效率相对较低;子类原型和实例中都存在父类的属性,造成一定的浪费。 javascript function Animal(name) { this.name = name; } Animal.prototype.eat = function() { console.log(this.name + " is eating."); }; function Dog(name, breed) { Animal.call(this, name); // 继承 Animal 的属性 this.breed = breed; } Dog.prototype = new Animal(); // 继承 Animal Dog.prototype.constructor = Dog; // 修正 constructor let dog1 = new Dog("Buddy", "Golden Retriever"); dog1.eat(); // Buddy is eating. console.log(dog1.name); // Buddy
原型式继承 不需要创建构造函数,直接基于现有对象创建一个新对象。 共享原型对象的属性,修改子类会影响到父类;继承的本质是复制,而不是继承关系。 javascript function object(o) { function F() {} F.prototype = o; return new F(); } let animal = { name: "Animal", eat: function() { console.log(this.name + " is eating."); } }; let dog1 = object(animal); dog1.name = "Buddy"; dog1.eat(); // Buddy is eating.
寄生式继承 在原型式继承的基础上,增强对象的功能。 共享原型对象的属性,修改子类会影响到父类;继承的本质是复制,而不是继承关系;增强对象的功能会导致代码难以维护。 javascript function object(o) { function F() {} F.prototype = o; return new F(); } function createDog(name) { let o = object({name: name, eat: function(){ console.log(this.name + " is eating.")}} ); o.bark = function() { console.log(this.name + " is barking."); }; return o; } let dog1 = createDog("Buddy"); dog1.bark(); // Buddy is barking. dog1.eat() // Buddy is eating.
寄生组合式继承 综合了构造函数继承和原型式继承的优点,是目前比较理想的继承方式。 代码相对复杂。 javascript function Animal(name) { this.name = name; } Animal.prototype.eat = function() { console.log(this.name + " is eating."); }; function Dog(name, breed) { Animal.call(this, name); // 继承 Animal 的属性 this.breed = breed; } Dog.prototype = Object.create(Animal.prototype); // 继承 Animal Dog.prototype.constructor = Dog; // 修正 constructor Dog.prototype.bark = function() { console.log(this.name + " is barking."); }; let dog1 = new Dog("Buddy", "Golden Retriever"); dog1.eat(); // Buddy is eating. dog1.bark(); // Buddy is barking.

5. instanceof 运算符

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

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

function Dog(name, breed) {
  Animal.call(this, name);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

let dog1 = new Dog("Buddy", "Golden Retriever");

console.log(dog1 instanceof Dog);   // true
console.log(dog1 instanceof Animal);  // true
console.log(dog1 instanceof Object);  // true

dog1 instanceof Dog 返回 true,因为 Dog.prototype 出现在 dog1 的原型链上。

dog1 instanceof Animal 返回 true,因为 Animal.prototype 也出现在 dog1 的原型链上。

dog1 instanceof Object 返回 true,因为 Object.prototype 是所有对象的最终原型。

6. hasOwnProperty() 方法

hasOwnProperty() 方法用于检测对象自身是否具有指定的属性,不查找原型链。

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

Animal.prototype.eat = function() {
  console.log(this.name + " is eating.");
};

let animal1 = new Animal("Generic Animal");

console.log(animal1.hasOwnProperty("name")); // true,name 是 animal1 自身的属性
console.log(animal1.hasOwnProperty("eat"));  // false,eat 是 Animal.prototype 上的属性

7. 总结

原型和原型链是 JavaScript 中非常重要的概念,理解它们对于编写高质量的 JavaScript 代码至关重要。

  • 原型 (Prototype): 每个函数都有一个 prototype 属性,指向一个对象,该对象是该函数创建的所有对象的原型。

  • 原型链 (Prototype Chain): 当访问一个对象的属性或方法时,如果对象自身没有该属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端 null

  • 继承: JavaScript 的继承是通过原型链来实现的。 通过将一个构造函数的原型设置为另一个构造函数的实例,可以实现继承。

希望今天的讲座能帮助你更好地理解 JavaScript 的原型和原型链。 记住,熟能生巧,多写代码,多练习,你也能成为原型链大师!

下次有机会,我们再聊聊其他的 JavaScript 话题。 感谢各位的观看!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注