理解 `Object.getPrototypeOf()` 与 `Object.setPrototypeOf()`

嘿,你懂原型链吗?(Object.getPrototypeOf()Object.setPrototypeOf() 的奇妙冒险)

各位靓仔靓女,晚上好!今天咱们不聊风花雪月,来点硬核的——聊聊 JavaScript 中那个神秘又重要的东西:原型链。而我们要深挖的两个宝藏函数,就是 Object.getPrototypeOf()Object.setPrototypeOf()

别听到“原型链”就头大,觉得枯燥乏味。今天,我会用最通俗易懂、甚至有点幽默的语言,带你走进原型链的奇妙世界,保证你听完之后,不仅知其然,更知其所以然,甚至还能用它们来耍点小花招!😉

1. 故事的开始:一切皆对象

在 JavaScript 的宇宙里,几乎所有东西都是对象。对象就像一个百宝箱,里面装着各种各样的属性和方法。但是,问题来了:每个对象都得自己准备一套吗?那岂不是太浪费资源了?

想象一下,你开了个水果店,卖苹果、香蕉、橘子。难道你要为每个水果都准备一个单独的标签,写上“我是苹果,我可以吃”、“我是香蕉,我可以吃”……? 多累啊!

聪明的你肯定会想到:我做一个通用标签,写上“我是水果,我可以吃”,然后把这个标签贴在每个水果上。这样,每个水果就都有了“可以吃”的属性,省时省力!

原型链,就是 JavaScript 中的那个“通用标签”。

2. 原型:对象的秘密基地

每个对象都有一个秘密基地,藏着一些共享的属性和方法,这个秘密基地就叫做原型(prototype)

我们可以把原型想象成一个模具。当你用这个模具创建一个新的对象时,这个新对象就自动拥有了模具里的所有东西。

