跳到主要内容

JavaScript 对象冻结

什么是对象冻结?

在JavaScript中,对象是可变的,这意味着我们可以随时添加、删除或修改对象的属性。然而,有时候我们需要防止对象被意外修改,这时就可以使用JavaScript提供的冻结功能。

对象冻结是一种使对象变为不可变的操作,它可以防止对象属性的添加、删除和修改。JavaScript提供了三种不同级别的对象锁定方法,分别是:

  1. Object.preventExtensions() - 防止添加新属性
  2. Object.seal() - 防止添加和删除属性
  3. Object.freeze() - 防止添加、删除和修改属性(完全冻结)

本文将主要聚焦于最强大的方法 —— Object.freeze()

Object.freeze() 方法详解

Object.freeze() 方法可以冻结一个对象,一旦对象被冻结,就无法再对该对象添加新属性、删除已有属性或修改已有属性的值和属性的可配置性、可写性。

基本语法

javascript
Object.freeze(obj);

其中,obj 是要被冻结的对象。该方法返回传入的同一个对象,但该对象已经被冻结。

简单示例

让我们看一个简单的例子,了解 Object.freeze() 的效果:

javascript
// 创建一个对象
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() 方法来检查一个对象是否被冻结:

javascript
const user = { username: "admin" };
console.log(Object.isFrozen(user)); // 输出: false

Object.freeze(user);
console.log(Object.isFrozen(user)); // 输出: true

深度冻结

需要注意的是,Object.freeze() 只能冻结对象的直接属性,不会递归冻结嵌套对象。这被称为"浅冻结"。

javascript
const person = {
name: "张三",
age: 30,
address: {
city: "北京",
district: "朝阳区"
}
};

Object.freeze(person);

// 尝试修改嵌套对象的属性
person.address.city = "上海";
console.log(person.address.city); // 输出: '上海',嵌套对象并没有被冻结

如果要实现"深冻结",我们需要递归地冻结所有嵌套对象:

javascript
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() 可以防止向对象添加新属性,但允许修改或删除现有属性。

javascript
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() 既防止添加新属性,也防止删除现有属性,但允许修改现有属性的值。

javascript
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. 创建常量对象

当你需要定义不应该被修改的配置或常量对象时:

javascript
const CONFIG = Object.freeze({
API_URL: "https://api.example.com",
MAX_RETRIES: 3,
TIMEOUT: 5000,
DEBUG: false
});

2. 防止意外修改

在大型应用程序中,确保关键对象不被意外修改:

javascript
// 创建用户权限配置
const userPermissions = Object.freeze({
canRead: true,
canWrite: false,
canDelete: false,
canAdmin: false
});

// 使用权限进行检查,确保在应用的任何地方都不会被修改
function checkAccess(user, operation) {
return user.permissions[`can${operation}`] || false;
}

3. 提高代码可靠性

确保函数参数不会在函数内部被意外修改:

javascript
function processUserData(userData) {
// 冻结输入对象,确保不会意外修改原始数据
const data = Object.freeze({...userData});

// 现在可以安全地处理数据,而不用担心修改原始数据
// ...处理逻辑

// 返回新的数据对象
return { ...data, processed: true };
}

4. 在类中保护内部状态

javascript
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

注意事项

  1. 冻结是不可逆的:一旦对象被冻结,就无法"解冻"它。如果需要修改,应该创建该对象的副本。

  2. 性能考虑:在处理大型对象或频繁操作的对象时,冻结可能会带来性能损失。因此,只在必要时使用对象冻结。

  3. 兼容性Object.freeze() 在所有现代浏览器中都支持,但在一些旧浏览器中可能不可用。

  4. 冻结数组:数组也是对象,可以被冻结,但冻结后不能添加、删除元素或修改元素值。

javascript
const frozenArray = Object.freeze([1, 2, 3]);
frozenArray.push(4); // 在严格模式下会抛出错误,在非严格模式下会被忽略
console.log(frozenArray); // 输出: [1, 2, 3]

总结

JavaScript对象冻结为我们提供了控制对象可变性的强大工具。根据需要,我们可以选择使用:

  • Object.preventExtensions() - 防止添加新属性
  • Object.seal() - 防止添加和删除属性
  • Object.freeze() - 防止添加、删除和修改属性(完全冻结)

对象冻结在创建常量配置、防止意外修改、提高代码可靠性和实现不可变数据模式等场景中非常有用。但要记住,冻结只是"浅冻结",如果需要"深冻结",需要递归处理所有嵌套对象。

练习

  1. 创建一个带有多个属性的对象,然后尝试使用 Object.freeze() 冻结它。尝试修改、添加和删除属性,观察结果。

  2. 实现一个深冻结函数,可以递归冻结对象及其所有嵌套对象。

  3. 创建一个简单应用,使用对象冻结来保护配置或设置对象,确保它不会在应用运行过程中被修改。

  4. 比较 Object.preventExtensions()Object.seal()Object.freeze() 的行为,并记录它们的区别。

提示

记住,不可变性是函数式编程的重要概念,学会正确使用对象冻结可以帮助你编写更安全、更可靠的代码!