嘿,你懂原型链吗?(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
构造函数的原型。person1
和 person2
都是通过 Person
构造函数创建的对象,它们都共享了 Person.prototype
上的 sayHello
方法。
关键点:
- 每个函数都有一个
prototype
属性,它指向一个对象,这个对象就是该函数的原型。 - 通过
new
关键字创建的对象,会继承其构造函数的原型上的属性和方法。
3. 原型链:寻宝之旅
如果对象自己没有某个属性或方法,它会沿着一条链向上查找,直到找到为止。这条链,就叫做原型链(prototype chain)。
原型链就像一个寻宝游戏。对象是寻宝者,属性和方法是宝藏,原型链是寻宝路线图。
寻宝规则:
- 首先,寻宝者会在自己的百宝箱里寻找宝藏。
- 如果没找到,寻宝者会去自己的“爸爸”(原型)那里寻找。
- 如果还没找到,寻宝者会去“爸爸的爸爸”(原型的原型)那里寻找。
- 以此类推,直到找到宝藏,或者到达原型链的尽头(
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
继承了 Animal
的 eat
方法,并添加了自己的 bark
方法。
9. 总结的总结:原型链,永不过时
原型链是 JavaScript 中一个非常重要的概念,理解原型链对于编写高质量的 JavaScript 代码至关重要。
虽然 Object.setPrototypeOf()
在某些情况下可以用来修改对象的原型,但我们应该尽量避免使用它,因为它可能会导致性能问题和副作用。
希望通过今天的讲解,你对原型链有了更深入的理解。记住,原型链就像一个寻宝游戏,只要掌握了寻宝规则,你就能找到你想要的宝藏! 💰
感谢大家的聆听!下次有机会,我们再聊聊 JavaScript 的其他有趣话题! 👋