举个栗子:

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.`);
};

const person1 = new Person("Alice", 30);
const 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 构造函数的原型。person1person2 都是通过 Person 构造函数创建的对象,它们都共享了 Person.prototype 上的 sayHello 方法。

关键点:

  • 每个函数都有一个 prototype 属性,它指向一个对象,这个对象就是该函数的原型。
  • 通过 new 关键字创建的对象,会继承其构造函数的原型上的属性和方法。

3. 原型链:寻宝之旅

如果对象自己没有某个属性或方法,它会沿着一条链向上查找,直到找到为止。这条链,就叫做原型链(prototype chain)

原型链就像一个寻宝游戏。对象是寻宝者,属性和方法是宝藏,原型链是寻宝路线图。

寻宝规则:

  1. 首先,寻宝者会在自己的百宝箱里寻找宝藏。
  2. 如果没找到,寻宝者会去自己的“爸爸”(原型)那里寻找。
  3. 如果还没找到,寻宝者会去“爸爸的爸爸”(原型的原型)那里寻找。
  4. 以此类推,直到找到宝藏,或者到达原型链的尽头(null)。

图示:

Object (最终原型,`__proto__` 指向 null)
  ^
  |
Person.prototype (继承自 Object.prototype)
  ^
  |
person1 (继承自 Person.prototype)

4. Object.getPrototypeOf():找到你的“爸爸”

Object.getPrototypeOf() 函数就像一个寻亲工具,它可以告诉你某个对象的原型(也就是它的“爸爸”)。

语法:

Object.getPrototypeOf(object)

举个栗子:

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

const prototypeOfPerson1 = Object.getPrototypeOf(person1);

console.log(prototypeOfPerson1 === Person.prototype); // 输出:true

这个例子告诉我们,person1 的原型就是 Person.prototype

应用场景:

  • 调试: 确认对象是否继承自某个原型。
  • 判断类型: 根据原型判断对象的类型。
  • 元编程: 动态修改对象的原型。

5. Object.setPrototypeOf():给对象换个“爸爸”

Object.setPrototypeOf() 函数就像一个“换爸爸”工具,它可以改变某个对象的原型。 (慎用!慎用!慎用!)

语法:

Object.setPrototypeOf(object, prototype)

注意:

  • Object.setPrototypeOf() 会直接修改对象的 __proto__ 属性,这是一个性能敏感的操作,尽量避免使用。
  • 修改原型会影响到所有继承自该原型的对象,可能会导致意外的副作用。

举个栗子(不推荐):

const person1 = new Person("Alice", 30);
const anotherPrototype = {
  greet: function() {
    console.log("Greetings!");
  }
};

Object.setPrototypeOf(person1, anotherPrototype);

person1.greet(); // 输出:Greetings!
person1.sayHello(); // 报错:person1.sayHello is not a function

在这个例子中,我们把 person1 的原型改成了 anotherPrototype。现在,person1 拥有了 anotherPrototype 上的 greet 方法,但失去了 Person.prototype 上的 sayHello 方法。

为什么不推荐使用 Object.setPrototypeOf()

  • 性能问题: 修改原型会触发 JavaScript 引擎的重新优化,导致性能下降。
  • 副作用: 修改原型会影响到所有继承自该原型的对象,可能会导致意外的副作用。
  • 可读性差: 使用 Object.setPrototypeOf() 容易使代码难以理解和维护。

替代方案:

如果需要修改对象的行为,建议使用以下方法:

  • 创建新的类或构造函数。
  • 使用组合(Composition)模式。
  • 使用代理(Proxy)对象。

6. __proto__:原型链的幕后推手

每个对象都有一个 __proto__ 属性(非标准属性,但大多数浏览器都支持),它指向该对象的原型。

__proto__ 属性是原型链的幕后推手,它决定了对象沿着哪条链向上查找属性和方法。

关系:

object.__proto__ === Object.getPrototypeOf(object)

7. 总结:原型链的奥秘

我们来总结一下今天学到的知识:

  • 原型: 每个对象都有一个原型,它是一个包含共享属性和方法的对象。
  • 原型链: 对象沿着原型链向上查找属性和方法。
  • Object.getPrototypeOf() 找到对象的原型(“爸爸”)。
  • Object.setPrototypeOf() 改变对象的原型(“换爸爸”,慎用!)。
  • __proto__ 指向对象的原型,是原型链的幕后推手。

表格:

函数/属性 作用 备注
Object.getPrototypeOf() 获取对象的原型 用于确定对象的原型,进行类型判断,调试等。
Object.setPrototypeOf() 设置对象的原型 慎用! 性能敏感,可能导致副作用,可读性差。建议使用其他方法代替。
prototype 函数的属性,指向该函数的原型对象 用于定义构造函数的原型,所有通过该构造函数创建的对象都会继承该原型上的属性和方法。
__proto__ 对象的属性(非标准),指向该对象的原型 连接对象和其原型的桥梁,原型链的幕后推手。尽管是非标准属性,但在大多数浏览器中都可用。

8. 实例演练:原型链的应用

现在,我们来通过一些实例演练,巩固一下原型链的知识。

例子 1:扩展内置对象

我们可以通过修改内置对象的原型,来扩展其功能。

Array.prototype.unique = function() {
  return [...new Set(this)];
};

const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = numbers.unique();

console.log(uniqueNumbers); // 输出:[1, 2, 3, 4, 5]

在这个例子中,我们给 Array.prototype 添加了一个 unique 方法,用于去除数组中的重复元素。现在,所有数组都可以使用 unique 方法了。

例子 2:实现继承

我们可以使用原型链来实现继承。

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

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 的原型为 Animal 的原型
Dog.prototype.constructor = Dog; // 修正 Dog 的 constructor 属性

Dog.prototype.bark = function() {
  console.log("Woof!");
};

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

dog1.eat(); // 输出:Buddy is eating.
dog1.bark(); // 输出:Woof!

在这个例子中,Dog 继承了 Animaleat 方法,并添加了自己的 bark 方法。

9. 总结的总结:原型链,永不过时

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

虽然 Object.setPrototypeOf() 在某些情况下可以用来修改对象的原型,但我们应该尽量避免使用它,因为它可能会导致性能问题和副作用。

希望通过今天的讲解,你对原型链有了更深入的理解。记住,原型链就像一个寻宝游戏,只要掌握了寻宝规则,你就能找到你想要的宝藏! 💰

感谢大家的聆听!下次有机会,我们再聊聊 JavaScript 的其他有趣话题! 👋

发表回复

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