JavaScript 对象属性描述符与不可扩展对象
在 JavaScript 中,对象不仅仅是一个键值对的集合。每一个属性背后都隐藏着一套控制机制,决定了这个属性是否可以被修改、被遍历或被删除。掌握这些机制,能让你精确控制对象的行为,防止代码被意外篡改。
1. 查看属性的“身份证”
每个属性都有一个“描述符”对象,它记录了该属性的元数据。默认情况下,直接通过字面量创建的属性,其描述符通常是特定的默认值。
打开浏览器的开发者工具控制台(Console),输入以下代码并按下回车:
const person = {
name: "Alice"
};
// 获取 name 属性的描述符
const descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor);
此时控制台会输出一个对象,包含 value(值)、writable(是否可写)、enumerable(是否可枚举)、configurable(是否可配置)这四个核心属性。
2. 精细控制属性特征
通过 Object.defineProperty 方法,你可以修改或定义属性的这些特征。这是修改属性“底层设置”的最底层 API。
复制并在控制台运行以下代码:
const product = {};
// 定义一个 price 属性
Object.defineProperty(product, 'price', {
value: 99.9,
writable: false, // 设置为不可写
enumerable: true, // 设置为可枚举
configurable: false // 设置为不可配置
});
console.log(product.price); // 输出 99.9
product.price = 199.9; // 尝试修改
console.log(product.price); // 依然是 99.9,因为 writable 为 false
在上面的代码中,即使尝试对 price 赋值新数值,由于我们将 writable 设置为 false,修改操作会静默失败(在非严格模式下)。如果是在严格模式下(use strict),这行代码会抛出错误。
理解以下四个关键字至关重要:
| 属性名 | 作用说明 |
|---|---|
value |
属性的具体数值。 |
writable |
为 false 时,属性值无法被修改(只读)。 |
enumerable |
为 false 时,属性不会出现在 for...in 循环或 Object.keys() 中。 |
configurable |
为 false 时,属性无法被删除,且无法修改其描述符(除了将 writable 从 true 改为 false)。 |
3. 使用存取器保护数据
除了直接存储值,属性还可以通过 getter 和 getter 函数来动态计算值或拦截赋值操作。这类似于其他语言中的“私有变量”保护模式。
输入以下代码体验存取器:
const account = {
_balance: 1000 // 约定以下划线开头表示“内部”属性
};
Object.defineProperty(account, 'balance', {
get: function() {
return this._balance;
},
set: function(value) {
if (value < 0) {
console.error("余额不能为负数");
return;
}
this._balance = value;
},
enumerable: true
});
account.balance = 500; // 正常修改
console.log(account.balance); // 输出 500
account.balance = -100; // 尝试设为负数
console.log(account.balance); // 依然输出 500,并被拦截
注意,存取器属性(getter/setter)不包含 value 和 writable 属性。
4. 锁定对象的三个层级
JavaScript 提供了三个逐步增强的方法,用来限制对对象结构的修改,从“禁止新增”到“完全冻结”。
4.1 防止扩展:Object.preventExtensions
这是最轻量级的锁定。它阻止向对象添加新属性,但现有属性依然可以修改和删除。
运行以下代码测试:
const obj1 = { a: 1 };
Object.preventExtensions(obj1);
obj1.b = 2; // 尝试添加新属性(静默失败)
console.log(obj1.b); // 输出 undefined
obj1.a = 2; // 修改现有属性(成功)
console.log(obj1.a); // 输出 2
4.2 密封对象:Object.seal
密封相当于在“防止扩展”的基础上,将所有现有属性的 configurable 标记为 false。这意味着你不能添加新属性,也不能删除现有属性。
运行以下代码测试:
const obj2 = { a: 1 };
Object.seal(obj2);
delete obj2.a; // 尝试删除属性(静默失败)
console.log(obj2.a); // 输出 1
obj2.a = 2; // 修改属性值(成功,只要 writable 为 true)
console.log(obj2.a); // 输出 2
4.3 冻结对象:Object.freeze
这是最高级别的锁定。它调用 Object.seal,同时将所有现有属性的 writable 设置为 false。对象变为完全只读。
运行以下代码测试:
const obj3 = { a: 1 };
Object.freeze(obj3);
obj3.a = 2; // 尝试修改(失败)
delete obj3.a; // 尝试删除(失败)
obj3.b = 3; // 尝试添加(失败)
console.log(obj3); // 输出 { a: 1 }
5. 对象锁定层级可视化
为了直观理解这三者的区别与包含关系,请参考下方的层级图。随着层级的加深,对象的权限越来越小,安全性越来越高。
6. 快速对比表
下表总结了这三种锁定方式对属性操作的具体影响:
| 操作类型 | 普通对象 | preventExtensions |
seal (密封) |
freeze (冻结) |
|---|---|---|---|---|
| 读取 | ✅ | ✅ | ✅ | ✅ |
| 修改值 | ✅ | ✅ | ✅* | ❌ |
| 新增属性 | ✅ | ❌ | ❌ | ❌ |
| 删除属性 | ✅ | ✅ | ❌ | ❌ |
| 修改描述符 | ✅ | ✅ | ❌ | ❌ |
*注:如果属性的 writable 被手动设为 false,则密封对象也不能修改值。
使用 Object.isExtensible()、Object.isSealed() 和 Object.isFrozen() 可以分别检查对象当前处于哪种状态。

暂无评论,快来抢沙发吧!