iWeb 学院

反思JavaScript:论for循环的死亡

我们一直在使用JavaScript的for 循环,但现在,在最新的函数式编程技巧的支持下,过时的它应该退休了。

幸运的是,这样一种改变,并不要求你成为一个函数式编程大师。更幸运的是,这就是眼前项目中可以立马做的事情!

那到底JavaScript的for循环有什么问题?

for 循环的设计会激起状态的可变以及对副作用的使用,这两者都会是导致错误和不可预知代码的潜在来源。

我们都知道全局状态是糟糕的,应该避免。可是,局部状态和全局状态一样糟糕,我们只是没有注意到,因为局部状态处于一个较小的规模中。因此,我们从来没有真正解决这个问题,我们只是忽略了这一点。

对于可变的状态,在一些未知的时间点,变量会因为某些未知的原因而改变。这时,你将会消耗数小时进行调试,寻找这个值改变的原因。光想这个问题,我就要抓头发了。

下面,我想赶紧谈谈副作用。这词听起来就烦人,副作用。。。妈蛋!难道会希望自己的程序有什么副作用么?当然不!

但什么是副作用? 当一个函数修改了这个函数作用域以外的某些东西时,它就被视为有副作用。可以是改变一个变量的值,读取键盘输入,产生一个接口调用,写入磁盘数据,打印日志,等等。

副作用很强大,但拥有强大权力的同时也被赋予了重大的责任。

副作用应该尽可能的被消除,或者封装在内部,或可控。有副作用的函数难以测试,也难以解释,所以尽你所能甩掉它。幸好这里我们先不用去担心副作用。

好了,闲话少说上代码。我们看一下这段或许你已经看过上千次典型的for循环

const cats = [
  { name: 'Mojo',    months: 84 },
  { name: 'Mao-Mao', months: 34 },
  { name: 'Waffles', months: 4 },
  { name: 'Pickles', months: 6 }
]
var kittens = []
// 典型的拙劣写法 `for loop`
for (var i = 0; i < cats.length; i++) {
  if (cats[i].months < 7) {
    kittens.push(cats[i].name)
  }
}
console.log(kittens)

我的计划是将这些代码一步一步的重构,这样你能清楚的看到将这些代码转换成更漂亮的写法是多么容易。

我要做的第一个改变就是抽出if里的声明。

**const isKitten = cat => cat.months < 7**
var kittens = []
for (var i = 0; i < cats.length; i++) {
  if (**isKitten(cats[i])**) {
    kittens.push(cats[i].name)
  }
}

通常抽出if语句是个不错的实践。从“小于七个月”到“判断是一个小猫”的过滤变化很重要。现在,当你再看这段代码,意图就变得清晰了。为什么要获取小于7个月的猫?一点都不明确。我们的意图是找到小猫,所以让代码表示出来!

另一个好处是iskitten现在就是可复用的了,而且我们都明白:

让代码可复用应该始终是我们的目标之一。

下一个改变就是提取出从对象中猫的类型到对应名称的转换(或者映射)。这种变化以后会更有意义,所以现在你只需要相信我。

const isKitten = cat => cat.months < 7
**const getName = cat => cat.name**
var kittens = []
for (var i = 0; i < cats.length; i++) {
  if (isKitten(cats[i])) {
    kittens.push(**getName(cats[i])**)
  }
}

我原本打算再描述一下filtermap的具体细节。但我想不描述它们而是展示给你理解这段代码是多么容易,即使没有介绍过map或者filter,这能最好的展示出你的代码可以变得多么易读。

const isKitten = cat => cat.months < 7
const getName = cat => cat.name
const kittens =
  **cats.filter(isKitten)
      .map(getName)**

还要注意,我们已经消除了 kittens.push(...)。不再有可变的状态,也不再有var!

使用const的代码有如魔鬼般性感(超过了var和let)。

这里充分说明下,我们自始至终都可以使用const是因为const并不会使对象本身不可变(更多是在于这一次不可变)。但是,嘿,这是一个人为的例子,所以放我一马!

我建议的最后一个重构就是提取出过滤和映射方法(你懂的,就是复用那回事)。

整合一起就是这样:

const isKitten = cat => cat.months < 7
const getName = cat => cat.name
const getKittenNames = cats =>
  cats.filter(isKitten)
      .map(getName)
const cats = [
  { name: 'Mojo',    months: 84 },
  { name: 'Mao-Mao', months: 34 },
  { name: 'Waffles', months: 4 },
  { name: 'Pickles', months: 6 }
]
const kittens = getKittenNames(cats)
console.log(kittens)

你会如何进一步分解这些函数?仔细想想'小于'或者'name'属性,'map'或'filter',说不定还有额外的收获。你还可以研究下函数的结构,收获双倍。

许多人问,“break有什么用?”参考第2部分:break就是循环的GOTO。 反思JavaScript: break就是循环的GOTO 在我上篇文章-for循环的死亡中,我试图说服你抛弃for循环获得更多复用功能...medium.com

请在下面的评论区留下你的观点。对你来说,for循环是不是也没用了呢?

我知道这很微不足道,但收到Medium和Twitter(@joelnet)上的留言对我意义非凡。或者如果你觉得我完全是扯淡,也请评论告诉我。 感谢!

400-710-1024
北京市昌平区禧乐汇商城5层(iWeb学院)