为对象添加某个属性
Category(分类): JavaScript Status: 已整理
JavaScript 对象属性的键只能是字符串或 symbol。点语法适合固定且合法的标识符,方括号语法适合动态键、带空格的键和 symbol 键。下面保留原文列出的多种方式,并补充它们在描述符、原型、浅拷贝和代理方面的差异。
一、点语法与方括号语法
const object = {}
object.name = 'Ada'
console.log(object.name) // Ada
const key = 'age'
object[key] = 18
object['user-name'] = 'ada@example.com'
console.log(object.age) // 18
console.log(object['user-name']) // ada@example.com
点语法右侧必须是一个合法的标识符名称,不能直接写 object.user-name 来访问带连字符的属性。方括号中的表达式会转换为属性键:
const object = {}
const symbolKey = Symbol('id')
object[symbolKey] = 100
object[1] = 'number key becomes a string key'
console.log(object[symbolKey]) // 100
console.log(object['1']) // number key becomes a string key
console.log(Object.getOwnPropertySymbols(object)) // [Symbol(id)]
使用不可信的动态键
普通对象有继承自 Object.prototype 的特殊访问器。把不可信字符串直接赋给 object['__proto__'] 可能改变对象原型,形成原型污染风险。处理外部键时,应校验键名,或使用无原型对象并明确创建数据属性:
const data = Object.create(null)
const key = '__proto__'
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
value: 'ordinary data',
writable: true,
})
console.log(data[key]) // ordinary data
console.log(Object.getPrototypeOf(data)) // null
二、Object.defineProperty:精细控制属性描述符
赋值语法创建的普通数据属性通常是可写、可枚举、可配置的。Object.defineProperty 可以显式设置这些属性特征:
const object = {}
Object.defineProperty(object, 'name', {
configurable: true,
enumerable: true,
value: 'Ada',
writable: true,
})
console.log(object.name) // Ada
console.log(Object.keys(object)) // ['name']
如果省略数据属性描述符,省略的 writable、enumerable 和 configurable 默认都是 false:
Object.defineProperty(object, 'hidden', {
value: 'secret',
})
console.log(object.hidden) // secret
console.log(Object.keys(object)) // ['name']
console.log(Object.getOwnPropertyDescriptor(object, 'hidden'))
// { value: 'secret', writable: false, enumerable: false, configurable: false }
严格模式下修改不可写属性或删除不可配置属性会抛出 TypeError;非严格模式可能静默失败。对象不可扩展、已密封或已冻结时,新增或修改属性也可能失败。
数据描述符和访问器描述符不能在同一个描述符中混用:
const user = {
_name: 'Ada',
}
Object.defineProperty(user, 'name', {
configurable: true,
enumerable: true,
get() {
return this._name
},
set(value) {
this._name = String(value)
},
})
user.name = 'Grace'
console.log(user.name) // Grace
console.log(Object.keys(user)) // ['_name', 'name']
三、Object.assign 与对象展开
Object.assign(target, source1, source2) 会把源对象的可枚举自有字符串键和 symbol 键读取后写入目标对象,并返回目标对象。它会修改第一个参数,而且只进行浅拷贝:
const target = { name: 'Ada' }
const source = {
age: 18,
profile: { language: 'JavaScript' },
}
const result = Object.assign(target, source)
console.log(result === target) // true
console.log(target.age) // 18
source.profile.language = 'TypeScript'
console.log(target.profile.language) // TypeScript:嵌套对象仍是同一个引用
对象展开通常用于创建一个新对象,也同样是浅拷贝;它不会复制源对象的原型和不可枚举属性:
const original = { existing: 'value' }
const updated = { ...original, added: true }
const replaced = { ...updated, existing: 'new value' }
console.log(updated) // { existing: 'value', added: true }
console.log(replaced.existing) // new value:后面的键覆盖前面的键
console.log(updated === original) // false
两者还会触发源对象的 getter,并在目标对象上创建普通数据属性;不要把它们当成“复制完整对象状态”或深拷贝工具。还要注意:Object.assign 写入目标时走 [[Set]],来自外部数据的自有 __proto__ 键在普通对象上可能触发遗留 setter;对象 spread 创建数据属性时不会触发这个目标 setter,但二者都不能替代输入校验。需要保留完整描述符时,可使用 Object.getOwnPropertyDescriptors 与 Object.defineProperties:
const source = {}
Object.defineProperty(source, 'hidden', {
configurable: true,
enumerable: false,
value: 1,
writable: false,
})
const cloneWithDescriptors = Object.defineProperties(
{},
Object.getOwnPropertyDescriptors(source)
)
console.log(Object.getOwnPropertyDescriptor(cloneWithDescriptors, 'hidden').writable)
// false
四、对象方法与原型属性
方法只是对象上的函数属性,可以直接添加:
const object = {}
object.sayHello = function () {
return 'Hello, world!'
}
console.log(object.sayHello()) // Hello, world!
如果许多实例共享同一个方法,应把方法放到原型或使用 class 方法,而不是在每次构造时创建新的函数:
class User {
constructor(name) {
this.name = name
}
greet() {
return `Hello, ${this.name}`
}
}
const ada = new User('Ada')
const grace = new User('Grace')
console.log(ada.greet()) // Hello, Ada
console.log(ada.greet === grace.greet) // true
也可以直接向自定义原型添加属性,但它会被所有相关实例共享;如果属性是可变对象,实例之间可能意外共享状态:
function MyObject() {}
MyObject.prototype.kind = 'shared value'
const first = new MyObject()
const second = new MyObject()
console.log(first.kind) // shared value
console.log(Object.hasOwn(first, 'kind')) // false
first.kind = 'own value'
console.log(second.kind) // shared value
修改 Object.prototype 或其他全局原型通常不推荐,因为它会影响大量无关对象,还可能造成属性名冲突。若只是给一组实例提供行为,优先使用自己的原型、class 或组合对象。
五、创建对象时定义属性
对象字面量适合同时声明初始属性和方法:
const object = {
property1: 'Value1',
property2: 42,
greet() {
return 'hello'
},
}
Object.create(proto, propertiesObject) 可以在指定原型的同时创建属性,但第二个参数使用的是完整属性描述符,而不是普通的 { key: value } 形式:
const parent = { kind: 'parent' }
const child = Object.create(parent, {
name: {
configurable: true,
enumerable: true,
value: 'child',
writable: true,
},
})
console.log(child.kind) // parent
console.log(child.name) // child
console.log(Object.hasOwn(child, 'name')) // true
若只想创建一个普通的新对象并复制数据,应使用对象字面量、展开或明确的赋值;不要误把 Object.create({}, { value: 'x' }) 中的 'value' 当成普通属性值,它会被当作描述符字段。
六、setter 不是通用的“动态加属性”语法
原文用 setter 接收形如 key:value 的字符串。这个做法可以演示访问器,但 setter 的职责通常是校验或转换一个已知属性,而不是替代普通的动态键赋值:
const object = {
_value: '',
set value(nextValue) {
this._value = String(nextValue).trim()
},
get value() {
return this._value
},
}
object.value = ' hello '
console.log(object.value) // hello
如果确实需要从键值对动态创建属性,应直接使用方括号,或在需要控制描述符时使用 Object.defineProperty。setter 赋值还可能调用原型链上的访问器,因此处理外部数据时应明确检查目标对象。
七、使用 Proxy 拦截属性写入
Proxy 可以观察或改变属性写入,但它不是普通添加属性的必要工具。set 陷阱必须返回布尔值;使用 Reflect.set 可以较好地保留原始的原型和 setter 语义:
const target = {}
const proxy = new Proxy(target, {
set(object, property, value, receiver) {
console.log(`setting ${String(property)}`)
return Reflect.set(object, property, value, receiver)
},
})
proxy.count = 1
console.log(target.count) // 1
如果目标对象存在不可配置且不可写的数据属性,或存在不可配置的访问器属性,代理不能通过 set 陷阱违反这些不变式,否则引擎会抛出 TypeError。代理还可能影响调试、性能和第三方库行为,只有在需要校验、响应式追踪、日志或虚拟属性时再使用。
八、其他相关操作
判断属性是否存在
const parent = { inherited: true }
const child = Object.create(parent)
child.own = true
console.log('own' in child) // true:自有或继承
console.log('inherited' in child) // true
console.log(Object.hasOwn(child, 'own')) // true:仅自有
console.log(Object.hasOwn(child, 'inherited')) // false
Object.hasOwn 是现代写法,适用于没有继承 hasOwnProperty 的对象;旧环境可使用 Object.prototype.hasOwnProperty.call(object, key)。
删除属性与不可扩展对象
const object = { removable: true }
Object.preventExtensions(object)
console.log(Reflect.deleteProperty(object, 'removable')) // true
console.log(Reflect.defineProperty(object, 'newKey', { value: 1 })) // false
Object.seal 会阻止新增、删除和改变属性描述配置;Object.freeze 还会把现有数据属性设为不可写,但它们都只是浅层操作,嵌套对象仍可能被修改。
总结
- 固定键使用点语法,动态键、特殊字符串和
symbol使用方括号。 Object.defineProperty用于精确控制值、枚举性、可写性和访问器。Object.assign和对象展开都会浅拷贝,并且前者会修改目标对象。- 共享行为可以使用原型或
class,不要随意修改全局原型。 - setter 和 Proxy 是特定机制,不是普通添加属性的替代品。
- 处理不可信键时防范
__proto__原型污染,并优先使用无原型对象或明确的数据属性。
参考资料: