JavaScript内核与高级编程之:`JavaScript`的`Lenses`:其在不可变数据结构中的更新操作。

嘿,大家好!今天咱们来聊点儿酷炫的,关于 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.emailtrue 改成 false。 如果不用 Lenses,你可能会这么写:

const updatedUser = {
  ...user,
  profile: {
    ...user.profile,
    preferences: {
      ...user.profile.preferences,
      notifications: {
        ...user.profile.preferences.notifications,
        email: false
      }
    }
  }
};

看到没?一层又一层的展开运算符(spread operator),简直像俄罗斯套娃!代码冗长,容易出错,而且可读性极差。如果结构更深,简直就是噩梦。

这还没完,如果数据结构改变了(比如中间某层属性不存在),你的代码可能会抛出错误。

Lenses 就是为了解决这些痛点而生的。它让我们以更简洁、更安全的方式处理这些嵌套的更新。

Lenses 的核心概念:getset

还记得上面说的吗?Lenses 由 getset 组成。让我们为上面的 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-lenspartial.lensesmonocle-ts等等。 这里我们以一个简化版本的ramda-lens的实现为例说明,如何使用Lenses。

首先,我们需要一个lens函数,它接收两个函数(gettersetter),并返回一个Lens对象。

const lens = (getter, setter) => ({
  get: getter,
  set: setter
});

然后,我们需要viewset函数,它们分别用于获取和设置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 需要一些时间,但它绝对值得你投入精力。

希望今天的讲解对你有所帮助! 如果你有任何问题,欢迎随时提问。 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注