嘿,大家好!今天咱们来聊点儿酷炫的,关于 JavaScript 的 Lenses,尤其是在不可变数据结构中,如何用它们优雅地更新数据。准备好了吗?Let's dive in!
### 啥是 Lenses?别告诉我你只知道眼镜!
Lenses,中文翻译成“镜头”,但在这里跟眼镜真没啥关系。在编程的世界里,它是一种强大的工具,允许你安全且简洁地访问和修改嵌套在复杂数据结构中的特定属性,而无需手动遍历整个结构。想象一下,你有一栋豪华别墅(数据结构),Lenses 就像是专门为你定制的望远镜,让你精准地观察和调整别墅里的某个角落,比如客厅里的壁炉。
更学术一点的说法:Lenses 是由一对 get 和 set 函数组成的。
* **get (view):** 接收一个数据结构,返回你想要访问的属性值。
* **set (update):** 接收一个数据结构和一个新的属性值,返回一个修改后的新的数据结构,原始数据保持不变(对于不可变数据结构来说)。
### 为什么要用 Lenses?痛点在哪里?
假设我们有一个嵌套很深的对象,比如:
```javascript
const user = {
id: 1,
name: 'Alice',
address: {
street: '123 Main St',
city: 'Anytown',
zipCode: '12345'
},
profile: {
email: '[email protected]',
preferences: {
theme: 'light',
notifications: {
email: true,
sms: false
}
}
}
};
现在,你想把 profile.preferences.notifications.email
从 true
改成 false
。 如果不用 Lenses,你可能会这么写:
const updatedUser = {
...user,
profile: {
...user.profile,
preferences: {
...user.profile.preferences,
notifications: {
...user.profile.preferences.notifications,
email: false
}
}
}
};
看到没?一层又一层的展开运算符(spread operator),简直像俄罗斯套娃!代码冗长,容易出错,而且可读性极差。如果结构更深,简直就是噩梦。
这还没完,如果数据结构改变了(比如中间某层属性不存在),你的代码可能会抛出错误。
Lenses 就是为了解决这些痛点而生的。它让我们以更简洁、更安全的方式处理这些嵌套的更新。
Lenses 的核心概念:get
和 set
还记得上面说的吗?Lenses 由 get
和 set
组成。让我们为上面的 user
对象的 profile.preferences.notifications.email
创建一个 Lens。
const emailNotificationsLens = {
get: (obj) => obj?.profile?.preferences?.notifications?.email, // 使用可选链式调用,防止报错
set: (obj, value) => ({
...obj,
profile: {
...obj.profile,
preferences: {
...obj.profile.preferences,
notifications: {
...obj.profile.preferences.notifications,
email: value
}
}
}
})
};
现在,我们可以这样使用它:
// 获取 email 通知设置
const emailEnabled = emailNotificationsLens.get(user);
console.log(emailEnabled); // true
// 更新 email 通知设置
const updatedUser = emailNotificationsLens.set(user, false);
console.log(updatedUser.profile.preferences.notifications.email); // false
console.log(user.profile.preferences.notifications.email); // true (原始 user 对象不变)
虽然这个 Lens 看起来仍然有点冗长,但它已经比直接修改对象属性要好一些了。 而且,我们可以通过一些工具库来简化 Lens 的创建。
使用 Lens 库:让生活更美好
手动创建 Lens 仍然很麻烦,所以社区涌现出许多 Lens 库,比如 ramda-lens
、partial.lenses
、monocle-ts
等等。 这里我们以一个简化版本的ramda-lens
的实现为例说明,如何使用Lenses。
首先,我们需要一个lens
函数,它接收两个函数(getter
和setter
),并返回一个Lens对象。
const lens = (getter, setter) => ({
get: getter,
set: setter
});
然后,我们需要view
和set
函数,它们分别用于获取和设置Lens对应的属性值。
const view = (lens, obj) => lens.get(obj);
const set = (lens, value, obj) => lens.set(obj, value);
现在,让我们用这些函数来更新 user
对象:
// 创建 Lens
const profileLens = lens(
(obj) => obj.profile,
(obj, value) => ({ ...obj, profile: value })
);
const preferencesLens = lens(
(obj) => obj.preferences,
(obj, value) => ({ ...obj, preferences: value })
);
const notificationsLens = lens(
(obj) => obj.notifications,
(obj, value) => ({ ...obj, notifications: value })
);
const emailLens = lens(
(obj) => obj.email,
(obj, value) => ({ ...obj, email: value })
);
// 组合 Lens
const emailNotificationsLensSimplified = {
get: (obj) => view(emailLens, view(notificationsLens, view(preferencesLens, view(profileLens, obj)))),
set: (obj, value) => {
const profileValue = view(profileLens, obj);
const preferencesValue = view(preferencesLens, profileValue);
const notificationsValue = view(notificationsLens, preferencesValue);
const newNotificationsValue = set(emailLens, value, notificationsValue);
const newPreferencesValue = set(notificationsLens, newNotificationsValue, preferencesValue);
const newProfileValue = set(preferencesLens, newPreferencesValue, profileValue);
return set(profileLens, newProfileValue, obj);
}
}
// 获取 email 通知设置
const emailEnabled = view(emailNotificationsLensSimplified, user);
console.log(emailEnabled);
// 更新 email 通知设置
const updatedUser = emailNotificationsLensSimplified.set(user, false);
console.log(view(emailNotificationsLensSimplified, updatedUser));
console.log(view(emailNotificationsLensSimplified, user));
可以看到,虽然我们自己实现的这个简化版Lens还是有点繁琐,但是已经避免了手动展开对象的写法。而更成熟的Lens库,会提供更简洁的API来组合Lens,使代码更加易读。
更进一步,我们可以使用compose函数来组合lens, 使得代码更简洁。
const compose = (...lenses) => ({
get: (obj) => lenses.reduce((acc, lens) => view(lens, acc), obj),
set: (obj, value) => lenses.reduceRight((acc, lens) => set(lens, acc, obj), value)
});
// 组合 Lens
const emailNotificationsLensComposed = compose(profileLens, preferencesLens, notificationsLens, emailLens);
// 获取 email 通知设置
const emailEnabledComposed = view(emailNotificationsLensComposed, user);
console.log(emailEnabledComposed);
// 更新 email 通知设置
const updatedUserComposed = emailNotificationsLensComposed.set(user, false);
console.log(view(emailNotificationsLensComposed, updatedUserComposed));
console.log(view(emailNotificationsLensComposed, user));
这个composed的写法更加简洁,更容易理解。
Lenses 与不可变数据结构:天生一对
Lenses 的真正威力在于与不可变数据结构结合使用。 不可变数据结构一旦创建,就不能被修改。 每次修改都会返回一个新的数据结构。 这使得 Lenses 的 set
操作非常安全,因为它不会改变原始数据,避免了副作用。
常见的不可变数据结构库包括:
- Immutable.js: Facebook 出品的,功能强大,提供了各种不可变数据结构,如 List、Map、Set 等。
- Mori: 基于 ClojureScript 的不可变数据结构库。
- seamless-immutable: 简单易用,直接将 JavaScript 对象转换为不可变对象。
让我们用 seamless-immutable
来演示一下:
import Immutable from 'seamless-immutable';
const immutableUser = Immutable(user);
// 使用 Lens 更新 email 通知设置
const updatedImmutableUser = emailNotificationsLensComposed.set(immutableUser, false);
console.log(immutableUser.profile.preferences.notifications.email); // true (原始 immutableUser 对象不变)
console.log(updatedImmutableUser.profile.preferences.notifications.email); // false (新的 immutableUser 对象)
// 尝试修改 immutableUser
try {
immutableUser.profile.preferences.notifications.email = false; // 会报错,因为对象是不可变的
} catch (error) {
console.error(error); // 抛出错误
}
使用不可变数据结构和 Lenses,可以确保数据的安全性,并避免意外的副作用。
Lenses 的优点总结
- 简洁性: 减少了嵌套展开运算符的使用,使代码更易读。
- 安全性: 与不可变数据结构结合使用,避免了副作用,确保数据的完整性。
- 可组合性: 可以将多个 Lenses 组合起来,访问和修改更深层次的属性。
- 可测试性: 易于编写单元测试,因为 Lenses 是纯函数,输入相同,输出也相同。
何时使用 Lenses?
- 当需要频繁地访问和修改嵌套在复杂数据结构中的属性时。
- 当使用不可变数据结构,并且需要确保数据的安全性时。
- 当需要编写可维护、可测试的代码时。
何时不使用 Lenses?
- 当数据结构非常简单,没有嵌套时。
- 当性能是首要考虑因素,并且手动修改属性的性能更高时(这种情况比较少见)。
- 当团队成员不熟悉 Lenses 的概念时(需要先进行培训)。
小结
Lenses 是一种强大的工具,可以帮助你更简洁、更安全地访问和修改嵌套在复杂数据结构中的属性。 结合不可变数据结构,Lenses 可以确保数据的安全性,并提高代码的可维护性。 虽然学习 Lenses 需要一些时间,但它绝对值得你投入精力。
希望今天的讲解对你有所帮助! 如果你有任何问题,欢迎随时提问。 祝大家编程愉快!