原型链(Prototype Chain):属性查找的路径
引言
嘿,大家好!今天咱们来聊聊JavaScript中的一个非常重要的概念——原型链(Prototype Chain)。如果你已经对JavaScript有所了解,那么你一定知道它是一个基于对象的语言。每个对象都有一个隐藏的属性,指向它的“原型”对象。而这个原型对象本身也有自己的原型,如此层层递进,就形成了所谓的“原型链”。
听起来有点绕是不是?别担心,接下来我会用轻松诙谐的语言,结合代码示例,带你一步步理解这个概念。相信我,等你看完这篇文章,原型链对你来说将不再是神秘的存在!
1. 对象与原型
在JavaScript中,几乎所有的东西都是对象。你可以把对象想象成一个装满了各种属性和方法的“盒子”。比如:
let person = {
  name: "Alice",
  age: 25,
  sayHello: function() {
    console.log("Hi, I'm " + this.name);
  }
};
person.sayHello(); // 输出: Hi, I'm Alice
在这个例子中,person 是一个对象,它有 name、age 和 sayHello 这三个属性。sayHello 是一个方法,也就是函数。
但是,你知道吗?person 这个对象其实并不是“孤零零”的存在。它有一个隐藏的属性,叫做 [[Prototype]],指向它的原型对象。这个原型对象也是一些属性和方法的集合。
2. __proto__ 与 prototype
在JavaScript中,我们可以通过 __proto__ 属性来访问对象的原型。比如:
console.log(person.__proto__); // 输出: Object.prototype
Object.prototype 是所有普通对象的默认原型。它包含了一些常用的方法,比如 toString()、hasOwnProperty() 等等。
需要注意的是,__proto__ 并不是标准的属性,它是浏览器提供的一个便捷方式。更正式的做法是使用 Object.getPrototypeOf() 方法:
console.log(Object.getPrototypeOf(person)); // 输出: Object.prototype
另外,当你创建一个函数时,函数本身也有一个 prototype 属性。这个 prototype 属性是一个对象,它会被用作通过该函数创建的对象的原型。比如:
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHello = function() {
  console.log("Hi, I'm " + this.name);
};
let alice = new Person("Alice", 25);
alice.sayHello(); // 输出: Hi, I'm Alice
在这里,alice 的 __proto__ 指向了 Person.prototype,而 Person.prototype 的 __proto__ 又指向了 Object.prototype。这就形成了一个原型链。
3. 原型链的工作原理
现在我们知道了对象有一个 __proto__ 属性,指向它的原型对象。那么,当我们在对象上查找属性时,JavaScript 是如何工作的呢?
答案是:沿着原型链逐层查找。
假设我们有以下代码:
let obj1 = {
  a: 1
};
let obj2 = {
  b: 2
};
obj2.__proto__ = obj1;
console.log(obj2.a); // 输出: 1
console.log(obj2.b); // 输出: 2
在这个例子中,obj2 的 __proto__ 指向了 obj1。当我们访问 obj2.a 时,JavaScript 会首先检查 obj2 是否有 a 属性。如果没有,它会继续沿着 __proto__ 查找,直到找到 a 属性为止。最终,它在 obj1 中找到了 a,因此输出 1。
如果我们继续沿着原型链查找,最终会到达 Object.prototype,而 Object.prototype 的 __proto__ 是 null,表示原型链的终点。
4. 原型链的继承
原型链的一个重要应用就是继承。通过原型链,我们可以让一个对象继承另一个对象的属性和方法。这在面向对象编程中非常有用。
比如,我们可以定义一个基类 Animal,然后让 Dog 继承 Animal:
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function() {
  console.log(this.name + " makes a noise.");
};
function Dog(name) {
  Animal.call(this, name); // 调用父类的构造函数
}
// 让 Dog 继承 Animal
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
  console.log(this.name + " barks.");
};
let dog = new Dog("Rex");
dog.speak(); // 输出: Rex barks.
在这里,Dog 继承了 Animal 的 speak 方法,并且覆盖了它。当我们调用 dog.speak() 时,JavaScript 会优先查找 Dog.prototype 中的 speak 方法,而不是 Animal.prototype 中的。
5. 原型链的性能问题
虽然原型链非常强大,但它也有一些潜在的性能问题。由于每次访问属性时,JavaScript 都需要沿着原型链逐层查找,如果链过长,可能会导致性能下降。
举个极端的例子:
let obj1 = { a: 1 };
let obj2 = { b: 2 };
let obj3 = { c: 3 };
obj2.__proto__ = obj1;
obj3.__proto__ = obj2;
console.log(obj3.a); // 输出: 1
在这个例子中,obj3 的 __proto__ 指向 obj2,obj2 的 __proto__ 指向 obj1。因此,当我们访问 obj3.a 时,JavaScript 需要经过两层查找才能找到 a 属性。如果链再长一些,性能问题就会更加明显。
为了避免这种情况,尽量保持原型链的长度适中,不要过度嵌套。
6. 如何打断原型链
有时候,我们可能不希望某个对象继承其原型上的某些属性或方法。这时,我们可以使用 Object.create(null) 来创建一个没有原型的对象:
let obj = Object.create(null);
console.log(obj.__proto__); // 输出: undefined
这样创建的对象没有任何原型,因此也不会继承任何属性或方法。
7. 总结
好了,今天的讲座到这里就差不多了!让我们回顾一下今天学到的内容:
- 原型链 是JavaScript中对象之间的一种继承机制。
 - 每个对象都有一个 
__proto__属性,指向它的原型对象。 - 当我们在对象上查找属性时,JavaScript 会沿着原型链逐层查找,直到找到属性或到达链的终点。
 - 原型链可以用于实现继承,但要注意避免过长的链,以免影响性能。
 - 我们可以通过 
Object.create(null)创建一个没有原型的对象。 
希望今天的讲解对你有所帮助!如果你有任何问题,欢迎随时提问。下次见! ?
引用:
- MDN Web Docs: JavaScript is a prototype-based language, which means that objects can inherit properties and methods from other objects through the prototype chain.
 - ECMAScript Specification: The [[Prototype]] internal slot of an object is either null or an object and is used for implementing inheritance.