JavaScript原型链完全指南:从prototype到继承底层原理

发布时间:2026/8/29 12:59:27
JavaScript原型链完全指南:从prototype到继承底层原理 1. 先别背答案原型链存在的理由比链表本身更重要前端面试卷到一定阶段原型链差不多是必考点。我面试过不少候选人喊一句你了解原型链吗对方立刻开始背prototype是函数的属性__proto__是对象的属性实例可以往上找方法……背得很熟但是再往深问一句那你说说JS 为什么要设计这么一套东西就卡住了。这就是典型的八股会背原理不会。八股不是不能背但你要背的是底层逻辑而不是症状描述。原型链这套机制说到底解决的是两个问题复用和继承。JS 本身没有传统意义上类和实例的强绑定关系——严格说它压根不是基于类的语言而是基于原型的语言。你写class语法本质上也只是把原型操作包了一层糖衣最终落地还是原型和原型链。我经常用一个生活化类比每个对象都像一个家族成员自己家里没有的东西就到爸爸那儿找爸爸没有就去爷爷那儿找一直找到老太爷老太爷也没有才算真没有。这个往上找的链条就是原型链。理解了这个核心逻辑你再看那些拗口的概念会发现全都能对号入座。为什么会设计成这样历史原因很直接JS 是 1995 年前后被快速设计出来的当时没有走 Java 那套类继承路线而是采用了基于原型的方式。类继承是一张设计图纸你得先有图纸再盖房子原型继承是直接找现成的房子克隆房子本身就能当模板。这种设计让对象创建非常轻量也让方法复用变得极其自然——同一个构造函数创建出来的实例不需要各自复制一份方法而是共享构造函数原型上的同一个方法。所以别再觉得原型链是面试官闲得没事编出来折磨人的东西。它是 JS 实现面向对象的底层地基new、class、继承、方法复用、甚至各种 polyfill全都跑在这条链上。把它弄明白你看很多前端代码会有一种看通了底层的爽感。2. prototype、proto、constructor三兄弟各管各的一张表分清很多新手学到这里就晕了因为名字实在太像。prototype、__proto__、constructor三个词绕来绕去像三个长得差不多的兄弟。我给个简单粗暴的记忆框架概念谁拥有指向谁一句话职责prototype函数一个普通对象被new之后这个对象会成为实例的上级__proto__任意对象该对象的原型对象记录我的上级是谁constructor原型对象上的属性指向构造函数本身让对象能反向找到自己的构造函数我再说细一点。普通对象只有__proto__没有prototype函数则有prototype因为函数可能要充当构造函数。那函数有没有__proto__有它也是对象它的__proto__指向Function.prototype。这里很多人第一次会绕晕记住一句话只有函数有prototype但所有对象都有__proto__函数也是对象。然后看new的时候到底发生了啥。我直接拆成四步function Person(name) { this.name name; } Person.prototype.sayHi function () { console.log(我是 this.name); }; const p1 new Person(张三); p1.sayHi(); // 我是张三当你写new Person(张三)时引擎这一步干了四件事创建一个空对象比如叫obj让obj.__proto__指向Person.prototype执行Person.call(obj, 张三)让构造函数里的this指向obj于是obj.name 张三返回obj如果构造函数自己返回了对象那就返回那个对象。所以你会发现p1自己身上只有name属性sayHi不在自己身上但依然能调用因为引擎沿着p1.__proto__找到了Person.prototype上的方法。这也是方法共享的精髓一万个Person实例共享同一个sayHi内存里只存了一份。说到这儿必须提一个坑手写new时很多人只做了前三步忘了第四步的返回值判断。因为构造函数如果返回的是引用类型new会直接返回那个引用类型。严谨一点的myNew大概是这个逻辑function myNew(Constructor, ...args) { const obj Object.create(Constructor.prototype); const result Constructor.apply(obj, args); return (typeof result object result ! null) || typeof result function ? result : obj; }Object.create(Constructor.prototype)这一步本质上就是建立obj.__proto__和Constructor.prototype的关联但用了标准 API比手动改__proto__安全得多。至于constructor它是prototype对象上一个默认存在的属性反向指回函数。你直接Person.prototype.constructor Person会得到true。这个反向引用平时用不太上但在一些场景里很有用后面我会专门讲重写 prototype 后 constructor 丢了这个经典坑。再补充一个规范细节__proto__其实是历史遗留的访问器属性标准推荐用Object.getPrototypeOf(obj)读取Object.setPrototypeOf(obj, parent)设置。日常调试用__proto__没问题但项目代码里尽量用标准 API面试时你能主动说出这一点是会加分的。3. 顺着链条逐级查找一次属性访问引擎到底走了多远现在我们把视角从是什么转到怎么查。JS 里你访问任何一个属性都不是只看看对象自身就完事而是一条链路走到底。我用一个最经典的例子const arr [1, 2, 3]; arr.map(x x * 2);arr自己有没有map没有。那为什么能调用因为arr.__proto__指向Array.prototype而map、forEach、filter这些方法都挂在Array.prototype上。引擎先查arr自己的属性没找到再查arr.__proto__找到了map于是调用。那如果Array.prototype上也没有呢继续往上Array.prototype.__proto__指向Object.prototype。比如arr.toString()Array.prototype上有一个自己的toString但如果某个属性Array.prototype上没有就会去Object.prototype里找。Object.prototype再往上是null查无可查最终返回undefined。所以整条链路长这样arr → Array.prototype → Object.prototype → null普通对象、函数对象的原型链分别是普通对象 obj → Object.prototype → null function f() {} → Function.prototype → Object.prototype → null这里有一个非常关键的记忆点所有内置构造函数的原型最终都会指向Object.prototype。所以不管多么复杂的对象往根上追一定能追到Object.prototype然后它的__proto__是null链条终结。Object.prototype里就放着toString、hasOwnProperty、valueOf这些几乎所有对象都能用的方法。我用代码来演示一次完整的属性查找路径const person { name: 张三, }; // person 自身有 name直接返回 console.log(person.name); // 张三 // person 没有 toString顺着 person.__proto__ 向上查 console.log(person.toString); // [Function: toString]那假如在原型链不同层级出现同名属性怎么办答案是就近原则也叫属性遮蔽先找到谁就先用谁不再往上找。const parent { color: red }; const child { color: blue }; Object.setPrototypeOf(child, parent); console.log(child.color); // bluechild 自身有 color不会继续找 parent要想判断属性到底来自自身还是原型链不能用in操作符因为in会沿着原型链查找。得用hasOwnPropertyconsole.log(color in child); // true原型链上有也算 console.log(child.hasOwnProperty(color)); // true自身属性 console.log(parent.hasOwnProperty(color)); // true父对象自身属性再举个原型链和for...in的例子const parent { inherited: 1 }; const child Object.create(parent); child.own 2; for (const key in child) { console.log(key); // 会打印 own 和 inherited }Object.keys(child)则只返回[own]。这个差异面试经常考它俩的区别本质就是你有没有上原型链去翻。我建议你在浏览器控制台做一个小实验console.dir([1,2,3])展开[[Prototype]]你能看到Array.prototype上挂了一大堆方法再往下点一层又能看到Object.prototype。亲眼看过一次比背十遍都管用。4. 会写才是真懂方法复用、原型继承和 class 的底层关系理解原型链最直接的价值就是知道自己写代码时哪些逻辑跑在自己身上哪些跑在公有链路上。我从三个典型场景展开说。4.1 用 prototype 做方法复用假设你要做一个用户模块很多人会这么写function User(name) { this.name name; this.greet function () { console.log(Hi, this.name); }; } const u1 new User(a); const u2 new User(b); u1.greet u2.greet; // false每一次new User都会创建一个全新的greet函数。一百个用户就有一百份greet浪费内存不说还没有任何收益。正确做法是把方法挂到User.prototype上function User(name) { this.name name; } User.prototype.greet function () { console.log(Hi, this.name); }; const u1 new User(a); const u2 new User(b); u1.greet u2.greet; // true同一个函数这才是原型机制的本职工作实例属性各自持有公共方法共享一份。4.2 基于原型链实现继承接下来是继承。如果你想让Admin继承User经典组合继承长这样function User(name) { this.name name; } User.prototype.sayName function () { console.log(我是 this.name); }; function Admin(name, permissions) { User.call(this, name); // 先借用 User 构造函数给实例赋属性 this.permissions permissions; } // 关键把 Admin.prototype 的原型指向 User.prototype Admin.prototype Object.create(User.prototype); // 修回 constructor否则它指向 User Admin.prototype.constructor Admin; Admin.prototype.manage function () { console.log(管理权限 this.permissions.join(,)); }; const admin new Admin(管理员, [create, delete]); admin.sayName(); // 我是管理员 admin.manage(); // 管理权限create,delete这段代码就是class extends的未包装形态。为什么Admin.prototype要重新赋值为Object.create(User.prototype)因为如果你直接写Admin.prototype User.prototype那往Admin.prototype上加方法就是在User.prototype上加方法污染了父类。用Object.create就能生成一个以User.prototype为原型的中间对象既能让Admin实例顺着原型链找到User.prototype的方法又能在中间对象上独立添加自己的方法。4.3 ES6 class 只是语法糖很多人学了class就以为不需要原型了其实不然。看这一段class Animal { constructor(name) { this.name name; } speak() { console.log(this.name 叫了一声); } } class Dog extends Animal { constructor(name, breed) { super(name); this.breed breed; } speak() { super.speak(); console.log(this.breed 汪汪汪); } } const d new Dog(旺财, 金毛); d.speak();class的extends背后做的主要是两件事把Dog.prototype的原型链接到Animal.prototype把Dog本身的__proto__接到Animal让静态方法的继承也能成立。也就是说你写的class越高级底层干的原型操作就越复杂但本质没变实例 → 子类原型 → 父类原型 → Object.prototype → null。所以不要再有我写 class 所以不用管原型的错觉简历上写着熟悉 JS问到底层只会 class 语法是撑不过深挖的。4.4 Object.create 是原型链的显式开关Object.create是直接指定原型创建对象的手段。你可以让两个看似毫无关系的对象产生血缘const base { version: 1.0, getVersion() { return this.version; }, }; const instance Object.create(base); instance.name 实例对象; console.log(instance.getVersion()); // 1.0 console.log(instance.__proto__ base); // trueObject.create(null)就更特殊了它会创建一个干净到没有原型的对象连toString、hasOwnProperty都没有。这种对象常被用来当字典或映射表因为不会被原型链上可能存在的同名属性干扰。5. 高频笔试考点拆解instanceof、for...in、constructor 修复到了面试环节原型链相关的考法非常固定。我把几个高频点串一遍你对照着自查一下。5.1 instanceof 和 String() 的判断差异instanceof的原理其实就是遍历原型链判断右边构造函数的prototype是否出现在左边对象的原型链上。const arr []; arr instanceof Array; // true因为 Array.prototype 在 arr 的原型链上 arr instanceof Object; // true因为 Object.prototype 也在 arr 的原型链上但要注意instanceof判断的是原型链关系不是类型描述。跨iframe场景下一个数组在另一个iframe里instanceof Array可能得到false因为两个iframe各自有独立的Array.prototype。想要更稳健的类型判断前端常用Object.prototype.toString.call(arr)返回[object Array]。5.2 constructor 为什么会丢前面提到过默认情况下Person.prototype.constructor指向Person。但一旦你整个重写 prototype 对象这个关联就断了function Person(name) { this.name name; } // 危险操作整体重写 prototype Person.prototype { sayHi() { console.log(hi); }, }; const p new Person(张三); console.log(p.constructor); // Object因为 Person.prototype.constructor 现在是 Object.prototype.constructor为什么变成Object了因为Person.prototype被换成了一个普通对象字面量它的constructor是从Object.prototype继承过来的指向Object。要修复很简单手动补回去Person.prototype.constructor Person;很多老代码里你会看到这种习惯原因就在这里。5.3 for...in 和 Object.keys 的差别这个考点我们前面提过这里从面试角度再强调一次。for...in遍历的是自身可枚举属性 原型链上的可枚举属性Object.keys只遍历自身可枚举属性。所以如果你用for...in遍历对象并做数据操作很容易把原型上不小心暴露的公共属性也遍历进去通常要配合hasOwnProperty过滤for (const key in obj) { if (!obj.hasOwnProperty(key)) continue; // 只处理自身属性 }或者干脆用Object.keys。5.4 手写 instanceof 的简化版本理解了原型链手写instanceof其实不复杂function myInstanceof(left, right) { let proto Object.getPrototypeOf(left); const prototype right.prototype; while (proto) { if (proto prototype) return true; proto Object.getPrototypeOf(proto); } return false; }核心逻辑就是一层层往上找找到null还找不到就返回false。这个题目能反映出你到底是真懂了还是背了别人的答案。5.5 快速记忆主线一句话讲清楚原型链既然标题说有1分26秒看懂我也给你一个可以背的版本每个对象都有一个隐藏属性__proto__指向它的原型对象函数在创建时会自带一个prototype对象用new调用函数时生成的实例会把__proto__指向这个prototype当访问属性时对象会先查自己查不到就沿着__proto__一级级往上查直到Object.prototype它的__proto__是null链路到此结束。这句话背下来你去面试时能把八股从背名词升级成讲流程。面试官最喜欢听到的就是对象查不到属性会沿着原型链向上找最顶上是 Object.prototype再往上就是 null——这说明你真的把链路串起来了。6. 调试控制台与日常避坑我踩过原型的几个真实教训最后聊点实战经验。原型链在理论上很容易画明白但真写起代码、调起 bug 来坑也不少。6.1 用 console.dir 直观察看原型链如果某天你发现一个对象能调用一个没定义过的方法第一反应就是去控制台看原型链。console.log和console.dir有区别console.log很多状况下会直接打出一个对象在某个引用层面上的状态而console.dir会以树状结构展示内部属性包括[[Prototype]]。我排查原型相关问题时固定操作就是console.dir(对象)然后层层展开[[Prototype]]看方法到底挂在哪一层。也可以直接在代码里用Object.getPrototypeOf一层层打印let proto Object.getPrototypeOf(someObj); while (proto) { console.log(proto.constructor proto.constructor.name); proto Object.getPrototypeOf(proto); }输出结果就是一条完整链路比如Person→Object或者Dog→Animal→Object。6.2 不要手动乱改proto很多人为了图方便直接写obj.__proto__ something。这是能出来你要的效果但性能很差而且__proto__本身是个存在争议的历史遗留方案。现代开发首选Object.create创建带指定原型的新对象或者用Object.setPrototypeOf在已有对象上调整。不过setPrototypeOf也会显著影响性能能用Object.create的场景就别用前者。尤其要避免原型链成环。我见过有人好奇尝试const obj {}; Object.setPrototypeOf(obj, obj);这一行下去当你再访问obj的某个属性时引擎会在原型链上无限循环直接爆栈。原型链为什么必须以null结束就是防止这条链变成死循环。这个案例我经常用来提醒新人原型链是一条有终点的单向链路不是随意环游的图。6.3 扩展原生 prototype 要克制网络上很多一行代码干掉数组去重之类的骚操作常见做法就是往Array.prototype上加自定义方法Array.prototype.unique function () { return [...new Set(this)]; };不是说绝对不能这么干但你要清楚风险污染全局任何数组对象都会多出这个方法未来冲突JS 官方以后如果真的加了同名方法你的实现会被覆盖出 bugfor...in 可枚举性老环境下自己加的属性默认是可枚举的用for...in遍历数组时会被带出来所以真要扩展至少要Object.defineProperty定义一个不可枚举属性。如果是为了 polyfill标准做法是先判断原生是否存在不存在再补if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, includes, { value(array, searchElement, fromIndex) { // polyfill 实现 }, enumerable: false, writable: true, configurable: true, }); }这是我在做老项目兼容时最常用的姿势能保证不破坏别人的代码。6.4 写大型项目时优先用组合而不是深继承原型链虽好但继承层次过深会让代码变得很难追踪。一个对象到底有哪些属性你得查它、查它的父、查它的祖父……排查成本成倍增长。我自己的经验是尽量让原型链保持扁平一两次继承就到头再复杂的对象关系优先考虑组合而不是硬造继承。这跟语言机制无关是工程可维护性的问题。我自己带过不少前端新人发现一个规律只要亲手写过一个构造函数加 prototype、再写一个继承、手动修过 constructor对原型链的理解就会发生质变。光看不练是永远记不住这套东西的。最后送你一个实测小技巧把Function.prototype、Array.prototype、Object.prototype这三个在控制台分别console.dir展开一次再把你自己写的一个普通函数console.dir展开一次你看一眼它们的[[Prototype]]指向胜过你背十篇文章。原型和原型链这关跨过去了后面再学class底层、看各种 polyfill 源码、甚至读框架源码里的继承部分都会顺畅很多。

相关新闻