在 JavaScript 里,对象的属性其实分为两种截然不同的"物种":数据属性(Data Property) 和 访问器属性(Accessor Property) 。
1.什么是数据属性?(你平时写的那个)
当你写:
javascript
class User {
username: string;
}
或者
javascript
const obj = { username: '' };
时,JS 引擎在底层把 username 定义为一个数据属性(Data Property) 。
- 数据属性里有一个叫
value的字段,直接存着''。 - 你写
obj.username = 'Alice',引擎是直接去修改那个value字段。没有函数参与,没有拦截,简单粗暴,也不需要 get/set。
2. 什么是访问器属性?
访问器属性根本不存值 。它是一个自动售货机 。
你没有储物柜,只有两个按钮:一个叫 get(取货),一个叫 set(存货)。
当你读这个属性时,引擎发现它是个访问器属性,就会去按 get 按钮,执行你写的 getter 函数。
当你写这个属性时,引擎就会去按 set 按钮,执行你写的 setter 函数。
例子(字面量写法):
javascript
javascript
const user = {
_name: 'Alice', // 这是个真正存值的普通属性
get name() {
console.log('正在读取 name');
return this._name;
},
set name(newVal) {
console.log('正在修改 name');
this._name = newVal;
}
};
user.name; // 触发 get,打印:正在读取 name
user.name = 'Bob'; // 触发 set,打印:正在修改 name
这里的 name 就是一个访问器属性,它本身没有值,值存在 _name 里。
3. 核心区别
| 特征 | 数据属性 (Data Property) | 访问器属性 (Accessor Property) |
|---|---|---|
| 本质 | 储物柜 | 自动售货机 |
是否有 value |
有,直接存值 | 没有,值由外部或闭包保存 |
是否有 get/set |
没有 | 有,读写全由函数接管 |
| 读写行为 | 直接读/写内存里的值 | 触发函数调用,执行代码逻辑 |
| 能否自定义行为 | 不能,赋值就是纯粹赋值 | 能,可以在读/写时做任何事(校验、日志、计算) |
4. 为什么要有访问器属性?它有什么用?
它赋予了开发者 "拦截并控制属性读写" 的超能力。比如:
场景 A:数据校验(防瞎填)
javascript
kotlin
const person = {
_age: 0,
set age(val) {
if (val < 0 || val > 150) {
throw new Error('年龄不合法!');
}
this._age = val;
},
get age() {
return this._age;
}
};
person.age = -5; // 报错:年龄不合法!
场景 B:计算属性(互相依赖)
javascript
arduino
const rect = {
width: 10,
height: 20,
get area() {
return this.width * this.height; // 每次读取,动态计算
}
};
console.log(rect.area); // 200
area 并不存值,它是通过 width 和 height 实时算出来的。
5. 为什么访问器属性自己没地方存值?
要理解这个,我们要回到 JS 引擎的底层设计。在 JS 中,属性描述符(Property Descriptor)是有固定形状(Shape) 的,它严格分为两套互相排斥的配置:
- 数据属性 的配置:
{ value, writable, enumerable, configurable } - 访问器属性 的配置:
{ get, set, enumerable, configurable }
JS 引擎有一条铁律:绝对不允许同时出现 value 和 get/set。
如果你强行在 Object.defineProperty 里同时写:
javascript
javascript
Object.defineProperty(target, 'username', {
value: '游客', // 试图存值
get() { return 'Alice'; } // 试图拦截
});
// 引擎会直接抛出 TypeError,报错说:Invalid property descriptor. Cannot both specify accessors and a value or writable attribute.
为什么引擎要这么设计?(结合比喻)
- 数据属性 就像一个储物柜 。引擎分配了一块内存存
value。 - 访问器属性 就像一个自动售货机 。它只提供了
get(出货按钮)和set(进货按钮)。
如果你把属性定义成了访问器属性(安装上了按钮),引擎就会在底层把这个属性标记为"访问器"。
此时,它不再分配用来存 value 的内存槽位 。
如果强行赋值 u2.username = 'Alice',引擎只会去按 set 按钮。如果没写 set,引擎既没按钮可按,又没 value 可改,只能报错。
所以,不是它"没地方存",而是 JS 引擎在底层强行把它的"存储槽位"砍掉了,逼迫你必须自己找地方存。 最常见的地方就是闭包变量 或者WeakMap。