先记住这个答案
用 in 会触发原型链查找,因此 'toString' in {} 是 true,而 hasOwnProperty 只检查对象自己的属性,不受原型影响。判断自有属性用 Object.hasOwn,判断属性是否可读用 in。当遍历 for...in 时,结合 Object.hasOwn 跳过继承属性,比直接比较更稳妥。
in能看到原型上的属性,hasOwn只看自身Object.hasOwn可避开覆写和空原型问题for...in遍历时用Object.hasOwn过滤继承项
查找路径:先看自身,再向上走原型
当用 obj.prop 读取属性时,引擎先查询对象自身的属性表,找不到就沿着内部 [[Prototype]] 指针跳到原型对象,如此反复直到 null。in 运算符完整复刻这条路线:它会依次检查当前对象和每一层原型,只要名称出现过就返回 true。而 hasOwnProperty 本质上调用的是对象内部对自有属性的检查,只访问第一层,不会进一步向上。这就是为什么 {} 上明显看到 toString,但 hasOwnProperty('toString') 返回 false。
原型链可能不止一层,例如 var a = {m:1}, b = Object.create(a);,b 没有 m 属性,但 b.m 可以读到。用 b.hasOwnProperty('m') 会返回 false,因为 m 不在 b 上;用 'm' in b 会得到 true,因为 m 在中间原型 a 上。检测时必须想清楚后续逻辑需要的是“能读”还是“自己拥有”。
另外,Object.prototype.hasOwnProperty 本身也是继承来的方法,若对象原型链被破坏,直接调用可能失败,所以现代静态方法更推荐:Object.hasOwn(obj, key)。
const proto = { inherited: 1 };
const obj = Object.create(proto);
obj.own = 2;
console.log('in inherited:', 'inherited' in obj);
console.log('in own:', 'own' in obj);
console.log('hasOwn inherited:', obj.hasOwnProperty('inherited'));
console.log('Object.hasOwn own:', Object.hasOwn(obj, 'own'));
console.log('Object.hasOwn inherited:', Object.hasOwn(obj, 'inherited'));
查看输出与解释
in inherited: true
in own: true
hasOwn inherited: false
Object.hasOwn own: true
Object.hasOwn inherited: false前两行 in 都是 true,自身和原型都能命中;后面四行 hasOwn 与 Object.hasOwn 都只认直接定义在 obj 上的 own。
浅拷贝场景:默认配置从原型链迁移
假设你用 Object.create(defaults) 建立实例,defaults 是原型,实例只覆盖部分字段。现在要把实例转成普通对象并传给第三方 JSON 库,若继续用 for...in 会把原型上的默认值也复制出来,产生多余字段且可能覆盖实际配置。一个可行做法是改用 Object.keys(instance),它只返回自身可枚举属性,直接得到用户真正配置的键,例如覆盖了 retry,原型上的 log 就被排除。
如果还要保留自身不可枚举的内部标记,Object.keys 会漏掉,但通常这不是业务想要导出的部分。若确要包含不可枚举,则要用 Object.getOwnPropertyNames,它不会自动过滤可枚举标志。实际选 API 的先后取决于要复制的是值还是全部描述符:普通场景用 Object.keys 最简单,既不需要 hasOwnProperty 也不需要 in,语义最容易维护。
失效条件:空原型与覆写的 hasOwnProperty
Object.create(null) 创建的对象没有任何原型方法,直接调用 obj.hasOwnProperty('x') 会抛出 TypeError,因为根本不存在这个方法。对于这种哈希映射,in 依然可以工作,但无法判断自有键;真正需要自有判断时必须使用 Object.hasOwn,因为它是静态函数,不依赖目标对象的原型链。这也是 Object.hasOwn 被引入的主要原因之一。
另一个失效点是某段代码在 Object.prototype 或中间原型上重写了 hasOwnProperty。比如把 hasOwnProperty 改成永远返回 false,调用 obj.hasOwnProperty('x') 会被欺骗。如果环境不支持 Object.hasOwn,安全写法是借用纯净的原方法:Object.prototype.hasOwnProperty.call(obj, 'x')。这一步要求小心传参,不能直接写成 obj.hasOwnProperty.call(obj, 'x'),因为原始引用可能也被污染。
容易答错的地方
- 把 in 当作自有判断
- “用
if ('foo' in obj)就能判断是否有自己属性”是错的。in查找整个原型链,对{}测'toString'返回true,但toString并非自有属性。检测自有属性需要Object.hasOwn或借用Object.prototype.hasOwnProperty.call。 - hasOwnProperty 一定安全
- 原生
hasOwnProperty会被对象覆写,也可能因创建时未继承Object.prototype而缺失。把它当成万能方法会踩坑。更可靠的是Object.hasOwn,若必须兼容旧环境,应写Object.prototype.hasOwnProperty.call(obj, key)并接受 call 中的 this 指向。
面试官还会怎么问?
`Object.keys` 和 `hasOwnProperty` 的返回范围有什么差别?
Object.keys 返回自身可枚举字符串键组成的数组;hasOwnProperty 返回单个属性是否存在的布尔值,且不关心 enumerable。若属性不可枚举,Object.keys 不列出,但 hasOwnProperty 仍返回 true。
要判断属性来自原型链的哪一层,怎么办?
从当前对象开始,逐层调用 Object.getOwnPropertyNames(obj) 检查键,再用 Object.getPrototypeOf(obj) 向上推进。hasOwn 只告诉是否存在,不能指出层级,该场景需要遍历原型链。
当属性值为 `undefined` 时,`in` 和 `hasOwnProperty` 是否都返回 `true`?
只要属性名存在,两者都返回 true,因为它们判断的是“拥有该名称”,而不是判断值是否非空。若想区分“值为 undefined”和“无此名称”,可以用 Object.keys 或 Reflect.ownKeys 查看键列表。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。