新兴的JavaScript模式:多个返回值

2020-10-29 18:29:39

在本文中,我想探索一种有趣的模式,我越来越多地在JavaScript代码中看到这种模式,它允许您从一个函数返回多个值。

您可能已经知道JavaScript本身并不支持多个返回值,因此本文实际上将探索一些“模拟”这种行为的方法。

我最近看到的这种模式最著名的用法之一是在Reaction Hooks中,但是在深入研究它之前,让我们通过在其他语言中探索这个概念来看看我所说的“多个返回值”是什么意思。

我想到的两种本机支持多个返回值的语言是Lua和Go。让我们实现一个简单的整数除法函数,它同时返回商和余数。

让我们从一个简单的Lua实现开始。绝对值得一提的是,Lua的官方文档将多个返回值定义为“一种非常规但相当方便的特性”:

函数intDiv(被除数,除数)局部商=数学。下限(被除数/除数)局部余数=被除数%除数返回商,余数结束打印(intDiv(10,3))--3 1。

Package main import";fmt";func intDiv(被除数,除数int)(int,int){商:=被除数/除数余数:=被除数%除数返回商,剩余数}func main(){fmt。Println(intDiv(10,3))//3 1}。

正如您在这两个代码片段中看到的,函数可以返回一个以上的值,这在逻辑上使您在一次计算中产生多个输出的情况下非常方便。

注意:围棋中更实际的实现是考虑错误(例如除以0),并添加额外的返回值来传播潜在的错误。出于本文的目的,我们不应该太担心这一点,但绝对值得一提的是,Go中的多个返回值在错误传播和错误处理方面大放异彩。我们将在本文后面更多地讨论这一点,以了解如何将此思想应用于JavaScript,特别是在异步/等待的上下文中。

因此,正如我们前面所说的,JavaScript本身并不支持从函数返回多个值的语法。我们可以通过使用复合值(如数组或对象)来解决此限制。

IntDiv=(被除数,除数)=>;{常商=数学。下限(被除数/除数)恒定余数=被除数%除数返回[商,余数]}控制台。Log(intDiv(10,3))//[3,1]。

这里我们只是打印除法的结果,但是让我们假设我们想要分别处理两个返回值,我们如何引用它们呢?

返回值是一个数组,因此我们可以使用索引0和1简单地访问数组中的两个元素:

常量结果=intDiv(10,3)常量商=结果[0]常量余数=结果[1]控制台。Log(`商=${商}`)//商=3console。Log(`剩余数=${剩余数}`)//剩余数=1。

这个语法可以说是冗长的,绝对不是很优雅。值得庆幸的是,ES2015阵列解构任务可以在这方面为我们提供帮助:

Const[商,余数]=intDiv(10,3)控制台。Log(`商=${商}`)//商=3console。Log(`剩余数=${剩余数}`)//剩余数=1

这本书读起来好多了,我们还删减了2行和3行代码,大赢了!

尽管该实现很好,但它有一个重要的缺点:返回值是位置的,因此在解构时需要小心并尊重顺序。

IntDiv=(被除数,除数)=>;{常商=数学。下限(被除数/除数)恒定余数=被除数%除数返回{商,余数}}。

请注意,这里我们使用的是ES2015(增强的对象文字语法)中的另一个语法糖,它允许我们非常简洁地定义对象。在ES2015之前,我们会将返回语句定义为{Quotient:Qutient,Rembers:Rembers}。

常量结果=intDiv(10,3)常量商数=结果商常数余数=结果.剩余控制台。Log(`商=${商}`)//商=3console。Log(`剩余数=${剩余数}`)//剩余数=1。

再说一次,这有点太冗长了,ES2015还有另一个奇妙的语法糖可以让它变得更好:

Const{商,余数}=intDiv(10,3)控制台。Log(`商=${商}`)//商=3console。Log(`剩余数=${剩余数}`)//剩余数=1。

这种语法糖称为对象析构赋值(Object Destructing Assignment)。使用这种方法,我们现在独立于返回值的位置(我们可以互换商和余数的位置,而不会产生副作用)。此语法还允许您重命名非结构化变量,这对于避免与其他局部变量的名称冲突非常有用,或者只是根据我们的需要使变量名更短或更具描述性。让我们看看这是如何工作的:

Const{余数:R,商:Q}=intDiv(10,3)控制台。Log(`商数=${q}`)//商数=3console。Log(`剩余数=${r}`)//剩余数=1。

在这里,我们不依赖于值的位置,而依赖于它们在返回对象中的名称。如果您正在设计一个具有多个返回值的API,那么您可以自己找出哪种折衷方案是保证正确的开发体验的最佳选择。

好了,现在您应该对如何在JavaScript中模拟多个返回值有了很好的了解。在下一节中,我们将看到一些利用此模式的更实际的示例。

正如前面提到的,该技术最近通过Reaction钩子得到了普及,因此我们将首先探讨这个用例。稍后,我们将看到与异步/等待相关的另外两个案例。

React钩子是React v16.7.0-alpha提供的一个新特性建议,它允许您使用状态和其他Reaction特性,而无需编写类。

让我们通过一个示例来看看它是如何工作的,让我们构建一个CSS颜色查看器组件。

从';react';function CssColorViewer(){const[cssColor,setCssColor]=useState(';Blue';)//<;--多个返回值const onCssColorChange=e=>;{setCssColor(e.target.value)}return(<;div>;<;input value={cssColor}onChange={onCssColorChange}/>;<;Div样式={{宽度:100,高度:100,背景:cssColor,}}/>;<;/div>;)}。

出于本文的目的,我们将只关注useState调用,但是如果您想更好地了解钩子本身在内部是如何工作的,我强烈建议您阅读官方的State Hook文档。我个人很想了解多个useState调用如何维护与特定状态属性的关系(因为没有显式的标签或引用)。如果您对此也很好奇,那么您应该阅读Hooks常见问题解答和Dan Abramov最近关于Hooks的文章。

UseState挂钩的作用类似于工厂:给定state属性的默认值(在我们的示例中为Blue),它将需要为您实例化两件事:

Reaction开发人员决定通过用一个数组模拟多个返回值来处理这一需求。

将其与数组解构和适当的变量命名相结合,结果是一个非常易于阅读和使用的API。

此Reaction功能仍处于非常实验性的阶段,在撰写本文时可能会进行更改,但是对于Reaction社区来说,让代码更具表现力并降低进入门槛以开始采用React听起来已经是件大事了。

我想指出的一点是,在这种特定情况下,多返回值模式在实现这一目标方面发挥了重要作用。

最近,在尝试将面向回调的API转换为等效的异步/等待API时,我发现了多返回值模式的另一个很好的用例。

为了弄清楚这一部分,我将非常快速地解释我用来将基于回调的API转换为可以与异步/等待一起使用的函数的一种方法。

函数doSomething(输入,回调){//...。异步执行某些操作并//计算响应或错误//完成后,调用回调:callback(error,response)}

要将此函数转换为可与异步/等待一起使用的功能,我们必须从本质上简化它。有一些库可以做到这一点,如果您使用的是Node.js,您甚至可以使用内置的util.promisify,但这是我们自己可以做的事情,只需创建一个包装器函数,如下所示:

Const doSomethingPromise=(Input)=>;new((Resolve,Reject)=>;{DoSomething(Input,(Error Response)=>;{if(Error){Return Reject(Error)}Return Resolve(Response)})})。

简而言之,我们的包装函数doSomethingPromise立即返回承诺。在承诺的主体内,我们使用一个回调调用原始的DoSomething函数,该回调将根据是否存在错误来解决或拒绝承诺。

注意:这将在出错的情况下抛出,因此请确保将其放在try/catch块中以正确处理错误。

如果您对缩短基于回调的函数感兴趣,我有一整篇文章专门介绍这个主题。

在我的特定用例中,我使用的是遵循以下约定的Twitter客户端库:

//Client是Twitter ClientClient的实例。GET(';Status/USER_TIMELINE';,params,函数回调(错误,推文,响应){IF(!Error){控制台。日志(推文)}})。

这里重要的细节是回调函数有点非常规,因为它接收3个参数:可能的错误、tweet列表和响应(表示原始HTTP响应对象)。传统的回调风格的API将只向回调函数发送两个参数:潜在错误和某种结果对象。

Const getUserTimeline=(客户端,参数)=>;new((解析,拒绝)=>;{客户端。GET(';status/user_timeline';,params,(error,twets,response)=>;{if(Error){return reject(Error)}return Resolve([twets,response])//多个返回值})。

//在异步函数内//...。设置`client`和`params`const[twets,response]=await getUserTimeline(client,params)。

简单地说,我们也可以使用多返回值模式,并承诺允许将它们解析为多个值。此技术为我们提供了非常好的界面,特别是在与异步/等待结合使用时。

我越来越多地在JavaScript中看到的另一个密切相关的模式是错误传播和处理。

在GO中,当函数可能产生错误时,该错误不会被抛出,而只是由函数返回。如果函数必须返回一些输出,并且还可能生成错误,则函数将有多个返回值(Output和Error)。

理想情况下,调用方代码应在继续操作之前验证返回的错误值是否为实际错误,如下面的GO代码示例所示:

一些JavaScript库开始推广与Go中相同的约定来报告错误,特别是在异步/等待方面。

让我们重写前面示例中的getUserTimeline函数,以遵循此方法:

Const getUserTimeline=(客户端,参数)=>;new((解析,拒绝)=>;{客户端。GET(';status/user_timeline';,params,(error,twets,response)=>;{Return Resolve([error,twets,response])//多个返回值})})。

请注意,我们从未告诉承诺拒绝,所以当我们将此函数与异步/等待一起使用时,我们不能使用try/catch来处理错误。我们应该这样做:

//在异步函数内//...。Set`client`and`params`const[error,twets,response]=await getUserTimeline(client,params)if(Error){//此处处理错误}//...。用`tweets`和`Response`做一些事情。

我不确定我是否会在JavaScript领域推荐此模式。我对此有一点复杂的感觉。一方面,我相当喜欢它,因为正如Go中发生的那样,它迫使您单独处理每个错误,这会让您更仔细地考虑处理错误细节的最佳方式。另一方面,这个模式仍然让人感觉有点被强加到JavaScript中,那些从未在其他语言中看到过这个模式的人可能会觉得它很烦人,甚至很难理解。此外,如果您不捕获和处理错误,这将不会自动升级(就像抛出或拒绝承诺时发生的那样),因此错误将完全被运行时吞噬,从而导致应用程序状态中潜在的不一致。我会让您对此得出您自己的结论!😇。

现在,我想向您展示一些可以用于数组解构的“小技巧”,它们在处理多个返回值时可能会派上用场。

例如,假设您有一个函数doStuff,它使用一个数组返回多个值,数组中的值是error、rawResponse和result。假设您对使用rawResponse不感兴趣,仅出于本例的原因,您可以在使用以下语法解构时轻松跳过该元素:

请注意那里的双逗号。这基本上意味着我们没有分配数组索引。您可以随意弯曲此技术,例如,您可能决定仅变形结果:

另一个有趣的把戏是你可以用特殊的..。语法将剩余的返回值累加到单个变量下。让我们举一个虚拟的例子来探讨这个想法。

假设我们有一个名为listDog的函数,它的实现如下所示:

数组的第一个元素是狗的数量,而其他每个元素都是实际的狗的名字。

因为这里返回的元素数量是可变的,所以直接使用析构可能很棘手。在这些情况下,我们可以使用此特殊语法:

那个..。语法基本上允许您将数组的任何剩余元素分解到另一个数组中。当然你只能有一个..。元素,并且该元素必须是列表中的最后一个元素。

我这里的示例非常虚假,但是这个模式在一些真实的用例中非常有用,例如,当您想要从正则表达式匹配中解压缩值时。

当Reaction钩子出现时,社区对这一新功能非常兴奋,但Google Chrome团队的一些性能专家(@bmeurer、@_developit和@rossmcilroy)都表示惊讶,想要更深入地挖掘,看看这种新方法是否会在网络上导致严重的性能问题。

他们的研究发表了一篇令人惊叹的论文,标题是“多值返回的数组析构(根据Reaction钩子)”。

我真的鼓励你阅读它,以获得所有的细节,但这里我试图给你一些TLDR;

在Reaction组件中,Render()方法经常被调用,因此那里的代码应该优化到不会减慢速度。这就是你使用反应钩子的地方。

数组解构是一种非常通用的API,它不仅适用于数组,而且适用于每种类型的可迭代对象,因此VM有很多工作要做才能弄清楚如何遍历和解析特定对象。

对象析构可能是一种性能更好的替代方案,但它可能会给开发人员带来不太愉快的体验。

如果您在松散模式下使用Babel,则数组解构将得到高度简化(以直接访问元素),并将产生高度优化的代码。

即使您没有使用Babel,仍在继续研究Google的V8引擎中的优化编译器的不同阶段是否能够优化解构。

在本文中,我们讨论了这种新出现的JavaScript模式,它越来越受欢迎,这主要是因为它在Reaction中被采用。

不过,用例并不局限于RESPECT,我希望您会发现这在您的日常开发生活中也是在RESPECT领域之外有用的。

我真的很好奇您会想出什么,所以请使用Twitter或本文中的评论与我保持联系。

对于非常好奇的问题,我想给您最后一个链接,这样您就可以比较多个返回值是如何在许多其他语言中实现的。