什么是原型和原型链?
简化版
每个对象都有内部原型,访问属性时如果对象自身没有,就会沿着原型链向上查找。函数有 prototype 属性,用于给通过 new 创建的实例提供共享方法。
详细版
核心关系:
function Person(name) {
this.name = name;
}
Person.prototype.say = function () {
return this.name;
};
const p = new Person('Tom');
p.say();
p 自身没有 say,会沿着 p.__proto__ 找到 Person.prototype。继续往上是 Object.prototype,再往上是 null。
原型链的价值是方法共享:实例属性放对象自身,公共方法放原型上,避免每个实例都复制一份函数。
面试要注意:__proto__ 是对象的内部原型访问器,prototype 是函数对象上的属性,两者不是一回事。
完整版教学
一、属性查找过程
当执行 p.say 时,引擎先查 p 自己有没有 say。没有就查 p 的原型,再查原型的原型,直到找到属性或到达 null。
这条查找路径就是原型链。找到访问器属性时,getter 中的 this 通常仍是最初接收者 p,而不是定义 getter 的原型对象,这说明属性查找位置与调用接收者是两个维度。
二、new 做了什么
new Person('Tom') 大致做四件事:
- 创建一个新对象。
- 把新对象的原型指向
Person.prototype。 - 用新对象作为
this执行构造函数。 - 如果构造函数没有返回对象,就返回这个新对象。
这解释了为什么实例能访问 Person.prototype 上的方法。
三、prototype 和 proto 的区别
prototype 是普通可构造函数默认拥有的属性,主要服务于构造函数创建实例;箭头函数等不可构造函数没有这种默认属性,普通对象也可以自行定义同名业务属性。
__proto__ 是对象访问内部原型的历史访问器。现代代码更推荐:
Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, proto);
四、原型链的风险
原型链太深会影响属性查找效率,也会让代码难维护。修改内置对象原型也很危险:
Array.prototype.foo = function () {};
这可能污染全局行为,影响第三方库和遍历逻辑。业务开发通常避免修改内置原型。
五、面试追问与工程落地
原型链题常会追问“属性遮蔽”。如果实例自身有 say,就不会继续去原型上找同名 say。这叫 shadowing。判断属性是否来自自身可以用 Object.hasOwn(obj, key) 或 hasOwnProperty,不要把“能访问到”误认为“一定是自身属性”。
还可能追问 constructor。默认情况下,Person.prototype.constructor === Person,但如果你整体替换了 prototype,constructor 可能丢失,需要手动修正。现代 class 语法减少了这类手写错误,但读老代码时仍然很重要。
工程里不要为了继承滥用深原型链。业务对象组合通常比继承更容易维护。原型链最有价值的场景是理解 JS 对象模型、class 本质、方法共享,以及排查属性查找和 monkey patch 带来的问题。
六、用对象关系图推导读取与写入
创建两个实例时,实例各自保存 name,但共享同一个 Person.prototype.say 函数。若每个函数对象粗略占 1KB,创建 1000 个实例时,把方法放构造器内部可能创建约 1000 份函数;放原型上只创建 1 份。这个数字只是帮助理解共享机制,真实内存取决于引擎优化,不能当性能基准。
p ──[[Prototype]]──> Person.prototype ──[[Prototype]]──> Object.prototype ──> null
│ │
└─ name: 'Tom' └─ say: function
| 表达式 | 检查或改变什么 | 结果含义 |
|---|---|---|
'say' in p | 自身加整条原型链 | 能访问到即为 true |
Object.hasOwn(p, 'say') | 仅自身属性 | 本例为 false |
Object.getPrototypeOf(p) | 内部 [[Prototype]] | 本例为 Person.prototype |
p instanceof Person | 默认检查 prototype 是否在链上 | 本例为 true |
Object.create(proto) | 以给定对象建立新对象原型 | 不执行构造函数 |
读取继承数据属性时会沿链返回值;给实例的同名可写数据属性赋值,通常在实例自身创建遮蔽属性,而不是修改原型。但若原型属性是 setter、不可写数据属性或对象使用 Proxy,写入行为会不同,所以“赋值永远写自身”也只是普通场景简化。
prototype是构造函数用于实例连接的普通属性,[[Prototype]]是每个对象参与继承查找的内部槽;两者通过new建立关系,但不是同一个概念。
七、常见误区与追问
- 误区:所有对象都有可用的
.prototype属性。 普通实例参与原型链却通常没有自身 prototype 属性;该属性主要存在于可构造函数上。 - 误区:
obj.__proto__就是规范内部槽本身。 它是历史兼容访问器,业务代码应优先使用Object.getPrototypeOf等标准方法。 - 误区:给继承属性赋值一定会修改原型。 普通可写数据属性通常会在接收者自身创建同名属性,从而遮蔽原型值。
- 追问:
new遇到构造函数显式返回值会怎样? 返回对象时采用该对象;返回原始值时通常忽略它并返回新创建的实例。 - 追问:替换
Person.prototype会影响旧实例吗? 旧实例仍连接旧原型对象,新实例连接新对象;替换变量属性不会自动重连已有实例。 - 追问:
instanceof能否被自定义? 可以,右侧对象的Symbol.hasInstance方法优先参与判断,因此它不总是单纯原型链检查。 - 追问:为什么不建议频繁
Object.setPrototypeOf? 动态改变原型会破坏引擎对对象形状与属性查找的优化假设,也会增加维护复杂度。
八、加强记忆
原型链可以记成“自己没有就问上级”。实例通过内部原型连到构造函数的 prototype,公共方法放原型上复用;prototype 属于函数,原型链属于对象属性查找机制。