各位靓仔靓女,早上好!今天咱们来聊聊 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
,包含 get
和 set
两个方法。 使用 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: '长安街' }
// }
上面的代码创建了 addressLens
和 cityLens
两个 Lens,然后将它们组合起来,创建了一个 addressCityLens
,可以方便地访问和修改 address.city
属性。
Lenses 库:更方便的使用方式
手动创建和组合 Lenses 比较繁琐,好在有很多 JavaScript 库提供了 Lenses 的实现,比如 ramda
、lodash/fp
、partial.lenses
等。 它们提供了更方便的 API,可以更轻松地创建和使用 Lenses。
这里以 ramda
为例,介绍如何使用 Lenses。
首先,安装 ramda
:
npm install ramda
然后,可以使用 R.lens
、R.view
和 R.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,避免手动编写大量的 get
和 set
方法。
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.lenses
比 ramda
更灵活,但也更复杂。 如果你需要更高级的 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 的学习曲线比较陡峭,但是一旦掌握了它,你就能在函数式编程的道路上更进一步。
练习题
-
使用
ramda
或partial.lenses
创建一个 Lens,用于访问以下对象中的items[0].price
属性。const data = { items: [ { id: 1, name: '苹果', price: 5 }, { id: 2, name: '香蕉', price: 3 } ] };
-
使用
ramda
或partial.lenses
创建一个 Lens,用于将以下对象中的address.city
属性转换为大写。const user = { name: '张三', age: 30, address: { city: 'beijing', street: '长安街' } };
今天的讲座就到这里,希望大家有所收获。 记住,编程的道路永无止境,不断学习和实践才能成为真正的编程高手! 下课!