JavaScript 原型链
解释对象属性查找如何沿原型链上升、与 constructor/prototype 的关系,以及 class 语法糖如何落到同一模型。
#concept
#growing
#dev
#javascript
#ecmascript
[!info] related notes
- 所属:类型与对象模型 MOC · ECMAScript MOC
- 路线:JavaScript 学习路线
- 前置:对象 · 原型
- 后续:原型不是 class · new · class · this
JavaScript 原型链
这个概念为什么出现
你在对象上读 sku.toString,自己明明没写这个方法,却能调用。团队若把这理解成“类继承复制成员”,后面在改 prototype、谈性能、写 polyfill 时会全面错位。
原型链回答:属性从哪里来,找不到时下一步去哪找。
[!abstract] 一句话理解 普通对象有一个内部原型链接;读属性时沿这条链向上查找,直到找到或到达
null。Proxy 等特殊对象可改变访问行为。
最小可运行示例
场景:库存领域对象共享格式化方法
// 业务意图:所有 SKU 视图模型共享 formatStock,不在每个实例上复制函数。
// 教学点:实例自身没有的方法,会走到构造函数 prototype。
function SkuView(code, qty) {
this.code = code
this.qty = qty
}
SkuView.prototype.formatStock = function formatStock() {
// 教学点:这里的 this 由调用方式决定,一般是实例
return this.qty < 5
? this.code + ' 低库存(' + this.qty + ')'
: this.code + ' 正常(' + this.qty + ')'
}
const a = new SkuView('A-01', 3)
const b = new SkuView('B-02', 20)
console.log(a.formatStock())
console.log(Object.hasOwn(a, 'formatStock')) // false:方法不在实例自身
console.log(Object.getPrototypeOf(a) === SkuView.prototype) // true
console.log(a.toString === Object.prototype.toString) // 继续沿链找到 Object.prototype
建议运行:
node sku-proto.js
期望输出:
A-01 低库存(3)
false
true
true
结合场景再看三个关注点
- 共享方法放在
Fn.prototype,实例变轻。 - 查找失败不是报错在第一层,而是继续
[[Prototype]]。 hasOwn/in不同:in会看链,hasOwn只看自身。
核心概念与准确模型
两条相关但不同的线
| 概念 | 是什么 |
|---|---|
实例的 [[Prototype]] | 对象的原型链接(Object.getPrototypeOf) |
构造函数的 .prototype | 普通 new 创建对象时用作实例原型;不是函数对象自身的原型 |
obj.__proto__ | 历史访问器,教学可用,生产更推荐标准 API |
a ---> SkuView.prototype ---> Object.prototype ---> null
读属性算法(压缩)
- 看对象自身
- 看
[[Prototype]] - 重复直到
null - 若是方法调用,
this仍由调用方式决定,不由方法所在原型决定
写属性
普通对象赋值通常在接收者自身创建或更新属性;继承的 setter 可以接管赋值,继承的不可写数据属性或无 setter 的访问器也可能阻止赋值(严格模式抛错)。
obj.items.push(x) 是修改查找到的数组,不是给 obj.items 赋值;若数组来自原型,所有读取同一数组的实例都会看到变化。
与 class 的关系
class SkuView {
constructor(code, qty) {
this.code = code
this.qty = qty
}
formatStock() {
/* 方法在原型上 */
}
}
class 是语法层;运行时仍是原型模型。见 原型不是 class。
设计动机
原型共享让方法与默认值可复用;需要独立的可变数据应由每个实例持有。类语言通常也不会逐实例复制方法,区别应放在对象间的动态委托关系上。
边界与反直觉
for...in遍历自身与链上的可枚举字符串键;Object.keys只看自身可枚举字符串键,两者都不包含 Symbol 键。\Object.create(null)创建无原型对象,适合纯字典,但没有继承的 toString 或 proto 访问器。- 函数不一定有可用于 new 的 prototype:箭头函数不可构造;普通构造函数的 prototype 若不是对象,new 会回退到相应内建原型。\
- 改内置
prototype是高风险全局行为。
常见误区
[!warning] 常见误区:原型链 = 类的父类字段复制 没有在实例上复制方法表;是链接查找。
[!warning] 常见误区:arrow 方法挂原型一个样 类字段箭头函数常装在实例上,不共享;与原型方法不同。
工程实践
- 优先
class语法,但用原型心智调试。 - 字典数据用
Map或Object.create(null),避免踩Object.prototype键。 - 不要在热路径上深层原型上挂过多可枚举垃圾属性。
本节总结
原型链是属性查找的链表。会画链,才能理解继承、new、class 与方法共享。
自测题
a.formatStock()时方法在原型,this 是谁?- 为什么修改
SkuView.prototype.x会影响已有实例读取?
参考答案
- 一般是
a(接收者),由调用形式决定。 - 仅当实例未以自有属性遮蔽 x 时,读取才会沿链接受原型修改影响。替换 Fn.prototype 为另一个对象不会重新连接已有实例。
延伸阅读
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| ECMA-262 | 规范 | 对象内部方法 |
| MDN Inheritance and prototype chain | 文档 | 原型链 |