上两节理清了 prototype 与 proto 的区别和联系,现在是时候把它们串成一条完整的“属性查找流水线”了。原型链的查找机制用一句话概括就是:就近原则,逐级上溯,碰到 null 结束。
一条链路,四个参与者
假设我们有以下代码:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log('Hi, I am ' + this.name);
};
const p1 = new Person('Alice');
此时 p1 的原型链可以这样画出:
p1 → p1.__proto__ → Person.prototype → Object.prototype → null
当访问 p1.name 时:
- 引擎首先在
p1自身属性中查找,发现有name,直接返回'Alice'。
当访问 p1.sayHi 时:
- 在
p1自身属性中查找,没有找到sayHi。 - 沿着
p1.proto进入Person.prototype,在它的属性中查找,找到sayHi方法,返回。 - 查找停止,不再继续向上。
当访问 p1.toString 时:
p1自身没有toString。Person.prototype也没有toString。- 继续向上,进入
Person.prototype.proto,也就是Object.prototype,在这里找到了toString方法,返回。
当访问一个完全不存在的属性 p1.unknown 时:
- 引擎逐级向上查找,直至到达
Object.prototype。 Object.prototype.proto为null,查找结束,返回undefined,不会报错。
这个机制带来的实用启示
1. 属性屏蔽(遮蔽)
如果实例自身有一个与原型链上同名的属性,那么访问时一定会命中自身的,原型链上的同名属性暂时“隐身”,这就是属性屏蔽。调试时可以通过 hasOwnProperty 方法确认某个属性到底属于哪个层级。
2. 修改原型影响所有实例
因为每次访问都是“运行时”查找,如果在运行时向 Person.prototype 上添加一个新方法,那么所有已经创建的 Person 实例都会立即能使用这个方法。这种动态性是原型模型的强大之处,但也需要谨慎使用,避免造成不可预期的行为。
3. 过长的原型链会拖慢查找
虽然现代引擎对原型链查找做了大量优化(如内联缓存),理论上每次属性访问都要逐级遍历,深层嵌套仍然会带来可感知的开销。在设计对象继承关系时,尽量保持链路扁平,不要人为制造过深的层次。
4. Object.create(null) 的妙用
如果完全不希望一个对象有原型链(比如用作纯数据字典),可以用 Object.create(null) 创建。这样的对象连 toString、hasOwnProperty 都没有,查找时完全不会遍历到 Object.prototype,既干净又略有性能优势。
原型链查找机制是整个 JavaScript 对象模型最核心的运转规则。当你理解了“从哪儿开始找、按什么顺序找、什么时候停”,那么构造函数、继承、instanceof 运算符等等背后的行为,都会变得透明而自然。