先记住这个答案
当执行 obj.prop 时,JavaScript 先查询 obj 自身的属性描述符。若不存在,则通过 Object.getPrototypeOf(obj) 取得原型并再次查找,如此逐级上溯。该查找遵循就近原则:第一个匹配的属性值会被返回,因此实例上的属性会遮蔽原型上的同名属性。继承的函数在调用时 this 绑定到当前实例,而非原型对象。如果没有属性匹配且原型链尽头是 null,表达式值为 undefined。注意查找是运行时行为,原型变更会立即影响结果。
- 查找先自身后原型,找到即停。
- 原型链末端是 null,未找到返回 undefined。
- 实例属性会遮蔽原型同名属性。
从自身属性到原型链逐级查找的运行时过程
obj.prop 的求值由内部 [[Get]] 实现。规范第一步是取得对象自身的属性描述符,若存在则调用其 value 或 getter 并返回。否则将对象替换为其原型,即 Object.getPrototypeOf(obj) 的结果,重复上述步骤。每个普通对象都有原型,除 Object.create(null) 创建的裸对象外,原型链最终会指向 Object.prototype,而它的原型是 null。
查找不会因一次不匹配而跳过某一层;一旦在某一层发现同名属性,无论它是可枚举、可写还是纯符号,都会立刻返回,后续层不再参与。若遇到属性描述符为 undefined(访问器不存在 getter),返回值也是 undefined。需要了解的是,in 运算符同样沿这条链判断存在性,但 for...in 会枚举链上的可枚举属性。
构造函数实例中的遮蔽与 this 绑定
考虑一个表单构造器 FormField,其原型定义了 validate()。当某个特定实例需要额外的校验逻辑时,直接给该实例赋值一个函数。由于赋值会创建自身属性,后续 instance.validate() 将不再查询原型,这就是遮蔽。遮蔽本身不会修改原型或其他实例,是一种轻量级定制。
若怀疑代码中发生了意外的遮蔽并想定位,可比较 Object.prototype.hasOwnProperty.call(instance,'validate') 与 'validate' in instance 的结果:前者为 true 而后者也为 true,说明存在自身属性;若前者为 false 且后者为 true,则属性来自原型。下方代码演示链式查找与遮蔽。
const layer1 = { x: 1 };
const layer2 = Object.create(layer1);
const layer3 = Object.create(layer2);
console.log(layer3.x); // 1,沿链找到
layer3.x = 3;
console.log(layer3.x); // 3,自身属性遮蔽
console.log(layer2.x); // 1,不受影响
console.log(layer3.hasOwnProperty('x')); // true查看输出与解释
1
3
1
true这段代码先演示了读取时沿链从 layer3 逐级向上找到 layer1 的 x;赋值 layer3.x 创建了自有属性,后续读取优先返回自有值,且层间互不影响。
查找顺序的边界条件与失败形态
有一个容易被忽视的边界:如果位于原型上的属性不可写(writable: false),那么子实例无法创建同名自身属性。在严格模式执行 instance.prop = value 会抛出 TypeError,非严格模式则静默失败;原因是赋值会先检查原型链是否存在不可写的 prop 且没有 setter,并决定是否允许定义新属性。
原型链还可能被 Object.setPrototypeOf 显式地改变,使查找顺序动态化。若构造出循环引用,引擎会拒绝设置成环的 prototype;过长链则可能拉低属性命中性能。要确定属性的真实来源,应结合 Object.getPrototypeOf 遍历或使用 hasOwn 判断每一层,而不是依赖对对象的静态写死理解。
容易答错的地方
- 误把 for...in 当作只枚举自身属性
for...in会遍历自身及原型链上的所有可枚举属性,不会停留在自身。若只想处理自有键,必须用Object.keys或hasOwnProperty过滤。很多面试者在这一细节上头,并因此混淆了枚举与查找的差异。- 混淆函数的 prototype 与对象内部原型
- 构造函数的
prototype属性仅用于new生成实例时设定其实例的内部原型,函数自身原型是Function.prototype。把这两者混为一条链,就会错误理解继承关系,例如以为obj.prototype指向其原型,实际上普通对象没有 prototype 属性。
面试官还会怎么问?
如何判断一个属性是实例自身还是继承来的?
使用 Object.prototype.hasOwnProperty.call(obj,'prop') 或现代 Object.hasOwn(obj,'prop') 判断自身;'prop' in obj 则包含原型链。二者结合可定位属性在链上的归属。
在继承方法中 this 指向哪里?
this 始终指向最开始时调用该方法的实例对象,而不是方法在原型链上被发现的那一层。这一规则保证实例状态可被共享方法访问,也是 super 调用机制的基础。
原型链查找会影响性能吗?
会,但现代引擎对短链有内联缓存优化。若链过长或频繁动态修改原型,缓存可能失效,导致每次访问都需多层查表。实践中保持链短且结构稳定是减少开销的关键。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。