5 种 JS 判断对象属性是否存在的方案
Category(分类): JavaScript Status: 已更新
原文作者:程序员半夏
历史文章:5 种 JS 判断对象属性是否存在的方案
原文整理了
in、Reflect.has()、hasOwnProperty()、Object.prototype.hasOwnProperty.call()和Object.hasOwn()五种写法。本文保留原来的学习顺序,并补充空原型对象、Symbol 属性、继承属性和现代代码中的选择原则。
背景:先说清楚“存在”是什么意思
JavaScript 中至少有两种常见问题:
- 属性是否存在于对象及其原型链上:使用
in或Reflect.has(); - 属性是否是对象自己的属性:使用
Object.hasOwn()或Object.prototype.hasOwnProperty.call()。
不要把下面这种“值不是 undefined”的判断当作通用属性存在性检查:
const object = { count: 0, value: undefined }
object.count !== undefined // true:值为 0 不会导致假阴性
object.value !== undefined // false negative:属性存在,但值恰好是 undefined
object.missing !== undefined // false
undefined 可能本来就是属性值,所以“值不是 undefined”不能等价于“属性存在”。
下面的示例对象有自己的 name,同时从原型继承了 toString:
const user = { name: '前端开发者' }
console.log('name' in user) // true
console.log('toString' in user) // true
console.log(Object.hasOwn(user, 'name')) // true
console.log(Object.hasOwn(user, 'toString')) // false
1. in 运算符:检查自身属性和继承属性
property in object 会沿着对象的原型链查找属性。左侧可以是字符串或 Symbol,右侧必须是对象或函数:
'name' in { name: '前端开发者' } // true
'age' in { name: '前端开发者' } // false
'toString' in {} // true,因为它来自 Object.prototype
in 判断的是属性键,不会读取属性值,也不会因为值是 undefined 就返回 false:
const settings = { value: undefined }
'value' in settings // true
'missing' in settings // false
数组也可以使用 in,但它检查的是索引属性,而不是“值是否在数组中”:
const values = []
values[1] = undefined
0 in values // false:索引 0 是空位
1 in values // true:索引 1 是一个实际属性
'length' in values // true
如果只是想判断数组是否包含某个值,应使用 includes();如果想判断某个索引是否有属性,才考虑 index in array。
in 还可以用于私有字段的品牌检查,但那是另一种语法:
class User {
#id
static hasBrand(value) {
return #id in value
}
}
普通属性判断仍然使用字符串或 Symbol 作为左操作数。
2. Reflect.has():in 的函数形式
Reflect.has(object, propertyKey) 与 propertyKey in object 的属性查找语义相同:都会检查原型链:
const user = { name: '前端开发者' }
Reflect.has(user, 'name') // true
Reflect.has(user, 'toString') // true
Reflect.has(user, 'age') // false
它适合已经采用 Reflect 风格 API 的元编程代码,也方便作为回调传递。普通业务代码使用 in 或 Object.hasOwn() 通常更直观。
const key = Symbol('cache')
const cache = { [key]: new Map() }
Reflect.has(cache, key) // true
Reflect.has() 与 in 一样,要求第一个参数是对象;传入 null 或 undefined 会抛出 TypeError。
3. hasOwnProperty():传统的自身属性检查
hasOwnProperty() 只检查对象自己的属性,不检查原型链:
const user = { name: '前端开发者' }
user.hasOwnProperty('name') // true
user.hasOwnProperty('toString') // false
但不建议无条件直接调用 object.hasOwnProperty(key),因为对象可能没有 Object.prototype,或者自己的属性覆盖了这个方法。
3.1 Object.create(null) 没有 hasOwnProperty
const dictionary = Object.create(null)
dictionary.name = '前端开发者'
// dictionary.hasOwnProperty('name')
// TypeError: dictionary.hasOwnProperty is not a function
3.2 自身属性可能覆盖同名方法
const object = {
hasOwnProperty: '这是一个普通属性',
name: '前端开发者'
}
// object.hasOwnProperty('name')
// TypeError: object.hasOwnProperty is not a function
因此,hasOwnProperty() 不是错误,但直接从不可信对象上取方法并调用并不安全。
4. Object.prototype.hasOwnProperty.call():兼容旧环境的安全写法
可以从稳定的原型方法上取出函数,再通过 call() 指定 this:
function hasOwnLegacy(object, propertyKey) {
return Object.prototype.hasOwnProperty.call(object, propertyKey)
}
hasOwnLegacy({ name: '前端开发者' }, 'name') // true
hasOwnLegacy({ hasOwnProperty: '覆盖了方法' }, 'name') // false
hasOwnLegacy(Object.create(null), 'name') // false
hasOwnLegacy({ toString: null }, 'toString') // true
这种写法可以处理空原型对象、覆盖同名属性和 Symbol 键:
const token = Symbol('token')
const object = { [token]: 123 }
Object.prototype.hasOwnProperty.call(object, token) // true
如果需要支持很老的 JavaScript 运行环境,这是可靠的兼容写法;现代代码通常优先使用下一节的 Object.hasOwn()。
5. Object.hasOwn():现代的自身属性检查
Object.hasOwn(object, propertyKey) 在 ECMAScript 2022 中标准化,用来判断自身属性,语义上替代了常见的:
Object.prototype.hasOwnProperty.call(object, propertyKey)
示例:
const user = { name: '前端开发者' }
Object.hasOwn(user, 'name') // true
Object.hasOwn(user, 'toString') // false
Object.hasOwn(Object.create(null), 'name') // false
Object.hasOwn({ hasOwn: 'yes' }, 'hasOwn') // true
它也支持 Symbol 属性:
const id = Symbol('id')
const record = { [id]: 1 }
Object.hasOwn(record, id) // true
Object.hasOwn() 只判断自身属性,不判断属性是否可枚举、是否可写,也不会读取 getter 的返回值。需要检查这些信息时,应使用属性描述符:
const descriptor = Object.getOwnPropertyDescriptor(record, id)
console.log(descriptor?.enumerable)
对比表
| 方式 | 检查原型链 | 检查自身属性 | 支持 Symbol | 推荐场景 |
|---|---|---|---|---|
key in object | 是 | 是 | 是 | 明确需要判断继承属性 |
Reflect.has(object, key) | 是 | 是 | 是 | Reflect/元编程代码 |
object.hasOwnProperty(key) | 否 | 是 | 是 | 仅限确定对象没有覆盖方法的场景 |
Object.prototype.hasOwnProperty.call(...) | 否 | 是 | 是 | 旧环境或通用兼容函数 |
Object.hasOwn(object, key) | 否 | 是 | 是 | 现代业务代码首选 |
实际选择建议
function hasProperty(object, key) {
// 需要包含继承属性
return key in object
}
function hasOwnPropertySafe(object, key) {
// 现代环境
return Object.hasOwn(object, key)
}
- 判断配置对象是否明确写入了某个字段:使用
Object.hasOwn(); - 判断对象是否能响应某个继承来的方法:使用
in或Reflect.has(),但仍要考虑属性值是否可调用; - 处理 JSON、字典或外部输入时,不要假设对象一定继承自
Object.prototype; - 需要兼容
Object.hasOwn()不存在的旧运行环境时,使用Object.prototype.hasOwnProperty.call(); - 如果只想遍历可枚举自身字符串键,使用
Object.keys();如果还要包括 Symbol 键,使用Reflect.ownKeys(),它们解决的是遍历问题,不是简单的属性存在性判断。
结论
原文中的五种方法仍然有学习价值,但现代项目不必平均使用它们:
- 需要查原型链:
in或Reflect.has(); - 需要查自身属性:优先
Object.hasOwn(); - 兼容旧环境:
Object.prototype.hasOwnProperty.call(); - 不要用
value !== undefined代替属性存在性判断; - 不要直接调用不可信对象上的
hasOwnProperty()。