news 2026/9/4 20:06:41

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript原型链完全指南:从prototype到继承底层原理

1. 先别背答案:原型链存在的理由,比链表本身更重要

前端面试卷到一定阶段,"原型链"差不多是必考点。我面试过不少候选人,喊一句"你了解原型链吗",对方立刻开始背:prototype是函数的属性,__proto__是对象的属性,实例可以往上找方法……背得很熟,但是再往深问一句"那你说说,JS 为什么要设计这么一套东西?"就卡住了。

这就是典型的"八股会背,原理不会"。八股不是不能背,但你要背的是底层逻辑,而不是症状描述。原型链这套机制,说到底解决的是两个问题:复用和继承。JS 本身没有传统意义上类和实例的强绑定关系——严格说,它压根不是基于类的语言,而是基于原型的语言。你写class语法,本质上也只是把原型操作包了一层糖衣,最终落地还是原型和原型链。

我经常用一个生活化类比:每个对象都像一个"家族成员",自己家里没有的东西,就到爸爸那儿找;爸爸没有,就去爷爷那儿找;一直找到老太爷,老太爷也没有,才算真没有。这个"往上找"的链条,就是原型链。理解了这个核心逻辑,你再看那些拗口的概念,会发现全都能对号入座。

为什么会设计成这样?历史原因很直接:JS 是 1995 年前后被快速设计出来的,当时没有走 Java 那套类继承路线,而是采用了基于原型的方式。类继承是一张"设计图纸",你得先有图纸再盖房子;原型继承是"直接找现成的房子克隆",房子本身就能当模板。这种设计让对象创建非常轻量,也让方法复用变得极其自然——同一个构造函数创建出来的实例,不需要各自复制一份方法,而是共享构造函数原型上的同一个方法。

所以别再觉得原型链是面试官闲得没事编出来折磨人的东西。它是 JS 实现面向对象的底层地基,newclass、继承、方法复用、甚至各种 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('张三')时,引擎这一步干了四件事:

  1. 创建一个空对象,比如叫obj
  2. obj.__proto__指向Person.prototype
  3. 执行Person.call(obj, '张三'),让构造函数里的this指向obj,于是obj.name = '张三'
  4. 返回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,而mapforEachfilter这些方法都挂在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里就放着toStringhasOwnPropertyvalueOf这些几乎所有对象都能用的方法。

我用代码来演示一次完整的属性查找路径:

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); // blue,child 自身有 color,不会继续找 parent

要想判断属性到底来自自身还是原型链,不能用in操作符,因为in会沿着原型链查找。得用hasOwnProperty

console.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();

classextends背后做的主要是两件事:

  1. Dog.prototype的原型链接到Animal.prototype
  2. 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); // true

Object.create(null)就更特殊了,它会创建一个干净到没有原型的对象,连toStringhasOwnProperty都没有。这种对象常被用来当字典或映射表,因为不会被原型链上可能存在的同名属性干扰。

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场景下,一个数组在另一个iframeinstanceof 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.logconsole.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); }

输出结果就是一条完整链路,比如PersonObject,或者DogAnimalObject

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)]; };

不是说绝对不能这么干,但你要清楚风险:

  1. 污染全局:任何数组对象都会多出这个方法;
  2. 未来冲突:JS 官方以后如果真的加了同名方法,你的实现会被覆盖出 bug;
  3. for...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.prototypeArray.prototypeObject.prototype这三个在控制台分别console.dir展开一次,再把你自己写的一个普通函数console.dir展开一次,你看一眼它们的[[Prototype]]指向,胜过你背十篇文章。原型和原型链这关,跨过去了,后面再学class底层、看各种 polyfill 源码、甚至读框架源码里的继承部分,都会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 22:56:41

Hermes Agent 命令行实战:7个斜杠命令串起三条日常工作流

Hermes Agent 命令行实战:7个斜杠命令串起三条日常工作流 【免费下载链接】hermes-agent The agent that grows with you 项目地址: https://gitcode.com/GitHub_Trending/he/hermes-agent 聊到一半发现方向跑偏,你敢不敢推倒重来?大多…

作者头像 李华
网站建设 2026/9/3 20:11:55

LSM6DSL低功耗IMU开发实战:从寄存器配置到FIFO与姿态融合

做嵌入式这些年,和惯性传感器打的交道不算少。从早年的 MPU6050 开始,到后来的 ICM20602、BMI160,再到今天要聊的 LSM6DSL,每一颗芯片都有自己的脾气。其中 LSM6DSL 是我在低功耗项目里用得最多、也最愿意推荐的一颗 6 轴惯性测量…

作者头像 李华
网站建设 2026/9/3 18:56:13

三步搭好Material-UI折叠面板:从零到FAQ页与设置面板

三步搭好Material-UI折叠面板:从零到FAQ页与设置面板 【免费下载链接】material-ui Material UI: Comprehensive React component library that implements Googles Material Design. Free forever. 项目地址: https://gitcode.com/GitHub_Trending/ma/material-u…

作者头像 李华
网站建设 2026/8/31 22:33:48

蓝桥杯Fibonacci数列题解:从递归超时到迭代取模的算法优化

1. 从“蓝桥入门训练”说起:为什么是Fibonacci数列? 如果你刚开始接触编程竞赛,或者正在准备“蓝桥杯”这类赛事,那么“入门训练”这个系列题目,尤其是那道关于Fibonacci数列的题,大概率是你绕不开的第一道…

作者头像 李华
网站建设 2026/8/31 11:55:19

STM32C0x1参考手册精读指南:从寄存器到时钟的嵌入式开发实战

手里拿到一块STM32C0x1最小系统板,第一件事不是急着接线,而是先搞清楚手头这份参考手册该怎么看。STM32C0x1是意法半导体面向入门级和成本敏感应用推出的Arm Cortex-M0内核32位MCU,它的参考手册和芯片本身一样,走的是“精简、够用…

作者头像 李华