先记住这个答案
执行 obj.prop 时,引擎先看 obj 自身有没有 prop,有就直接返回。没有就拿到它的原型,也就是 Object.getPrototypeOf(obj),在原型上再找一遍,这样一层层往上,找到第一个同名属性就停。普通对象的链最后是 Object.prototype,它的原型是 null,走到这里还没找到,结果就是 undefined。因为是就近原则,对象自己的属性会遮住原型上的同名属性。
- 先自身、再原型,找到第一个就停
- 走到
null还没找到,返回undefined - 读会沿链往上找,普通赋值只写在对象自身
- 继承方法里的
this是调用它的那个对象
看一条三层原型链怎么查
用 Object.create 搭一条三层的链:obj 的原型是 mid,再往上一层是 base,方法只写在 base 上。
const base = {
kind: 'base',
hello() { return 'I am ' + this.kind }
}
const mid = Object.create(base)
const obj = Object.create(mid)
console.log(obj.hello())
mid.kind = 'mid' // 给中间层加一个同名属性
console.log(obj.hello())
obj.kind = 'obj' // 给对象自身加
console.log(obj.hello(), base.kind)
console.log(obj.missing)
console.log(Object.getPrototypeOf(Object.prototype))查看输出与解释
I am base
I am mid
I am obj base
undefined
null第一次调用,obj 和 mid 上都没有 kind,一直找到 base。第二次在 mid 上加了 kind,查找走到 mid 就停了,base 上的不再参与。第三次 obj 自己有了,第一层就命中。base.kind 始终是 'base',遮蔽不会改动原型。
这段代码还说明两件事。一是查找发生在每次读取时,不是创建对象时拷贝一份,所以运行中改原型,下一次读取就能看到。二是 hello 虽然是在 base 上找到的,但里面的 this 指向的是调用它的 obj,所以它读到的 this.kind 也是从 obj 开始查的。这就是为什么所有实例能共用原型上的一份方法,又各自读到自己的数据。
读和写不对称
读属性会沿链往上找,普通赋值不会去改原型上的属性,而是在对象自身上新建一个。上面的 obj.kind = 'obj' 就是这样,base.kind 没变。
但赋值前引擎也会沿原型链看一眼同名属性,有两种情况会改变结果:原型上是一个 setter,或者原型上是只读属性。
const proto = {
set name(v) { this._name = v.trim() }
}
const user = Object.create(proto)
user.name = ' Tom '
console.log(Object.keys(user).join(','))
console.log(user._name)
console.log(user.name)查看输出与解释
_name
Tom
undefineduser.name = ... 找到了原型上的 setter,于是调用它,this 是 user,结果 user 身上多了 _name,但没有 name。读 user.name 时找到的还是原型上这个访问器,它只有 set 没有 get,所以读到 undefined。注意查找在这一层就停了,不会因为没有 getter 继续往上找。
- 原型上的只读属性会挡住赋值
- 如果原型链上的同名属性是
writable: false,给子对象赋值会失败:严格模式抛TypeError,非严格模式静默失败,也不会在子对象上新建属性。想绕过只能用Object.defineProperty直接在子对象上定义。
容易答错的地方
- 把
prototype属性当成对象的原型 - 构造函数的
prototype属性,只是new创建实例时拿来当实例原型的那个对象。函数自己的原型是Function.prototype。普通对象根本没有prototype属性,要拿原型得用Object.getPrototypeOf(obj)。 - 以为
for...in只遍历自身属性 for...in会把原型链上可枚举的属性也列出来,和in运算符的范围一致。只要自身属性就用Object.keys,或者在循环里用Object.hasOwn过滤。- 以为
this指向找到方法的那一层原型 this由调用方式决定,obj.hello()里就是obj,和方法定义在第几层无关。原型上的 getter 也一样,this是最开始读属性的那个对象。
面试官还会怎么问?
怎么判断一个属性是自己的还是继承来的?
Object.hasOwn(obj, 'prop') 只看自身,'prop' in obj 会查整条链。前者为 false 而后者为 true,就说明属性来自原型。
原型链能不能形成环?
不能。用 Object.setPrototypeOf 或给 __proto__ 赋值时,如果新原型会让链绕回自己,引擎直接抛 TypeError,所以查找一定会在 null 处结束。Object.create(null) 创建的对象原型直接就是 null,链只有一层。
原型链太长会不会影响性能?
理论上每多一层就多一次查找,但现代引擎会对属性访问做缓存,结构稳定的短链几乎没有额外开销。真正影响性能的是运行中用 Object.setPrototypeOf 改已有对象的原型,这会让引擎的优化失效,MDN 也明确不推荐这样做。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。