JavaScript原型链精讲:从底层机制到面试实战的完整指南

发布时间:2026/8/29 21:56:33
JavaScript原型链精讲:从底层机制到面试实战的完整指南 先把结论说在前面JavaScript 原型链这块内容算是前端面试里“会者不难、难者不会”的典型代表。很多人背了一堆概念什么prototype、__proto__、constructor张口就能默写但真被面试官追问一句“你如何判断一个属性是对象自身拥有的还是从原型链上继承来的”一下子就卡壳了。这篇博客不打算搞“1分26秒速通”那种短平快的路子而是想从底层逻辑出发把原型和原型链讲透同时结合我在面试别人和自己写业务代码时踩过的坑聊聊到底该怎么理解、怎么用、怎么回答面试官。这篇文章适合三类人看准备前端面试的应届生和转行选手写业务代码多年但没系统梳理过 JS 对象机制的同学以及想弄明白class继承和原型链到底什么关系的工程师。我尽量把复杂度拆开用生活化类比和实际代码案例来讲确保你读完不只是会背八股而是能真正说出“为什么”。1. 先搞明白原型到底在解决什么问题1.1 面试题背后真正想考察的能力很多前端面试题看起来很“八股”但实际面试官想考察的并不是“你背没背过这个知识点”而是你能不能把一个抽象的概念用清晰的语言讲出来能不能举一反三能不能从现象推导出机制。原型和原型链这道题本质考察的是三个层次第一层知不知道prototype和__proto__是什么、有什么关系第二层能不能解释清楚“属性查找机制”是怎么沿着原型链一层层往上找的第三层能不能把原型链和new、instanceof、继承、class这些知识点串起来大部分候选人挂在第一层往第二层过渡的阶段。因为第一层只需要记忆第二层需要理解。我面试过不少人能把Function.prototype、Object.prototype这些特殊对象背得滚瓜烂熟但问到“const a {}中a的原型是谁”时反而犹豫了。这其实就是没把概念真正内化。1.2 从对象创建说起不是所有东西都叫“有原型”先回到最基础的问题JavaScript 里面有多少种方式可以创建一个对象// 方式1对象字面量 const obj1 { name: 张三 }; // 方式2构造函数 function Person(name) { this.name name; } const obj2 new Person(李四); // 方式3Object.create const obj3 Object.create({ age: 18 });这三种方式创建出来的对象在“查找属性”这条链路上走的是同一个机制也就是原型链。但很多新手会误以为只有通过构造函数创建的对象才有原型这是不对的。实际上几乎所有对象在创建时都会关联到另一个对象这个被关联的对象就是它的“原型”。对象本身没有的属性会去它的原型上找。这个“查找行为”串联起来的一条链就是原型链。这里有个很容易被忽略的点Object.create(null)创建出来的对象是真的“没有原型”的。也就是说它连toString、hasOwnProperty这些方法都没有因为那些方法定义在Object.prototype上而这个对象压根没关联到Object.prototype。这种对象在面试里偶尔会出现常用来考“空对象到底是不是真的空”。注意面试时提到“{}和Object.create(null)的区别”后者才是真正意义上的“空对象”前者其实继承了Object.prototype上的所有方法。2. 从零开始理解原型与原型链2.1 prototype 是构造函数的专属属性很多同学第一次接触原型时会被prototype和__proto__这两个长得差不多的词搞晕。我们先记住一条最核心的区分方式prototype是构造函数才有的属性__proto__是每个对象都有的属性包括函数对象也就是说prototype是函数的“附属品”它表示“当你用这个构造函数new出实例时这些实例的原型是什么”。function Person(name) { this.name name; } Person.prototype.sayHi function () { console.log(Hi, I am ${this.name}); }; const p new Person(张三); p.sayHi(); // Hi, I am 张三这里的p本身没有sayHi这个方法但它能调用是因为p.__proto__指向了Person.prototype然后 JS 引擎在p上找不到sayHi时就去p.__proto__上找。用生活场景类比Person.prototype就像是一个“公共技能包”每个通过Person创建的实例都默认可以“借用”这个技能包里的东西而不需要自己复制一份。这其实就是前端领域“共享”思想的体现和原型模式的初衷完全一致。2.2proto是每个对象的“链接指针”__proto__在早期是浏览器厂商私自实现的非标准属性后来被标准化了但官方更推荐的访问方式是Object.getPrototypeOf()和Object.setPrototypeOf()。来看一个经典的三元关系function Foo() {} const foo new Foo(); console.log(foo.__proto__ Foo.prototype); // true console.log(Foo.prototype.constructor Foo); // true console.log(Foo.__proto__ Function.prototype); // true console.log(Foo.prototype.__proto__ Object.prototype); // true这四行代码基本把原型链的骨架给打出来了foo是Foo的实例所以foo.__proto__指向Foo.prototypeFoo.prototype是函数Foo的原型对象它的constructor属性指回FooFoo本身也是一个函数对象函数的原型对象是Function.prototype所以Foo.__proto__ Function.prototypeFoo.prototype是一个普通对象它的原型是Object.prototype理解到这里原型链的轮廓已经出来了从实例到构造函数的prototype再从prototype到更上层的Object.prototype最终到null。2.3 原型链的终点为什么最终指向 null为什么原型链的终点是null而不是undefined或者其他什么东西Object.prototype是几乎所有对象的“默认原型”而Object.prototype本身也是一个对象它也需要有一个原型。但你总得在某个地方停住否则就会无限查找下去形成死循环。JS 的设计就是让Object.prototype.__proto__指向null作为原型链的“断路开关”。console.log(Object.prototype.__proto__); // null这个设计非常巧妙。如果把原型链看成一条高速公路null就是高速公路尽头的收费站到了这里就明确告诉你前面没路了。当 JS 引擎在整条链上都找不到某个属性时返回undefined而不是报错这是很多面试中隐藏的考点。注意Function.prototype也是一个对象所以Function.prototype.__proto__ Object.prototype是true。但Function.prototype本身比较特殊它是一个函数对象却“没有 prototype 属性”指向自己。这个问题留到下一节细讲。3. 核心关系与高频考点拆解3.1 理顺构造函数、实例、原型的关系面试里最高频的追问是“能不能说下constructor、prototype、__proto__三者之间的关系”我建议你用一段代码和一句口诀来回答。先看代码function Person() {} const p new Person(); // 1. 实例的 __proto__ 指向构造函数的 prototype console.log(p.__proto__ Person.prototype); // 2. 原型对象上的 constructor 指回构造函数 console.log(Person.prototype.constructor Person); // 3. 构造函数的原型对象是 Function.prototype console.log(Person.__proto__ Function.prototype);一句话口诀是“实例点__proto__找原型原型点constructor找构造函数构造函数点prototype找原型。”这个循环关系是理解整个原型链的“坐标原点”。你只要能在纸上画出“构造函数 —— 原型对象 —— 实例”这三者构成的三角关系并且说出每一对边对应的属性那么面试官问到new的实现原理你也顺带能答出来。new做的事情本质上就是创建一个新对象让这个新对象的__proto__指向构造函数的prototype将构造函数里的this绑定到这个新对象上执行构造函数代码给新对象添加属性如果构造函数显式返回了一个对象则返回那个对象否则返回新对象其中第二步就是“接上原型链”的关键动作。3.2 instanceof 的底层原理instanceof运算符也是面试高频考点。它的原理不复杂判断左边的对象.__proto__这条链上是否出现过右边的构造函数.prototype。function Animal() {} const dog new Animal(); console.log(dog instanceof Animal); // true console.log(dog instanceof Object); // true console.log(dog instanceof Function); // falsedog是Animal的实例所以dog.__proto__指向Animal.prototype因此dog instanceof Animal为true。同时Animal.prototype是一个普通对象它的__proto__指向Object.prototype所以dog instanceof Object也是true。那为什么dog instanceof Function是false因为dog.__proto__找下去只会经过Animal.prototype和Object.prototype最后到null中间不会出现Function.prototype。instanceof有个坑如果右边的构造函数没有prototype属性会直接报错。比如console.log(dog instanceof null); // TypeError手写一个instanceof也是面试常见题思路就是沿着__proto__往上遍历function myInstanceof(left, right) { let proto Object.getPrototypeOf(left); const prototype right.prototype; while (true) { if (proto null) return false; if (proto prototype) return true; proto Object.getPrototypeOf(proto); } }3.3 属性遮蔽与 hasOwnProperty原型链上属性查找有一个重要规则先查自身查不到再沿着原型链往上查。如果自身和原型链上有同名属性自身的优先级更高这个现象叫“属性遮蔽”。function Person() {} Person.prototype.name 张三; const p new Person(); p.name 李四; console.log(p.name); // 李四自身属性遮蔽了原型上的属性 console.log(Person.prototype.name); // 张三原型上的属性没变这个知识点在实际项目中非常重要尤其是当你做组件二次封装、给原生对象扩展方法时。如果不小心在实例上定义了和原型链上同名同义的字段很容易出现“改了没效果”的诡异 bug。判断属性是“自身的”还是“继承来的”用hasOwnPropertyconsole.log(p.hasOwnProperty(name)); // true因为自身定义了 name console.log(p.hasOwnProperty(name2)); // false原型上有 name2但自身没有ES2022 新增了Object.hasOwn()静态方法写法更简洁推荐在支持的环境里直接使用。4. 继承方案原型链的实战应用4.1 原型链继承与它的缺陷聊完原型链的基础接下来是面试中最容易展开的部分继承。原型链继承是把子类的prototype指向父类的一个实例从而让子类实例通过原型链访问到父类原型上的方法。function Parent() { this.hobbies [羽毛球, 阅读]; } Parent.prototype.showHobbies function () { console.log(this.hobbies); }; function Child() {} Child.prototype new Parent(); const c1 new Child(); const c2 new Child(); c1.hobbies.push(编程); console.log(c2.hobbies); // [羽毛球, 阅读, 编程]问题很明显如果父类的属性是引用类型比如数组、对象子类实例会共享同一份数据。c1修改了hobbiesc2也跟着变了。这在业务里几乎属于“不可接受”的行为因为实例之间应该是相互独立的。另一个问题Child.prototype.constructor在Child.prototype new Parent()之后被覆盖了指向了Parent需要手动纠正。4.2 组合式继承与寄生组合式继承为了解决共享引用类型属性问题经典的办法是“组合继承”——在子类构造函数里调用父类构造函数继承实例属性再把子类的prototype指向父类实例继承原型方法。function Parent(name) { this.name name; this.hobbies [羽毛球]; } Parent.prototype.sayName function () { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age age; } Child.prototype new Parent(); Child.prototype.constructor Child;这种方式属性不共享了但有一个浪费Parent构造函数被调用了两次一次在Child里用call一次在new Parent()时导致Child.prototype上挂了一份多余的实例属性。更优雅的做法是寄生组合式继承用Object.create把父类的prototype拿过来作为子类prototype的原型绕开父类构造函数的额外调用。Child.prototype Object.create(Parent.prototype); Child.prototype.constructor Child;Object.create(Parent.prototype)创建了一个新对象它的__proto__指向Parent.prototype完美实现了“子类原型继承父类原型”这件事又不会多跑一次父类构造函数。实操心得面试问到继承如果能写出寄生组合式继承并解释为什么它比组合继承更优通常能拿到一个不错的加分项。因为至少证明你不只是背了 ES6 的class语法。4.3 class 只是语法糖但细节需要注意很多面试者以为 ES6 的class和原型链没关系了这个理解是错的。class的底层仍然是原型链只是语法上更接近传统面向对象语言。class Animal { constructor(name) { this.name name; } speak() { console.log(${this.name} makes a sound); } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed breed; } speak() { super.speak(); console.log(${this.breed} barks); } } const dog new Dog(旺财, 金毛);这段代码如果用原型链来理解Dog.prototype的原型就是Animal.prototype所以dog实例沿着原型链查找speak方法时先找到Dog.prototype.speak再往上找到Animal.prototype.speak。但class和传统构造函数写法有几个重要区别class内部方法不可枚举class必须用new调用不能直接作为普通函数调用class存在“暂时性死区”类似let子类constructor里必须先调用super才能使用this这几个点都是面试里容易延伸出去问的细节。如果你能把class与原型链的关系说到这个层面说明你是真的理解了。5. 面试中容易翻车的细节与实战排查5.1 高频易错点清单整理一下我在面试候选人和帮同事 review 代码时反复遇到的关于原型链的易错高频点易错点正确理解常见误区Function.prototype的原型是Object.prototype以为函数对象的原型链到Function.prototype就结束了Object.prototype.__proto__是null以为所有对象都有原型忽略了null是终点函数也是对象函数foo.__proto__ Function.prototype以为只有用new创建的对象才有原型hasOwnProperty保护只判断“自身属性”不判断原型链误以为hasOwnProperty会沿原型链查找constructor的指向Person.prototype.constructor Person覆盖prototype后忘记修复constructor其中最隐蔽的是第一点。很多人能答出foo.__proto__ Function.prototype但问“Function.prototype.__proto__是谁”就答不上来。正确答案是Object.prototype因为Function.prototype本质上也是一个对象。5.2 项目里真正用到原型链的场景原型链不是只活在面试题里的概念在真实业务中也有不少应用场景。一个典型场景是给第三方库或内置对象做扩展。比如给Array.prototype扩展一个自定义的myMap方法Array.prototype.myMap function (callback) { const result []; for (let i 0; i this.length; i) { result.push(callback(this[i], i, this)); } return result; }; const arr [1, 2, 3]; console.log(arr.myMap((item) item * 2)); // [2, 4, 6]但这里有个非常严肃的警告不要随意修改内置对象的原型尤其是在大型合作项目里。因为你不知道哪一天别人写了一个和你同名的方法或者浏览器原生更新后自带了相同行为的方法两个定义之间就会产生覆盖和冲突。更严重的潜在问题在于给内置原型加方法会影响所有数组对象等于全局污染。另一个实用场景是做数据校验或字段补齐的统一方法扩展。比如业务里所有接口返回的数据对象都需要统一的格式化逻辑可以挂在自定义类的原型上而不是给每个实例单独复制函数。class User { constructor(data) { Object.assign(this, data); } getFullName() { return ${this.firstName} ${this.lastName}; } }这样所有User实例共享getFullName这个方法本身而不是每个实例都保存一份函数副本内存更省逻辑也更统一。实操心得如果你必须扩展内置原型至少做到两点一是命名足够独特带上项目前缀二是判断当前环境是否已有同名方法避免重复定义。5.3 手写实现题型的思路与模板面试里经常出现“手写一个new”“手写instanceof”“手写Object.create”这类题。它们的核心考察点其实都指向同一个能力你是否理解对象、原型、构造函数三者之间的关系。手写new的模板function myNew(Constructor, ...args) { // 1. 创建新对象并链接原型 const obj Object.create(Constructor.prototype); // 2. 执行构造函数绑定 this const result Constructor.apply(obj, args); // 3. 根据返回值类型决定返回什么 return (typeof result object result ! null) || typeof result function ? result : obj; }手写Object.create的精简版function myCreate(proto) { function F() {} F.prototype proto; return new F(); }这两个实现合在一起几乎把原型链的核心机制都覆盖了myNew里调用了Object.create来建立原型链而myCreate又是通过临时构造函数和设置prototype来实现的。如果你能在面试现场写出这两个东西并且把每一步的原理讲清楚面试官基本可以放心你的 JS 基础。5.4 一个实际调试案例为什么组件数据互相影响了最后分享一个真实项目的 bug它能很好地把原型链的坑串起来。某次做后台管理系统时同事写了一个列表组件内部定义const defaultConfig { pageSize: 10, filters: [status], }; function ListView(config) { this.config Object.assign(defaultConfig, config); } const list1 new ListView({ pageSize: 20 }); const list2 new ListView({ filters: [status, type] });结果发现list2实例创建后list1的pageSize也被改成了 20。原因是Object.assign(defaultConfig, config)直接修改了defaultConfig这个对象本身而list1和list2的this.config都引用着同一个默认对象。这个问题其实不是严格意义上的原型链问题但它和原型链属性共享的原理是同一类问题多个对象共享同一份数据时一个改全体受影响。在原型链里这种共享发生在prototype上的引用类型属性在业务代码里这种共享发生在模块内部的默认配置对象上。排查思路也很直接用hasOwnProperty检查某个属性是实例自己的还是从原型链上共享来的如果实例属性是共享的看它是否来自prototype或者某个全局默认对象。注意无论是原型链上的引用类型属性还是业务代码里的共享默认配置核心规避方案都是“先复制再修改”。对于原型链尽量不在prototype上放引用类型属性对于默认配置使用深拷贝或Object.assign({}, defaultConfig)来创建新对象。6. 我实际面试时最常用的一道追问题面试到这我基本会出一个延伸问题来区分候选人到底懂没懂Function instanceof Object和Object instanceof Function分别输出什么为什么第一眼看上去有点绕两个都是函数Object是函数Function也是函数。实际上这两个表达式输出都是true。Function.instanceof Object为true是因为Function.__proto__是Function.prototype而Function.prototype.__proto__ Object.prototype所以链上能遇到Object.prototypeObject instanceof Function为true是因为Object.__proto__ Function.prototype直接命中能把这个链条完整推出来的人说明他不是在背题而是真的理解了__proto__的查找机制。这个能力比记住“原型链到 null 结束”这样的语料重要得多。7. 多补充一个实际经验原型链相关代码在浏览器控制台里的调试姿势最后分享一个实际调试技巧。平时写代码时如果想快速确认“某个属性到底是不是原型链上的”在浏览器控制台可以这样操作const obj { a: 1 }; obj.toString(); // 原型链上的方法 Object.hasOwn(obj, a); // true Object.hasOwn(obj, toString); // false也可以直接在控制台输入obj展开[[Prototype]]链路逐层查看。Chrome DevTools 会把原型链层级以嵌套方式展示出来稍微点开几层就能直观看到Object.prototype那层和null的终点。在 Node.js 环境里可以用util.inspect或者node --inspect配合 DevTools 来做类似检查。遇到 Vue 或 React 项目组件实例的控制台展开同样能看到原型链上的方法这对排查“这个方法从哪来的”非常有帮助。8. 最后再讲一个我自己的感受做了几年前端写过业务也带过人我越来越觉得原型链这个知识点是 JS 世界里“少而关键”的基石之一。它看起来只涉及几个属性、几条引用关系但牵扯出来的东西一个比一个重要new的原理、继承的实现、instanceof的机制、class的本质、甚至一些底层库的扩展方案。我的建议是不要指望“1分26秒看懂”这种速食方式能把这个问题真正搞定。你至少要花一个晚上亲手在控制台里打印一下Person.prototype、person.__proto__、Person.prototype.constructor自己把那张三角关系图画一遍再手动实现一遍new和instanceof才能真正有肌肉记忆。等你能闭上眼睛把“实例到原型、原型的原型、终点 null”这条链路走通再往下学class、Symbol的Symbol.hasInstance都会顺畅很多。这些看似零散的知识点最终会在你脑子里织成一张网。到那个时候你会发现面试题只是这张网最表面的几根线而已。