JavaScript内核与高级编程之:`JavaScript`的`Lenses`:其在函数式编程中的数据更新。

各位靓仔靓女,早上好!今天咱们来聊聊 JavaScript 里一个挺有意思的概念—— Lenses(镜头)。 别害怕,这玩意儿听起来高大上,但其实理解起来并不难,掌握了之后能让你的函数式编程功力更上一层楼。

开场白:对象更新的烦恼

在 JavaScript 里,咱们经常要更新对象的状态。最常见的做法就是直接修改对象属性,或者创建新对象,把旧对象属性复制过去,再改动需要更新的部分。

const user = {
  name: '张三',
  age: 30,
  address: {
    city: '北京',
    street: '长安街'
  }
};

// 简单粗暴的修改
user.age = 31;
user.address.city = '上海';

// 看起来更函数式的做法(但还是不够优雅)
const updatedUser = {
  ...user,
  age: 31,
  address: {
    ...user.address,
    city: '上海'
  }
};

上面的代码虽然简单,但有几个问题:

  • 直接修改对象: 会产生副作用,在大型项目中容易造成混乱。
  • 层层展开对象: 代码冗长,嵌套层级深了之后,简直是噩梦。如果地址还有更深的嵌套,比如 address.detail.building.floor.room,那代码可就没眼看了。
  • 可读性差: 看着一堆 ...,很难一眼看出到底更新了哪些属性。

有没有更优雅、更函数式的方法呢? 答案是肯定的,那就是 Lenses!

什么是 Lenses?

Lenses,顾名思义,就是像镜头一样的东西。 它提供了一种聚焦(focus)和操作对象内部特定属性的方式,而无需直接修改原对象。 它由两个函数组成:

  • getter (get): 从对象中获取特定属性的值。
  • setter (set): 创建一个新对象,该对象与原对象相同,但特定属性的值已更新。

你可以把 Lens 想象成一个小型 API,专门用来访问和修改对象内部的某个属性。

Lenses 的优势

  • 不可变性 (Immutability): Lenses 不会直接修改原对象,而是返回一个全新的对象,保证了数据的不可变性。
  • 组合性 (Composability): Lenses 可以像管道一样组合起来,方便地访问和修改嵌套对象中的属性。
  • 可读性 (Readability): Lenses 可以提高代码的可读性,让你更容易理解代码的目的。
  • 复用性 (Reusability): 同一个 Lens 可以被多次使用,避免重复编写代码。

如何创建 Lenses?

咱们先来手动创建一个简单的 Lens。以 user 对象为例,创建一个访问和修改 age 属性的 Lens。

const user = {
  name: '张三',
  age: 30,
  address: {
    city: '北京',
    street: '长安街'
  }
};

const ageLens = {
  get: (obj) => obj.age,
  set: (obj, value) => ({ ...obj, age: value })
};

// 使用 Lens 获取 age
const age = ageLens.get(user); // 30

// 使用 Lens 更新 age
const updatedUser = ageLens.set(user, 31); // { name: '张三', age: 31, address: { city: '北京', street: '长安街' } }

console.log(user); // { name: '张三', age: 30, address: { city: '北京', street: '长安街' } }  // 原对象未改变
console.log(updatedUser); // { name: '张三', age: 31, address: { city: '北京', street: '长安街' } } // 返回新对象

上面的代码手动创建了一个 ageLens,包含 getset 两个方法。 使用 get 方法可以获取 user 对象的 age 属性,使用 set 方法可以创建一个新的 user 对象,其中 age 属性被更新为新的值。

访问嵌套属性:Lenses 的组合

手动创建简单的 Lens 还可以,但是对于嵌套的对象,手动创建就太麻烦了。 这时候,Lenses 的组合性就派上用场了。 咱们可以先创建访问嵌套属性的 Lenses,然后将它们组合起来。

const user = {
  name: '张三',
  age: 30,
  address: {
    city: '北京',
    street: '长安街'
  }
};

// 创建访问 address 属性的 Lens
const addressLens = {
  get: (obj) => obj.address,
  set: (obj, value) => ({ ...obj, address: value })
};

// 创建访问 city 属性的 Lens
const cityLens = {
  get: (obj) => obj.city,
  set: (obj, value) => ({ ...obj, city: value })
};

// 组合 Lenses,创建一个访问 address.city 属性的 Lens
const addressCityLens = {
  get: (obj) => cityLens.get(addressLens.get(obj)),
  set: (obj, value) => addressLens.set(obj, { ...addressLens.get(obj), ...cityLens.set(addressLens.get(obj),value)})
};

// 使用组合后的 Lens 获取 city
const city = addressCityLens.get(user); // 北京

// 使用组合后的 Lens 更新 city
const updatedUser = addressCityLens.set(user, '上海');

console.log(updatedUser);
// {
//   name: '张三',
//   age: 30,
//   address: { city: '上海', street: '长安街' }
// }

上面的代码创建了 addressLenscityLens 两个 Lens,然后将它们组合起来,创建了一个 addressCityLens,可以方便地访问和修改 address.city 属性。

Lenses 库:更方便的使用方式

