跳到主要内容

JavaScript 对象密封

在JavaScript中,对象是最基本也是最重要的数据类型之一。有时候,我们需要限制对象的可扩展性,防止添加新属性或删除现有属性,这时就可以使用对象密封(Object Sealing)。

什么是对象密封?

对象密封是JavaScript提供的一种限制对象可变性的方法。当一个对象被密封后:

  • 不能添加新的属性
  • 不能删除现有属性
  • 现有属性的值仍然可以修改
  • 现有属性的特性(如可写性、可枚举性等)不能修改

这与冻结(Object.freeze())不同,冻结会使对象的属性值也不能修改。

如何密封对象

JavaScript提供了Object.seal()方法来密封对象:

javascript
const user = {
name: "张三",
age: 25
};

// 密封对象
Object.seal(user);

// 尝试添加新属性
user.address = "北京市"; // 在严格模式下会抛出错误,非严格模式下静默失败
console.log(user.address); // 输出: undefined

// 尝试删除现有属性
delete user.age; // 在严格模式下会抛出错误,非严格模式下静默失败
console.log(user.age); // 输出: 25

// 修改现有属性的值
user.name = "李四"; // 成功
console.log(user.name); // 输出: "李四"

检查对象是否被密封

你可以使用Object.isSealed()方法检查一个对象是否已经被密封:

javascript
const user = {
name: "张三"
};

console.log(Object.isSealed(user)); // 输出: false

Object.seal(user);
console.log(Object.isSealed(user)); // 输出: true

密封对象的属性描述符

使用Object.getOwnPropertyDescriptor()方法,我们可以查看密封对象的属性描述符:

javascript
const user = {
name: "张三"
};

Object.seal(user);

const descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// 输出: {value: "张三", writable: true, enumerable: true, configurable: false}

注意,密封后属性的configurable特性变为false,这意味着属性不能被删除,且其描述符不能被改变。

备注

被密封的对象的属性的configurable属性都被设置为false,但writable属性保持不变。

对象密封与对象不可扩展的区别

JavaScript提供了三种限制对象可变性的方法,从弱到强依次是:

  1. Object.preventExtensions() - 防止添加新属性
  2. Object.seal() - 防止添加新属性和删除现有属性
  3. Object.freeze() - 防止添加新属性、删除现有属性以及修改现有属性的值

下面是一个比较示例:

javascript
const obj1 = { prop: 1 };
const obj2 = { prop: 1 };
const obj3 = { prop: 1 };

Object.preventExtensions(obj1);
Object.seal(obj2);
Object.freeze(obj3);

// 尝试添加新属性
obj1.newProp = 2;
obj2.newProp = 2;
obj3.newProp = 2;

// 尝试修改现有属性
obj1.prop = 10;
obj2.prop = 10;
obj3.prop = 10;

// 尝试删除现有属性
delete obj1.prop;
delete obj2.prop;
delete obj3.prop;

console.log(obj1); // { prop: 10 } - 只阻止了添加,允许修改和删除
console.log(obj2); // { prop: 10 } - 阻止了添加和删除,允许修改
console.log(obj3); // { prop: 1 } - 阻止了添加、删除和修改

实际应用场景

对象密封在以下场景特别有用:

1. 配置对象

当你创建一个配置对象,并且不希望其结构被意外修改时:

javascript
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
};

Object.seal(config);

// 应用程序可以修改配置值
config.timeout = 10000;

// 但不能添加或删除配置项
config.newSetting = true; // 失败
delete config.retries; // 失败

2. 用户角色和权限

在管理用户权限时,可以确保权限对象的结构保持不变:

javascript
const userRole = {
canView: true,
canEdit: false,
canDelete: false,
canAdmin: false
};

Object.seal(userRole);

// 可以修改权限
userRole.canEdit = true;

// 但不能添加新的未定义权限
userRole.canExport = true; // 失败

3. 防止API对象结构变化

当你提供一个API时,可以使用Object.seal()确保返回对象的结构稳定:

javascript
function getUserAPI() {
const api = {
fetchUser: function(id) { /* 实现细节 */ },
updateUser: function(id, data) { /* 实现细节 */ },
deleteUser: function(id) { /* 实现细节 */ }
};

return Object.seal(api);
}

const userAPI = getUserAPI();
// 用户只能使用预定义的方法,不能扩展或删除

深度密封

需要注意的是,Object.seal()只是浅密封,它只对对象的直接属性生效,不会递归到嵌套对象:

javascript
const user = {
name: "张三",
profile: {
address: "北京市",
phone: "12345678901"
}
};

Object.seal(user);

// 不能修改顶层结构
user.age = 30; // 失败
delete user.name; // 失败

// 但可以修改嵌套对象的结构
user.profile.email = "zhangsan@example.com"; // 成功
delete user.profile.phone; // 成功

console.log(user.profile); // { address: "北京市", email: "zhangsan@example.com" }

如果需要深度密封,需要递归处理:

javascript
function deepSeal(obj) {
// 获取所有属性,包括不可枚举的
const props = Object.getOwnPropertyNames(obj);

// 首先密封所有嵌套对象
props.forEach(prop => {
const value = obj[prop];
if (value && typeof value === 'object') {
deepSeal(value);
}
});

// 然后密封对象本身
return Object.seal(obj);
}

const user = {
name: "张三",
profile: {
address: "北京市",
phone: "12345678901"
}
};

deepSeal(user);

// 现在嵌套对象也被密封了
user.profile.email = "zhangsan@example.com"; // 失败
delete user.profile.phone; // 失败

注意事项

使用Object.seal()时,需要注意以下几点:

警告
  1. 在非严格模式下,违反密封规则的操作会静默失败;在严格模式下,会抛出TypeError。
  2. 密封对象只是浅密封,嵌套对象需要单独处理。
  3. 密封后对象的现有属性仍然可以修改。
  4. 密封操作不可逆,一旦对象被密封,就不能取消密封。

总结

JavaScript的对象密封(Object.seal())提供了一种介于完全自由和完全冻结之间的对象保护机制。它允许修改现有属性的值,但防止对象结构的变化,使其成为保护对象结构同时保留一定灵活性的理想选择。

使用对象密封可以让你的代码更加稳健,特别是在处理配置对象、API接口或任何需要保持结构稳定的情况下。

练习题

  1. 创建一个包含学生信息的对象,然后密封它。尝试添加、删除和修改属性,观察结果。
  2. 实现一个深度密封函数,能够递归密封嵌套对象。
  3. 创建一个配置管理系统,使用对象密封确保配置项只能被修改但不能被添加或删除。
  4. 比较Object.preventExtensions()Object.seal()Object.freeze()在同一个对象上的不同效果。

扩展阅读