先记住这个答案
Object.getPrototypeOf 是读取对象原型的标准 API,ES5 就有了,完全可以放心用,它取代了早期浏览器私自实现的 __proto__。Object.setPrototypeOf 是 ES2015 加的,能在对象创建之后换掉它的原型。它功能上没问题,但 MDN 明确提示这个操作在所有引擎里都很慢,而且影响会波及用到这个对象的所有代码。所以现代写法是创建时就确定原型:用 Object.create(proto)、class extends,或者对象字面量里写 __proto__: proto。
- 读原型优先用
Object.getPrototypeOf - 运行时换原型会拖慢属性访问,能不用就不用
- 创建时用
Object.create等确定原型 - 原型成环会让设置操作抛错
两个 API 的行为
先看它们怎么用、失败时会怎样。
const animal = { eat() { return 'eat' } }
const bird = { fly() { return 'fly' } }
const obj = Object.create(animal) // 推荐:创建时就定好原型
console.log(Object.getPrototypeOf(obj) === animal)
Object.setPrototypeOf(obj, bird) // 运行时换原型,能用但不推荐
console.log(typeof obj.eat, obj.fly())
try {
Object.setPrototypeOf(bird, obj) // obj 的原型已经是 bird,再让 bird 指向 obj 就成环了
} catch (e) {
console.log(e.name)
}
console.log(Reflect.setPrototypeOf(Object.freeze({}), bird)) // Reflect 版本不抛错,返回 false查看输出与解释
true
undefined fly
TypeError
false换了原型之后,obj 就再也找不到 eat,原来那条原型链整个断开了。原型链不允许成环,引擎会检查并抛 TypeError。冻结或不可扩展的对象也不能换原型,Object.setPrototypeOf 会抛错,而 Reflect.setPrototypeOf 只返回 false,适合在不想写 try 的地方用。
为什么换原型会慢
V8 这类引擎会给对象的「结构」建一个内部描述,叫隐藏类,也叫 shape,记录它有哪些属性、原型是谁。访问属性时,引擎会在调用处缓存「这种结构的对象,属性在什么位置」,下次遇到同样结构的对象直接按缓存取,这叫内联缓存。
原型也是结构的一部分。运行时换原型,这个对象的结构就变了,之前的缓存要作废;如果它本身也是别人的原型,依赖这条链的缓存也要一起作废。引擎还可能对被这样改过的对象放弃一部分优化,之后的属性访问都跟着变慢。所以慢的不是 setPrototypeOf 这一次调用,而是之后的每一次属性访问。
什么时候确实需要它
少数场景没法在创建时定原型。比如继承静态方法:ES5 风格的构造函数想让子类继承父类的静态方法,要写 Object.setPrototypeOf(Child, Parent),Babel 编译 class extends 时也是这样做的。又比如处理第三方给你的对象,你不能控制它的创建过程。这些场景一次性设置、之后不再改,影响可以接受。
要避免的是在热路径上反复修改原型,比如每次请求都给对象换个原型来「切换行为」,这种需求用组合或策略对象来做更合适。
容易答错的地方
- 以为
__proto__是标准推荐写法 obj.__proto__访问器是为了兼容老代码才写进规范附录的,只在继承自Object.prototype的对象上有效,Object.create(null)创建的对象就没有。读写原型应该用Object.getPrototypeOf和Object.setPrototypeOf。对象字面量里的__proto__: proto是另一回事,它是创建时指定原型的语法,没有性能问题。- 以为
getPrototypeOf返回的是prototype属性 Object.getPrototypeOf(fn)返回的是函数自己的原型,也就是Function.prototype,不是fn.prototype。fn.prototype是给new fn()出来的实例用的。
面试官还会怎么问?
Object.getPrototypeOf('abc') 会报错吗?
ES2015 起不会,原始值先被包装成对象,返回 String.prototype。ES5 里传非对象会抛 TypeError。传 null 或 undefined 现在仍然会抛错。
Object.create(proto) 和 setPrototypeOf({}, proto) 结果一样,为什么推荐前者?
结果看起来一样,但前者是对象一出生结构就确定了,后者是先造了一个原型为 Object.prototype 的对象,再改结构。前者对引擎友好,后者会触发上面说的缓存失效。
怎么判断一个对象是否在另一个对象的原型链上?
用 proto.isPrototypeOf(obj),它会沿着 obj 的原型链一路往上找。如果只关心直接原型,用 Object.getPrototypeOf(obj) === proto。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。