wleonardo

反思JavaScript: 通过函数代替break

wleonardo · 2017-01-25翻译 · 1207阅读 原文链接

在我的上一篇文章 Death of the for Loop中,我试图去说服你放弃使用for 循环改用函数式的解决方案。反过来,你提出了一个很好的问题,那么for循环中break怎么办?

> break 会相当于循环中的GOTO,我们应该避免使用。

break 应该像GOTO一样被废弃。

你可能会想“算了吧Joel,你这只是耸人听闻,break怎么可能会像GOTO一样?”

// bad code. no copy paste.
outer:
  for (var i in outerList) {
inner:
    for (var j in innerList) {
      break outer;
    }
  }

我可以提供标记作为证明。在其他语言中,标记和GOTO是相互对应的。在JavaScript,标记与breakcontinue也是相互对应的。因为breakcontinue来自于相同标记组,这也导致了它们和GOTO很像。

> JavaScript标签,break和continue是GOTO和非结构化编程时代的遗留

xkcd

“但是如果它没有伤害任何人,那么我们为什么不把它留下语法中,而我们可以选择其他的方案?”

我们为什么限制我们如何编写软件?

这个听上去有些违背直觉,但是限制是一个好事。限制我们使用GOTO就是一个很好的例子。我们也很欢迎限制我们的“use strict”,甚至批评不使用它的人。

> “limitations can make things better. A lot better. “— Charles Scalfani

限制(规则)可以使我们写出更好的代码。

为什么编程需要限制 _限制使艺术,设计,生活更美好._medium.com

我们对于break的选择是什么?

我不是要做一个虚有其表的事情,但是也没有一个方案可以适合所有的情况。 这是一个完全不同的编程方式。 一个完全不同的思考方式。函数式编程的思想。

有一个好消息是,有很多的库和工具可以帮助我们,例如Lodash, Ramda, lazy.js, 递归等等

我们将从一个简单的cats集合和一个isKitten函数开始,这些将在下面所有的例子中被用到。

const cats = [
  { name: 'Mojo',    months: 84 },
  { name: 'Mao-Mao', months: 34 },
  { name: 'Waffles', months: 4 },
  { name: 'Pickles', months: 6 }
]
const isKitten = cat => cat.months < 7

让我们从一个我们熟悉的for循环的例子开始。它会遍历我们的cats,然后当找到第一只小猫的时候退出循环。

var firstKitten
for (var i = 0; i < cats.length; i++) {
  if (isKitten(cats[i])) {
    firstKitten = cats[i]
    break
  }
}

现在,让我们和lodash中一个相同作用的例子做比较。

const firstKitten = _.find(cats, isKitten)

这个例子相当的简单。接下来让我们尝试一些边缘情况吧。现在我们改为遍历cat集合,然后选出前5只小猫,然后退出循环。

var first5Kittens = []
// old-school edge case kitty loop
for (var i = 0; i < cats.length; i++) {
  if (isKitten(cats[i])) {
    first5Kittens.push(cats[i])
    if (first4Kittens.length >= 5) {
      break
    }
  }
}

简单的方式

lodash是一个很好的库也可以坐很多的事情,但是有时候你需要一些其他更加专业的工具。这里我们介绍一个新朋友, lazy.js. “像Underscore,但是更加偷懒”。但是偷懒就是我们想要的.

const result = Lazy(cats)
  .filter(isKitten)
  .take(5)

困难的方法

库都是有趣的,但是有时候真正有趣的是从头开始创造东西

所以我们可以创建一个通用的函数,让它可以像filter一样使用也可以增加限制的功能。

第一步就是把我们上面写的边缘情况的for循环封装在一个函数中。

接下来,让我们是这个函数更加通用并且遍历所有cat具体的内容。使用limit来代替5,predicate来代替isKittenlist来代替cats。然后把这些作为函数的参数。

现在我们有了一个可用的且可重复的takeFirst函数,这个可以让我们完全不用去关心我们cat的逻辑实现!

我们的函数现在依旧还是一个纯函数。也就是说函数的输出只和输入的参数有关。如果传入相同的参数,一定会得到相同的结果。

现在我们已经还是有那个肮脏的for循环,所以让我们继续重构。下一步就是把inewList放入参数列表。

limit变为0的时候 (limit会在递归过程中减少)或者是遍历完了列表,我们希望可以退出递归(isDone)。

如果递归还在进行,我们将会核对是否有符合我们的过滤条件predicate的值。如果当前值符合过滤条件,我们会调用takeFirst,减少limit并把当前值保存在我们的newList中,否者,移动到列表的下一个值。

如果你还没有看过Rethinking JavaScript: The if statement,它会解释这个用三元表达式代替if'的最后一步。

Rethinking JavaScript: The if statement _Thinking functionally has opened my mind about programming._medium.com

现在我们像下面这样调用我们的新方法:

const first5Kittens = takeFirst(5, isKitten, cats)

为了兼容更多的情况,我们可以柯里化takeFirst,然后使用它去创建一些其他的函数(关于柯里化的介绍在另一篇文章中)

const first5 = takeFirst(5)
const getFirst5Kittens = first5(isKitten)
const first5Kittens = getFirst5Kittens(cats)

总结

现在有很多优秀的库例如 lodash, ramda和lazy.js供我们使用。但是如果我们足够大胆,也可以使用递归来创建我们自己的函方法。

我必须要警告虽然takeFirst看上去很酷 但是使用递归是有得也有失的. 递归在Javascript中是很危险的,它很容易就会导致超出最大调用堆栈大小的报错。

我将会在我的下一篇文章中重写JavaScript的递归。敬请关注。

我知道这只是一件小事,但是当我收到来自Medium和Twitter (@joelnet)的follow通知时会使我很开心。当然,如果你觉得我实在胡说八道,你也可以在下面的讨论区告诉我。

Cheers!

译者wleonardo尚未开通打赏功能

相关文章