JavaScript原型与原型链

113 阅读2分钟

先修知识:当JavaScript遇上面向对象

在ES6之前,JavaScript没有真正的class关键字,但这并不妨碍它成为一门优秀的面向对象语言。让我们先看一个经典的构造函数示例:

function Person(name, age) {
  this.name = name;
  this.age = age;
  function greet() {
  console.log(`Hello, my name is ${this.name}`);
 }
}
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

此时每个实例都会创建独立的greet方法,损耗了性能和内存。这种设计显然不够优雅,于是我们需要原型来拯救世界。

什么是原型?

在 JavaScript 中,几乎所有的对象都有一个隐藏的内部属性 [[Prototype]],它指向了另一个对象,称为该对象的原型。每个对象都能访问到它的原型对象上的属性和方法。

prototype 属性

每个函数(包括构造函数)都自带一个 prototype 属性,指向一个包含实例共享方法和属性的对象。构造函数通过 prototype 来定义所有实例共享的方法。


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

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

const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

person1.greet(); // Hello, my name is Alice
person2.greet(); // Hello, my name is Bob

在上面的例子中,greet 方法通过 Person.prototype 被所有 Person 的实例共享。这样,person1person2 不需要分别拥有自己的 greet 方法,从而节省了内存。

什么是原型链?

原型链是 JavaScript 中对象继承的机制。当我们访问一个对象的属性或方法时,如果这个对象本身没有该属性或方法,JavaScript 会沿着原型链向上查找,直到找到该属性或者到达原型链的末端(即 Object.prototype)。

  • 对象上的属性__proto__就相当于原型链上的一个链接点。

原型链的结构

假设我们有以下对象结构:

  • person1Person 构造函数创建的一个实例。
  • Person.prototypePerson 构造函数的原型对象。
  • Object.prototype 是所有 JavaScript 对象的最终原型。

所以,person1 的原型链是这样的:

person1 -> Person.prototype -> Object.prototype -> null

当我们访问 person1.greet() 时,JavaScript 会按照以下顺序查找:

  1. 首先查找 person1 是否有 greet 方法,没有。
  2. 然后查找 Person.prototype 是否有 greet 方法,找到了。
  3. 调用 Person.prototype.greet 方法。

__proto__prototype 的区别

  • __proto__ 是每个实例对象内部的一个属性,保存着该实例对象的构造函数的prototype
  • prototype 是每个函数的属性,它指向构造函数的原型对象。

console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true

实例化过程与原型链

当我们使用 new 运算符创建一个对象时,发生的事情是:

  1. 创建一个新的空对象。
  2. 将这个对象的 __proto__ 属性指向构造函数的 prototype 属性。
  3. 执行构造函数中的代码,将属性和方法添加到对象中。
  4. 返回这个对象。
function Person(name, age) {
  this.name = name;
  this.age = age;
}

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

const person1 = new Person('Alice', 30);

// 创建过程:
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.__proto__ === Object.prototype); // true

在上面的例子中,person1__proto__ 指向了 Person.prototype,而 Person.prototype__proto__ 指向了 Object.prototype,直到最后找到 null

image.png

如何判断属性来自原型链?

我们可以使用 hasOwnPropertyin 来判断一个属性是对象自身的,还是来自原型链。

  • hasOwnProperty 只检查对象自身的属性,不会检查原型链上的属性。
  • in 运算符会检查对象自身和原型链上的属性。

const person = new Person('Alice', 30);

console.log(person.hasOwnProperty('name')); // true
console.log(person.hasOwnProperty('greet')); // false

console.log('greet' in person); // true

在上面的代码中,nameperson 自己的属性,所以 hasOwnProperty('name') 返回 true。而 greet 是通过 Person.prototype 定义的原型方法,因此 hasOwnProperty('greet') 返回 false,但是 greet 存在于 person 的原型链中,所以 'greet' in person 返回 true