Skip to content

对象属性与描述符

平时写对象属性时,最常看到的是“键值对”。但在 JavaScript 底层,一个属性除了值本身,还带着一组描述信息。

属性不只有 value

最常见的数据属性,通常还带有三个标志:

  • writable:能不能重新赋值
  • enumerable:会不会出现在遍历结果里
  • configurable:能不能继续修改描述符本身,或删除这个属性

查看属性描述符

可以用 Object.getOwnPropertyDescriptor() 查看一个属性的完整描述:

js
const user = {
  name: 'Lee',
}

console.log(Object.getOwnPropertyDescriptor(user, 'name'))

普通直接声明出来的属性,默认通常是:

  • writable: true
  • enumerable: true
  • configurable: true

使用 defineProperty

如果想精确控制属性行为,通常使用 Object.defineProperty()

js
const user = {}

Object.defineProperty(user, 'name', {
  value: 'Lee',
  writable: false,
  enumerable: true,
  configurable: true,
})

这时 name 就不再是一个普通可写属性了。

writable

writable: false 表示这个属性不能被重新赋值:

js
const user = {}

Object.defineProperty(user, 'name', {
  value: 'Lee',
  writable: false,
})

user.name = 'Fan'

在严格模式下,这类写入会报错;在非严格模式下通常会静默失败。

enumerable

enumerable: false 表示这个属性不会出现在枚举结果里:

js
const user = {
  name: 'Lee',
  toString() {
    return this.name
  },
}

Object.defineProperty(user, 'toString', {
  enumerable: false,
})

console.log(Object.keys(user)) // ['name']

这会影响:

  • for...in
  • Object.keys()
  • Object.entries()

configurable

configurable: false 表示这个属性不能再被删除,也不能继续改描述符的大部分配置。

js
const user = {}

Object.defineProperty(user, 'name', {
  value: 'Lee',
  configurable: false,
})

delete user.name // false 或报错

可以把它理解成“这个属性的结构已经基本锁死”。

数据属性和访问器属性

对象属性大致可以分成两类:

  • 数据属性:有 value
  • 访问器属性:有 get / set

这两类描述符不是简单叠加关系。

getter 和 setter

访问器属性看起来像普通属性,但读写时实际会触发函数:

js
const user = {
  name: 'Lee',
  surname: 'Fan',
  get fullName() {
    return `${this.name} ${this.surname}`
  },
  set fullName(value) {
    ;[this.name, this.surname] = value.split(' ')
  },
}

console.log(user.fullName)
user.fullName = 'V Fan'

这类属性很适合:

  • 派生值
  • 读写校验
  • 对外暴露统一接口

访问器描述符

访问器属性没有:

  • value
  • writable

它有的是:

  • get
  • set
  • enumerable
  • configurable

也可以通过 defineProperty() 创建:

js
const user = {
  name: 'Lee',
  surname: 'Fan',
}

Object.defineProperty(user, 'fullName', {
  get() {
    return `${this.name} ${this.surname}`
  },
  set(value) {
    ;[this.name, this.surname] = value.split(' ')
  },
  enumerable: true,
  configurable: true,
})

一个常见用法:只读派生属性

js
function User(name, birthday) {
  this.name = name
  this.birthday = birthday

  Object.defineProperty(this, 'age', {
    get() {
      const currentYear = new Date().getFullYear()
      return currentYear - this.birthday.getFullYear()
    },
  })
}

这里的 age 不是直接存储的值,而是读取时即时计算的结果。

和普通赋值的区别

直接写:

js
user.name = 'Lee'

更适合日常普通属性。

defineProperty() 更适合:

  • 限制写入
  • 控制枚举行为
  • 暴露访问器
  • 做底层封装

使用边界

属性描述符很强,但不是所有属性都需要这么设计。

如果只是普通业务字段,直接赋值通常最清晰。

只有在确实要控制属性行为时,再考虑:

  • 不可写
  • 不可枚举
  • 不可配置
  • getter / setter

使用建议

  • 日常属性优先用普通对象写法。
  • 需要控制属性行为时,再想到 Object.defineProperty()
  • 区分数据属性和访问器属性,不要把 valueget/set 混在一起理解。
  • 写 getter / setter 时,先确认这是不是“属性语义”,而不是普通方法更合适。

基于 MIT 许可发布