JavaScript 对象冻结
什么是对象冻结?
在JavaScript中,对象是可变的,这意味着我们可以随时添加、删除或修改对象的属性。然而,有时候我们需要防止对象被意外修改,这时就可以使用JavaScript提供的冻结功能。
对象冻结是一种使对象变为不可变的操作,它可以防止对象属性的添加、删除和修改。JavaScript提供了三种不同级别的对象锁定方法,分别是:
Object.preventExtensions()
- 防止添加新属性Object.seal()
- 防止添加和删除属性Object.freeze()
- 防止添加、删除和修改属性(完全冻结)
本文将主要聚焦于最强大的方法 —— Object.freeze()
。
Object.freeze() 方法详解
Object.freeze()
方法可以冻结一个对象,一旦对象被冻结,就无法再对该对象添加新属性、删除已有属性或修改已有属性的值和属性的可配置性、可写性。
基本语法
Object.freeze(obj);
其中,obj
是要被冻结的对象。该方法返回传入的同一个对象,但该对象已经被冻结。
简单示例
让我们看一个简单的例子,了解 Object.freeze()
的效果:
// 创建一个对象
const person = {
name: "张三",
age: 30,
address: {
city: "北京",
district: "朝阳区"
}
};
// 冻结对象
Object.freeze(person);
// 尝试修改属性值(在严格模式下会抛出错误)
person.name = "李四";
// 尝试添加新属性
person.email = "zhangsan@example.com";
// 尝试删除属性
delete person.age;
console.log(person);
// 输出: { name: '张三', age: 30, address: { city: '北京', district: '朝阳区' } }
// 可以看到,所有修改操作都被忽略了
在严格模式下('use strict'),尝试修改冻结对象会抛出 TypeError 异常。在非严格模式下,修改操作会被默默地忽略。
检查对象是否被冻结
JavaScript 提供了 Object.isFrozen()
方法来检查一个对象是否被冻结:
const user = { username: "admin" };
console.log(Object.isFrozen(user)); // 输出: false
Object.freeze(user);
console.log(Object.isFrozen(user)); // 输出: true
深度冻结
需要注意的是,Object.freeze()
只能冻结对象的直接属性,不会递归冻结嵌套对象。这被称为"浅冻结"。
const person = {
name: "张三",
age: 30,
address: {
city: "北京",
district: "朝阳区"
}
};
Object.freeze(person);
// 尝试修改嵌套对象的属性
person.address.city = "上海";
console.log(person.address.city); // 输出: '上海',嵌套对象并没有被冻结
如果要实现"深冻结",我们需要递归地冻结所有嵌套对象:
function deepFreeze(object) {
// 获取对象的所有属性(包括不可枚举属性)
const propNames = Object.getOwnPropertyNames(object);
// 在冻结自身之前,先冻结所有属性中的对象
for (const name of propNames) {
const value = object[name];
// 如果属性值是对象,则递归冻结
if (value && typeof value === "object") {
deepFreeze(value);
}
}
// 最后冻结自身
return Object.freeze(object);
}
// 使用深冻结
const person = {
name: "张三",
age: 30,
address: {
city: "北京",
district: "朝阳区"
}
};
deepFreeze(person);
// 尝试修改嵌套对象的属性
person.address.city = "上海";
console.log(person.address.city); // 输出: '北京',修改失败
Object.preventExtensions() 和 Object.seal()
除了 Object.freeze()
,JavaScript 还提供了两个相关方法,它们对对象施加的限制较少:
Object.preventExtensions()
Object.preventExtensions()
可以防止向对象添加新属性,但允许修改或删除现有属性。
const user = { name: "用户1" };
Object.preventExtensions(user);
// 尝试添加新属性
user.age = 25;
console.log(user.age); // 输出: undefined,添加失败
// 可以修改现有属性
user.name = "用户2";
console.log(user.name); // 输出: '用户2',修改成功
// 可以删除现有属性
delete user.name;
console.log(user.name); // 输出: undefined,删除成功
Object.seal()
Object.seal()
既防止添加新属性,也防止删除现有属性,但允许修改现有属性的值。
const user = { name: "用户1" };
Object.seal(user);
// 尝试添加新属性
user.age = 25;
console.log(user.age); // 输出: undefined,添加失败
// 可以修改现有属性
user.name = "用户2";
console.log(user.name); // 输出: '用户2',修改成功
// 尝试删除现有属性
delete user.name;
console.log(user.name); // 输出: '用户2',删除失败
比较三种方法
为了更直观地理解这三种方法的区别,我们可以看一下下面的表格:
操作 | Object.preventExtensions() | Object.seal() | Object.freeze() |
---|---|---|---|
添加新属性 | ❌ 不允许 | ❌ 不允许 | ❌ 不允许 |
删除已有属性 | ✅ 允许 | ❌ 不允许 | ❌ 不允许 |
修改已有属性 | ✅ 允许 | ✅ 允许 | ❌ 不允许 |
实际应用场景
1. 创建常量对象
当你需要定义不应该被修改的配置或常量对象时:
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
MAX_RETRIES: 3,
TIMEOUT: 5000,
DEBUG: false
});
2. 防止意外修改
在大型应用程序中,确保关键对象不被意外修改:
// 创建用户权限配置
const userPermissions = Object.freeze({
canRead: true,
canWrite: false,
canDelete: false,
canAdmin: false
});
// 使用权限进行检查,确保在应用的任何地方都不会被修改
function checkAccess(user, operation) {
return user.permissions[`can${operation}`] || false;
}
3. 提高代码可靠性
确保函数参数不会在函数内部被意外修改:
function processUserData(userData) {
// 冻结输入对象,确保不会意外修改原始数据
const data = Object.freeze({...userData});
// 现在可以安全地处理数据,而不用担心修改原始数据
// ...处理逻辑
// 返回新的数据对象
return { ...data, processed: true };
}
4. 在类中保护内部状态
class ImmutablePoint {
constructor(x, y) {
this._data = Object.freeze({ x, y });
}
get x() { return this._data.x; }
get y() { return this._data.y; }
// 不提供setter方法
// 返回新对象而不是修改现有对象
add(point) {
return new ImmutablePoint(
this.x + point.x,
this.y + point.y
);
}
}
const p1 = new ImmutablePoint(1, 2);
const p2 = new ImmutablePoint(3, 4);
const p3 = p1.add(p2); // 创建新点 (4, 6),而不是修改p1
注意事项
-
冻结是不可逆的:一旦对象被冻结,就无法"解冻"它。如果需要修改,应该创建该对象的副本。
-
性能考虑:在处理大型对象或频繁操作的对象时,冻结可能会带来性能损失。因此,只在必要时使用对象冻结。
-
兼容性:
Object.freeze()
在所有现代浏览器中都支持,但在一些旧浏览器中可能不可用。 -
冻结数组:数组也是对象,可以被冻结,但冻结后不能添加、删除元素或修改元素值。
const frozenArray = Object.freeze([1, 2, 3]);
frozenArray.push(4); // 在严格模式下会抛出错误,在非严格模式下会被忽略
console.log(frozenArray); // 输出: [1, 2, 3]
总结
JavaScript对象冻结为我们提供了控制对象可变性的强大工具。根据需要,我们可以选择使用:
Object.preventExtensions()
- 防止添加新属性Object.seal()
- 防止添加和删除属性Object.freeze()
- 防止添加、删除和修改属性(完全冻结)
对象冻结在创建常量配置、防止意外修改、提高代码可靠性和实现不可变数据模式等场景中非常有用。但要记住,冻结只是"浅冻结",如果需要"深冻结",需要递归处理所有嵌套对象。
练习
-
创建一个带有多个属性的对象,然后尝试使用
Object.freeze()
冻结它。尝试修改、添加和删除属性,观察结果。 -
实现一个深冻结函数,可以递归冻结对象及其所有嵌套对象。
-
创建一个简单应用,使用对象冻结来保护配置或设置对象,确保它不会在应用运行过程中被修改。
-
比较
Object.preventExtensions()
、Object.seal()
和Object.freeze()
的行为,并记录它们的区别。
记住,不可变性是函数式编程的重要概念,学会正确使用对象冻结可以帮助你编写更安全、更可靠的代码!