← 返回题目列表

JavaScript 属性描述符是什么?defineProperty 能控制哪些行为?

中等 第 21 / 29 题 更新于 2026/07/29
JavaScript对象属性描述符

简化版

属性描述符描述的是对象属性的“元信息”。普通数据属性可以控制 valuewritableenumerableconfigurable;访问器属性可以控制 getsetenumerableconfigurable

Object.defineProperty 可以精确修改这些元信息。它常用于隐藏属性、只读属性、兼容性响应式实现、库内部 API 保护等场景。

详细版

对象属性不只有值,还有能不能改、能不能枚举、能不能重新配置等行为。

数据属性示例:

const user = {};

Object.defineProperty(user, "id", {
  value: 1001,
  writable: false,
  enumerable: true,
  configurable: false
});

user.id = 2002;
console.log(user.id); // 1001,严格模式下会抛错

访问器属性示例:

const state = { first: "Ada", last: "Lovelace" };

Object.defineProperty(state, "fullName", {
  get() {
    return `${this.first} ${this.last}`;
  },
  set(value) {
    [this.first, this.last] = value.split(" ");
  },
  enumerable: true,
  configurable: true
});

需要注意:同一个属性不能同时是数据属性和访问器属性,也就是不能同时写 valueget。通过 defineProperty 新增属性时,如果不显式写布尔配置,writableenumerableconfigurable 默认都是 false

完整版教学

一、属性描述符解决的核心问题

JavaScript 对象看起来像键值表,但属性实际有两层信息:第一层是业务值,第二层是这个属性的行为规则。

比如 user.id = 1 只看到了值,但引擎还要判断:

  • 能不能改这个值
  • 能不能被 for...in 遍历出来
  • 能不能被 delete 删除
  • 能不能重新改成 getter 或 setter

属性描述符就是这些规则的结构化表达。

面试里问属性描述符,本质是在问你是否理解“属性”和“变量”不是一回事,对象属性有自己的可配置行为。

二、两类属性:数据属性和访问器属性

类型关键字段代表含义
数据属性valuewritable属性直接保存一个值
访问器属性getset读取或写入时触发函数
共同字段enumerableconfigurable是否可枚举、是否可重新配置

数据属性像普通储物格,访问器属性像门口有一个检查员。

const obj = {};

Object.defineProperty(obj, "score", {
  value: 90,
  writable: true
});

Object.defineProperty(obj, "level", {
  get() {
    return this.score >= 60 ? "pass" : "fail";
  }
});

score 存值,level 不存固定值,而是每次读取时计算。

三、三个布尔开关分别控制什么

writable 控制数据属性的值能否被赋值修改。

enumerable 控制属性是否出现在 Object.keysfor...in、对象展开等枚举场景中。

configurable 控制属性能否被删除,以及能否重新定义描述符。它一旦变成 false,通常就很难再回头。

const obj = {};
Object.defineProperty(obj, "token", {
  value: "abc",
  enumerable: false,
  configurable: false
});

console.log(Object.keys(obj)); // []
delete obj.token;
console.log(obj.token); // "abc"

这里 token 仍然存在,只是不参与常规枚举。

四、默认值是最容易踩的坑

对象字面量创建的属性,三个布尔值通常都是 true

const a = { x: 1 };
console.log(Object.getOwnPropertyDescriptor(a, "x"));

defineProperty 新增属性时,没写的布尔值默认是 false

const b = {};
Object.defineProperty(b, "x", { value: 1 });

console.log(Object.keys(b)); // []
b.x = 2;
console.log(b.x); // 1

同样是 x: 1,行为完全不同。这也是很多库用 defineProperty 做内部隐藏字段的原因。

五、和 Vue2 响应式的关系

Vue2 使用 Object.defineProperty 劫持对象属性的读取和设置。

function reactive(obj, key) {
  let value = obj[key];

  Object.defineProperty(obj, key, {
    get() {
      console.log("collect dependency");
      return value;
    },
    set(next) {
      value = next;
      console.log("trigger update");
    }
  });
}

这解释了 Vue2 对新增属性、数组索引变更等场景需要额外处理,因为 defineProperty 是针对已有属性逐个劫持,不是代理整个对象。

六、判断和复制属性时要看描述符

如果只用赋值复制对象,属性描述符会丢失。

const source = {};
Object.defineProperty(source, "id", {
  value: 1,
  enumerable: false
});

const copy = { ...source };
console.log(copy.id); // undefined

要完整复制,可以组合 Object.getOwnPropertyDescriptorsObject.defineProperties

const clone = Object.defineProperties(
  {},
  Object.getOwnPropertyDescriptors(source)
);

七、常见误区与追问

  • 误区:属性描述符只影响能不能赋值。 它还影响枚举、删除、重新定义和访问器行为。
  • 误区:defineProperty 新增属性默认可以枚举。 默认 enumerablefalse
  • 误区:configurable: false 只是不能 delete。 它还限制后续描述符修改。
  • 误区:getter 一定会存一个值。 getter 是读取时执行函数,不一定有独立存储。
  • 追问:数据属性和访问器属性能混用吗? 不能,value/writableget/set 属于两套描述符。
  • 追问:为什么 Vue2 对数组和新增属性处理复杂? 因为 defineProperty 只能劫持已定义属性,不代理整个对象。

八、加强记忆

记住一条线:对象属性 = 值 + 行为开关。

property
  ├─ data: value + writable
  ├─ accessor: get + set
  └─ flags: enumerable + configurable

看到 Object.defineProperty,优先检查四件事:有没有默认 false、是不是可枚举、能不能重新配置、数据属性和访问器属性有没有混写。面试回答时先分两类属性,再解释三个开关,最后补 Vue2 或库内部隐藏字段的实际场景。