手动创建和组合 Lenses 比较繁琐,好在有很多 JavaScript 库提供了 Lenses 的实现,比如 ramdalodash/fppartial.lenses 等。 它们提供了更方便的 API,可以更轻松地创建和使用 Lenses。

这里以 ramda 为例,介绍如何使用 Lenses。

首先,安装 ramda

npm install ramda

然后,可以使用 R.lensR.viewR.set 等函数来创建和使用 Lenses。

import * as R from 'ramda';

const user = {
  name: '张三',
  age: 30,
  address: {
    city: '北京',
    street: '长安街'
  }
};

// 创建访问 age 属性的 Lens
const ageLens = R.lens(R.prop('age'), R.assoc('age'));

// 使用 Lens 获取 age
const age = R.view(ageLens, user); // 30

// 使用 Lens 更新 age
const updatedUser = R.set(ageLens, 31, user);

console.log(updatedUser);
// {
//   name: '张三',
//   age: 31,
//   address: { city: '北京', street: '长安街' }
// }

// 创建访问 address.city 属性的 Lens
const addressCityLens = R.lensPath(['address', 'city']);

// 使用 Lens 获取 city
const city = R.view(addressCityLens, user); // 北京

// 使用 Lens 更新 city
const updatedUser2 = R.set(addressCityLens, '上海', user);

console.log(updatedUser2);
// {
//   name: '张三',
//   age: 30,
//   address: { city: '上海', street: '长安街' }
// }

R.lens(getter, setter) 创建一个 Lens,其中 getter 用于获取属性值,setter 用于创建新的对象。
R.view(lens, obj) 使用 Lens 获取对象 obj 中特定属性的值。
R.set(lens, value, obj) 使用 Lens 创建一个新的对象,该对象与原对象 obj 相同,但特定属性的值已更新为 value
R.lensPath(path) 创建一个访问嵌套属性的 Lens,其中 path 是一个数组,表示属性的路径。

使用 ramda 库,可以更简洁地创建和使用 Lenses,避免手动编写大量的 getset 方法。

Lenses 的实际应用场景

Lenses 在实际项目中有很多应用场景,比如:

  • Redux 等状态管理库: Lenses 可以方便地更新 Redux Store 中的状态,避免直接修改 Store。
  • 表单处理: Lenses 可以方便地访问和修改表单数据,简化表单处理的逻辑。
  • 数据转换: Lenses 可以方便地将一种数据结构转换为另一种数据结构。
  • API 数据处理: 从API获取的数据结构复杂,使用Lenses方便读取和更新数据。

Lenses 的优缺点

优点 缺点
提高代码的可读性和可维护性 学习成本较高,需要理解 Lenses 的概念和使用方式。
保证数据的不可变性 可能会降低性能,因为每次更新都需要创建新的对象。
方便地访问和修改嵌套对象中的属性 对于简单的对象,使用 Lenses 可能显得过于复杂。
提高代码的复用性 需要引入额外的库,增加项目的依赖。
更容易进行单元测试 如果 Lens 设计不合理,可能会导致代码难以理解和调试。
适合处理复杂的数据结构和状态更新

更高级的 Lens 操作:使用 partial.lenses

partial.lenses 是另一个强大的 Lens 库,它提供了更多的 Lens 操作,比如:

  • Traversal: 遍历集合中的元素,并对每个元素应用 Lens。
  • Optional: 处理可能不存在的属性。
  • Choice: 根据条件选择不同的 Lens。

partial.lensesramda 更灵活,但也更复杂。 如果你需要更高级的 Lens 操作,可以考虑使用 partial.lenses

import * as L from 'partial.lenses';

const data = {
  users: [
    { id: 1, name: '张三', age: 30 },
    { id: 2, name: '李四', age: 25 },
    { id: 3, name: '王五', age: 35 }
  ]
};

// 创建一个 Lens,用于遍历 users 数组,并将每个用户的 age 增加 1
const addAgeLens = ['users', L.elems, 'age', L.modify(x => x + 1)];

// 使用 Lens 更新 data
const updatedData = L.set(addAgeLens, data);

console.log(updatedData);
// {
//   users: [
//     { id: 1, name: '张三', age: 31 },
//     { id: 2, name: '李四', age: 26 },
//     { id: 3, name: '王五', age: 36 }
//   ]
// }

总结

Lenses 是一种强大的函数式编程工具,可以帮助你更优雅地处理对象更新。 它可以提高代码的可读性、可维护性和复用性,并保证数据的不可变性。

虽然 Lenses 的学习曲线比较陡峭,但是一旦掌握了它,你就能在函数式编程的道路上更进一步。

练习题

  1. 使用 ramdapartial.lenses 创建一个 Lens,用于访问以下对象中的 items[0].price 属性。

    const data = {
      items: [
        { id: 1, name: '苹果', price: 5 },
        { id: 2, name: '香蕉', price: 3 }
      ]
    };
  2. 使用 ramdapartial.lenses 创建一个 Lens,用于将以下对象中的 address.city 属性转换为大写。

    const user = {
      name: '张三',
      age: 30,
      address: {
        city: 'beijing',
        street: '长安街'
      }
    };

今天的讲座就到这里,希望大家有所收获。 记住,编程的道路永无止境,不断学习和实践才能成为真正的编程高手! 下课!

发表回复

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