技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

为对象添加某个属性

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/78-为对象添加某个属性
本文目录9 个章节
  1. 一、点语法与方括号语法
  2. 二、Object.defineProperty:精细控制属性描述符
  3. 三、Object.assign 与对象展开
  4. 四、对象方法与原型属性
  5. 五、创建对象时定义属性
  6. 六、setter 不是通用的“动态加属性”语法
  7. 七、使用 Proxy 拦截属性写入
  8. 八、其他相关操作
  9. 总结

为对象添加某个属性

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']

如果省略数据属性描述符,省略的 writableenumerableconfigurable 默认都是 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.getOwnPropertyDescriptorsObject.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 还会把现有数据属性设为不可写,但它们都只是浅层操作,嵌套对象仍可能被修改。

总结

  1. 固定键使用点语法,动态键、特殊字符串和 symbol 使用方括号。
  2. Object.defineProperty 用于精确控制值、枚举性、可写性和访问器。
  3. Object.assign 和对象展开都会浅拷贝,并且前者会修改目标对象。
  4. 共享行为可以使用原型或 class,不要随意修改全局原型。
  5. setter 和 Proxy 是特定机制,不是普通添加属性的替代品。
  6. 处理不可信键时防范 __proto__ 原型污染,并优先使用无原型对象或明确的数据属性。

参考资料:

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS