现代 JavaScript 中的优雅模式:RORO
订阅 DevMastery 新闻通讯
JavaScript 语言发明后不久,我就写下了我的最初几行代码。如果你当时告诉我,有一天我会写一系列关于JavaScript优雅模式的文章,我肯定会把你笑出门外。我当时觉得 JavaScript 是一种奇怪的小语言,甚至勉强算得上是“真正的编程”。
嗯,自那时以来的20年里,很多事情都发生了变化。我现在在JavaScript中看到了Douglas Crockford在撰写《JavaScript:优点》时所看到的东西:“一种杰出的、动态的编程语言……具有强大的表达能力。”
废话不多说,下面是我最近在代码中使用的一个很棒的小模式。希望你也能像我一样喜欢它。
请注意 :我非常确定这些代码都不是我发明的。很可能是我在别人的代码里偶然发现的,然后自己也采用了。
接收一个对象,返回一个对象(RORO)
我的大多数函数现在都接受单个类型的参数object
,并且其中许多函数也返回或解析为类型的值object
。
部分得益于ES2015 引入的解构特性,我发现这是一个非常强大的模式。我甚至给它起了个傻傻的名字“RORO”,因为……品牌效应?🤷♂️
注意: 解构是我最喜欢的现代 JavaScript 特性之一。我们将在本文中大量使用它,所以如果你不熟悉它,这里有一个Beau Carnes的快速视频可以帮助你快速上手。
以下是您会喜欢这种模式的一些原因:
- 命名参数
- 更清晰的默认参数
- 更丰富的返回值
- 简单的函数组合
让我们逐一看一下。
命名参数
假设我们有一个函数,它返回给定角色中的用户列表,并且假设我们需要提供一个选项来包含每个用户的联系信息,以及另一个选项来包含非活动用户,传统上我们可能会写:
function findUsersByRole (
role,
withContactInfo,
includeInactive
) {...}
对该函数的调用可能如下所示:
findUsersByRole(
'admin',
true,
true
)
注意最后两个参数有多么模糊。“true, true”指的是什么?
如果我们的应用几乎从不需要联系信息,但几乎总是需要非活跃用户,会发生什么?我们必须始终与这个中间参数作斗争,即使它实际上并不重要(稍后会详细介绍)。
简而言之,这种传统方法会给我们留下潜在的歧义、嘈杂的代码,这些代码更难理解,也更难编写。
让我们看看当我们收到单个对象时会发生什么:
function findUsersByRole ({
role,
withContactInfo,
includeInactive
}) {...}
请注意,我们的函数看起来几乎完全相同,只是我们在参数周围加上了括号。这表明我们的函数现在不需要接收三个不同的参数,而只需要一个具有名为 、 和 的属性role
的withContactInfo
对象includeInactive
。
这是因为 ES2015 中引入了一项名为Destructuring的 JavaScript 功能。
现在我们可以像这样调用我们的函数:
findUsersByRole({
role: 'admin',
withContactInfo: true,
includeInactive: true
})
这样歧义性就少了很多,而且更容易阅读和理解。此外,省略或重新排序参数不再是问题,因为它们现在是对象的命名属性。
例如,这有效:
findUsersByRole({
withContactInfo: true,
role: 'admin',
includeInactive: true
})
这也一样:
findUsersByRole({
role: 'admin',
includeInactive: true
})
这也使得在不破坏旧代码的情况下添加新参数成为可能。
这里需要注意的是,如果我们希望所有参数都是可选的,换句话说,如果以下是一个有效的调用……
findUsersByRole()
...我们需要为参数对象设置一个默认值,如下所示:
function findUsersByRole ({
role,
withContactInfo,
includeInactive
} = {}) {...}
对参数对象使用解构的另一个好处是它提升了不可变性。当我们object
在函数中解构它时,我们会将对象的属性赋给新的变量。更改这些变量的值不会改变原始对象。
请考虑以下情况:
const options = {
role: 'Admin',
includeInactive: true
}
findUsersByRole(options)
function findUsersByRole ({
role,
withContactInfo,
includeInactive
} = {}) {
role = role.toLowerCase()
console.log(role) // 'admin'
...
}
console.log(options.role) // 'Admin'
即使我们改变的值,role
的值options.role
仍然保持不变。
值得注意的是,解构会产生浅拷贝,因此如果我们的参数对象的任何属性属于复杂类型(例如array
或object
),则更改它们确实会影响原始对象。
更清晰的默认参数
从 ES2015 开始,JavaScript 函数获得了定义默认参数的能力。事实上,我们最近在上面的函数={}
中添加参数对象时就使用了默认参数findUsersByRole
。
使用传统的默认参数,我们的findUsersByRole
函数可能看起来像这样。
function findUsersByRole (
role,
withContactInfo = true,
includeInactive
) {...}
如果我们想设置includeInactive
为,true
我们必须明确传递undefined
值以withContactInfo
保留默认值,如下所示:
findUsersByRole(
'Admin',
undefined,
true
)
这有多可怕?
将其与使用参数对象进行比较,如下所示:
function findUsersByRole ({
role,
withContactInfo = true,
includeInactive
} = {}) {...}
现在我们可以写...
findUsersByRole({
role: ‘Admin’,
includeInactive: true
})
...并且我们的默认值withContactInfo
被保留。
奖励:必需参数
您多久写过一次类似的东西?
function findUsersByRole ({
role,
withContactInfo,
includeInactive
} = {}) {
if (role == null) {
throw Error(...)
}
...
}
注意: 我们在上面使用双等号(==)来通过单个语句测试 null
和。 undefined
如果我告诉您可以使用默认参数来验证所需参数,那会怎样?
首先,我们需要定义一个requiredParam()
抛出错误的函数。
像这样:
function requiredParam (param) {
const requiredParamError = new Error(
`Required parameter, "${param}" is missing.`
)
// preserve original stack trace
if (typeof Error.captureStackTrace === ‘function’) {
Error.captureStackTrace(
requiredParamError,
requiredParam
)
}
throw requiredParamError
}
我知道,我知道。requiredParam 不支持 RORO。这就是为什么我说“我的 很多 函数”——而不是 “全部” 。
现在,我们可以将 的调用设置requiredParam
为 的默认值role
,如下所示:
function findUsersByRole ({
role = requiredParam('role'),
withContactInfo,
includeInactive
} = {}) {...}
使用上述代码,如果有人findUsersByRole
在不提供的情况下拨打电话role
,他们将收到一条Error
消息,内容为Required parameter, “role” is missing.
从技术上讲,我们也可以将此技术与常规默认参数一起使用;我们不一定需要一个对象。但这个技巧太有用了,值得一提。
更丰富的返回值
JavaScript 函数只能返回一个值。如果该值是一个,object
它可以包含更多信息。
假设有一个函数将数据保存User
到数据库。当该函数返回一个对象时,它可以向调用者提供大量信息。
例如,一种常见的模式是在保存函数中“更新插入”或“合并”数据。这意味着,我们将行插入数据库表中(如果它们尚不存在)或更新它们(如果它们存在)。
在这种情况下,知道 Save 函数执行的操作是INSERT
还是 会很方便UPDATE
。准确地了解数据库中存储的内容也很好,并且知道操作的状态也很好;它是否成功了,是否作为更大事务的一部分处于待处理状态,是否超时了?
返回对象时,可以很容易地一次性传达所有这些信息。
类似于:
async saveUser({
upsert = true,
transaction,
...userInfo
}) {
// save to the DB
return {
operation, // e.g 'INSERT'
status, // e.g. 'Success'
saved: userInfo
}
}
从技术上讲,上面返回的Promise
是解析为的,object
但你明白我的意思了。
简单的函数组合
函数组合是将两个或多个函数组合起来生成一个新函数的过程。将函数组合在一起就像将一系列管道连接在一起,让数据流经它们。—— Eric Elliott
我们可以使用pipe
类似这样的函数将函数组合在一起:
function pipe(...fns) {
return param => fns.reduce(
(result, fn) => fn(result),
param
)
}
上述函数接受一个函数列表并返回一个可以从左到右应用该列表的函数,从给定的参数开始,然后将列表中每个函数的结果传递给列表中的下一个函数。
如果您感到困惑,请不要担心,下面的示例可以帮助您理清思路。
这种方法的一个限制是列表中的每个函数只能接收一个参数。幸运的是,当我们使用 RORO 时,这不是问题!
这里有一个例子,我们有一个saveUser
函数,它通过 3 个独立的函数来传递一个userInfo
对象,这些函数按顺序验证、规范化和保存用户信息。
function saveUser(userInfo) {
return pipe(
validate,
normalize,
persist
)(userInfo)
}
我们可以在、和函数中使用剩余参数来解构每个函数所需的值,并将所有内容传回给调用者。validate
normalize
persist
这里有一段代码可以让你了解其要点:
function validate(
id,
firstName,
lastName,
email = requiredParam(),
username = requiredParam(),
pass = requiredParam(),
address,
...rest
) {
// do some validation
return {
id,
firstName,
lastName,
email,
username,
pass,
address,
...rest
}
}
function normalize(
email,
username,
...rest
) {
// do some normalizing
return {
email,
username,
...rest
}
}
async function persist({
upsert = true,
...info
}) {
// save userInfo to the DB
return {
operation,
status,
saved: info
}
}
RO 还是不 RO,这是个问题
我在一开始就说过,我的大多数函数都会接收一个对象,其中许多函数也会返回一个对象。
与其他模式一样,RORO 应该被视为我们工具箱中的一种工具。我们使用它来提升价值,例如使参数列表更清晰灵活,并使返回值更具表现力。
如果你编写的函数只需要接收一个参数,那么接收object
就有些过了。同样,如果你编写的函数可以通过返回一个简单的值来向调用者传达清晰直观的响应,那么就没有必要返回object
。
我几乎从不 RORO 的一个例子是断言函数。假设我们有一个函数isPositiveInteger
,用于检查给定参数是否为正整数,那么 RORO 很可能对这个函数没有任何好处。
如果您喜欢这篇文章,请点击♥️图标来传播它。如果您想阅读更多类似的内容,请在下方订阅我的“Dev Mastery”新闻通讯。
订阅 DevMastery 新闻通讯
我会对您的信息保密,并且绝不会发送垃圾邮件。
文章来源:https://dev.to/billsourour/elegant-patterns-in-modern-javascript-roro-5b5i