JavaScript 属性描述符是什么?defineProperty 能控制哪些行为?
简化版
属性描述符描述的是对象属性的“元信息”。普通数据属性可以控制 value、writable、enumerable、configurable;访问器属性可以控制 get、set、enumerable、configurable。
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
});
需要注意:同一个属性不能同时是数据属性和访问器属性,也就是不能同时写 value 和 get。通过 defineProperty 新增属性时,如果不显式写布尔配置,writable、enumerable、configurable 默认都是 false。
完整版教学
一、属性描述符解决的核心问题
JavaScript 对象看起来像键值表,但属性实际有两层信息:第一层是业务值,第二层是这个属性的行为规则。
比如 user.id = 1 只看到了值,但引擎还要判断:
- 能不能改这个值
- 能不能被
for...in遍历出来 - 能不能被
delete删除 - 能不能重新改成 getter 或 setter
属性描述符就是这些规则的结构化表达。
面试里问属性描述符,本质是在问你是否理解“属性”和“变量”不是一回事,对象属性有自己的可配置行为。
二、两类属性:数据属性和访问器属性
| 类型 | 关键字段 | 代表含义 |
|---|---|---|
| 数据属性 | value、writable | 属性直接保存一个值 |
| 访问器属性 | get、set | 读取或写入时触发函数 |
| 共同字段 | enumerable、configurable | 是否可枚举、是否可重新配置 |
数据属性像普通储物格,访问器属性像门口有一个检查员。
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.keys、for...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.getOwnPropertyDescriptors 和 Object.defineProperties。
const clone = Object.defineProperties(
{},
Object.getOwnPropertyDescriptors(source)
);
七、常见误区与追问
- 误区:属性描述符只影响能不能赋值。 它还影响枚举、删除、重新定义和访问器行为。
- 误区:
defineProperty新增属性默认可以枚举。 默认enumerable是false。 - 误区:
configurable: false只是不能 delete。 它还限制后续描述符修改。 - 误区:getter 一定会存一个值。 getter 是读取时执行函数,不一定有独立存储。
- 追问:数据属性和访问器属性能混用吗? 不能,
value/writable和get/set属于两套描述符。 - 追问:为什么 Vue2 对数组和新增属性处理复杂? 因为
defineProperty只能劫持已定义属性,不代理整个对象。
八、加强记忆
记住一条线:对象属性 = 值 + 行为开关。
property
├─ data: value + writable
├─ accessor: get + set
└─ flags: enumerable + configurable
看到 Object.defineProperty,优先检查四件事:有没有默认 false、是不是可枚举、能不能重新配置、数据属性和访问器属性有没有混写。面试回答时先分两类属性,再解释三个开关,最后补 Vue2 或库内部隐藏字段的实际场景。