真实面经题目 · 原创解析
JavaScript 原型链的查找机制是什么?
JavaScript 原型链的查找机制,本质是对象属性访问时的一套逐级委托规则:先查对象自身属性,找不到再沿对象内部的 [[Prototype]] 指向继续查找,直到找到属性或到达 null 为止。面试中不仅要能说出查找顺序,还要区分 __proto__、构造函数的 prototype、实例对象之间的关系,并理解属性屏蔽、hasOwnProperty、in、class 语法糖这些高频追问点。
真实面经题目 · 原创解析
JavaScript 原型链的查找机制,本质是对象属性访问时的一套逐级委托规则:先查对象自身属性,找不到再沿对象内部的 [[Prototype]] 指向继续查找,直到找到属性或到达 null 为止。面试中不仅要能说出查找顺序,还要区分 __proto__、构造函数的 prototype、实例对象之间的关系,并理解属性屏蔽、hasOwnProperty、in、class 语法糖这些高频追问点。
JavaScript 中访问一个对象属性时,会先在对象自身的属性集合里查找。如果自身存在这个属性,就直接返回这个属性值;如果自身不存在,运行时会读取对象内部的 [[Prototype]] 引用,也就是通常所说的原型,继续到原型对象上查找。这个过程会一层一层向上委托,形成原型链,最终通常会到达 Object.prototype,再往上是 null。当查找到 null 仍然没有找到目标属性时,普通属性读取结果就是 undefined。需要注意,__proto__ 只是多数环境提供的访问 [[Prototype]] 的历史性访问器,不应把它等同于规范中的内部槽;构造函数的 prototype 是用于创建实例时指定实例原型的对象,不是函数自己的原型链。属性赋值还可能在实例上创建同名自有属性,从而屏蔽原型上的属性。判断属性来源时,hasOwnProperty 只看自有属性,in 会同时检查自有属性和原型链。class 本质上仍基于原型机制,方法通常定义在类的 prototype 上,extends 则建立实例原型链和构造函数继承关系。
当代码读取 obj.name 这类属性时,引擎首先检查 obj 自己是否拥有 name 这个属性,也就是自有属性。这个阶段不会先去看构造函数,也不会先去看父类,而是从当前被访问的对象本身开始。自有属性包括对象字面量中定义的属性、构造函数中通过 this 赋值的属性、Object.defineProperty 定义在对象本身上的属性等。只要这里命中,查找立即结束,即使原型上也有同名属性,最终返回的仍然是对象自身的那个值。
如果对象自身没有目标属性,引擎会沿着对象内部的 [[Prototype]] 继续查找。[[Prototype]] 是每个普通对象内部保存的原型引用,它指向另一个对象或 null。这个向上查找不是复制父对象的属性,而是把查找请求委托给原型对象。原型对象本身也是对象,如果它没有目标属性,就继续读取它自己的 [[Prototype]]。这一串引用关系就是原型链,JavaScript 的很多继承和方法复用能力都建立在这个机制之上。
__proto__ 通常用于访问某个对象的 [[Prototype]],例如实例的 __proto__ 往往指向创建它的构造函数的 prototype。但 __proto__ 不是创建实例方法的地方,它只是一个访问原型引用的历史性属性。构造函数的 prototype 则是函数对象上的一个普通属性,这个属性值会在 new 调用时成为新实例的 [[Prototype]]。因此应该说实例的原型通常等于构造函数的 prototype,而不是说实例继承了构造函数本身。
原型链不是无限向上查找的,它有明确终点。当查找到某个对象的 [[Prototype]] 为 null 时,链条结束。普通对象的链路通常是实例对象到构造函数 prototype,再到 Object.prototype,最后到 null。如果整个链条都没有找到属性,读取结果就是 undefined。这个 undefined 只表示属性查找失败,不等于属性存在但值一定不是 undefined,因为对象自身或原型上也可以显式保存一个值为 undefined 的属性。
如果对象自身和原型链上存在同名属性,访问时会优先返回对象自身属性,这叫属性屏蔽。比如实例上有 name,原型上也有 name,读取实例的 name 时不会继续往原型查。给实例赋值一个普通数据属性时,也常常会在实例自身创建同名属性,从而遮住原型属性。面试中容易忽略这一点,误以为修改实例属性会自动修改原型属性;实际是否影响原型,要看赋值目标、属性描述符以及访问器属性等细节。
hasOwnProperty 用来判断属性是否直接存在于对象自身,不会沿原型链查找;而 in 操作符会检查对象自身和整条原型链。比如某个方法定义在构造函数的 prototype 上,实例调用它没有问题,但 instance.hasOwnProperty(methodName) 会返回 false,而 methodName in instance 可能返回 true。这个差异很适合用来解释自有属性、继承属性和属性访问之间的关系,也是前端面试中判断候选人是否真正理解原型链的重要点。
ES6 class 改变的是书写方式,不是底层对象模型。类中定义的普通方法通常会放在构造函数的 prototype 上,实例通过原型链访问这些方法;构造器中对 this 的赋值则会成为实例自有属性。extends 会让子类实例的原型链连接到父类原型,从而复用父类方法,同时也会处理子类构造函数与父类构造函数之间的继承关系。因此 class 可以理解为更规范、更清晰的原型语法糖,而不是另一套完全独立的继承系统。
__proto__ 通常是对象访问自身 [[Prototype]] 的入口,关注的是某个对象向上委托到哪里;prototype 是函数对象上的属性,关注的是这个函数作为构造函数被 new 调用时,实例的原型应该指向哪里。简单说,实例通过 [[Prototype]] 找原型,构造函数通过 prototype 提供实例原型。
hasOwnProperty 只检查属性是否直接定义在对象自身,不关心原型链;in 会检查对象自身以及整条原型链。一个实例可以正常调用定义在 prototype 上的方法,此时方法名 in 实例通常为 true,但实例自身的 hasOwnProperty 对这个方法名会是 false。
属性屏蔽指对象自身存在某个属性时,会优先返回自身属性,原型链上的同名属性被遮住。它不会删除原型属性,只是查找过程提前结束。很多实例赋值操作会创建自有属性,因此看起来像修改了继承来的属性,实际可能只是把原型上的属性屏蔽了。
class 是基于原型机制的语法形式。类的实例属性通常在 constructor 中通过 this 创建,类的普通方法通常定义在构造函数的 prototype 上。extends 会建立子类和父类之间的原型继承关系,所以用 class 写继承时,底层仍然离不开原型链查找。
因为属性读取会从对象自身开始,沿 [[Prototype]] 一直向上查找。如果到达 null 仍然没有找到对应属性,访问表达式就返回 undefined。需要注意,这和属性存在但值为 undefined 的情况表现相同,因此要判断是否真正存在属性,应结合 hasOwnProperty 或 in。