Fork me on GitHub

After code
  • Geeker
  • Gamer
  • JS
  • C
  • Node
  • React
  • Hippop
  • TDD
That Is An Byte of Me

[简译]这些情况下别用 for 循环

19 Jun 2017

最近在做一个”程序员英语娱乐主题节目”http://www.douyu.com/aftercode的斗鱼直播, 直播的时候简单的解读了下Don’t pay the for-loop tax. 事后想想觉得觉得蛮有意思的,觉得可以写篇 blog.

原文简译

原文作者的意思,在日常开发和 code review 的时候发现了很多没有必要的for循环,而这些循环可以通过一些更加可读的方式来实现.所以作者开玩笑定义了一个 for 循环税, 向那些写了没有必要的 for 循环的程序员征收.

接下来列举下作者说的4种情况

1. 通过一个数组获得一个结果

比如数组求和

const sum = (array) => {
    let result = 0;
    for (let i = 0; i < array.length; i++) {
        result += array[i];
    }
    return result;
}

const numbers = [5, 25, 8, 18];
console.log(sum(numbers)); // logs 56

这样写求和是完全没有问题.只是这样来求和,你需要注意一些细节;i循环变量的控制要从0开始,而且是小于array.length. 有注意的地方的话就可能写错,滋生 bug.

比较推荐的方法不是直接用for 循环,而是采用 reduce(有些语言叫fold或者aggregate).这样可以完全不用去理会循环变量,关注你的求和的逻辑就好了.

const sum = (array) => array.reduce(
  (total, current) => total + current,
  0);

const numbers = [5, 25, 8, 18];
console.log(sum(numbers)); // logs 56

2. 从一个数组通过变化得到一个新的数组

const discount = (originalPrices, discountAmount) => {
    const multiplier = 1 - discountAmount;
    // we must clone the array
    let result = new Array(originalPrices);
    for (let i = 0; i < originalPrices.length; i++) {
        result[i] = originalPrices[i] * multiplier;
    }
    return result;
}

const prices = [5, 25, 8, 18];
console.log(discount(prices, 0.2)); //logs [ 4, 20, 6.4, 14.4 ]

这 for 循环也和上一个有一样的循环变量需要控制.推荐的做是采用map方法.那采用map 函数带来的另外一个好处就是,map函数直接返回一个新的数组,这样保证的原先的数组没有被修改过.

const discount = (originalPrices, discountAmount) => {
    const multiplier = 1 - discountAmount;
    return originalPrices.map(price => price * multiplier);
}

const prices = [5, 25, 8, 18];
console.log(discount(prices, 0.2)); // logs [ 4, 20, 6.4, 14.4 ]

3. 需要含有从 n 到 m 的数组

其实这个例子是我觉得比较值得商榷的例子.作者举的例子是获得前 n 个自然数的平方.

作者给出的反例是

const squaresBad = (n) => {
    let result = [];
    for (let i = 1; i <= n; i++) {
        result.push(i * i);
    }
    return result;
}

const squares = (n) => {
    let result = new Array(n);
    for (let i = 1; i <= n; i++) {
        result[i - 1] = i * i;
    }
    return result;
}

console.log(squaresBad(5)); // logs [ 1, 4, 9, 16, 25 ]
console.log(squares(5)); // logs [ 1, 4, 9, 16, 25 ]

第一个 squaresBad 实现非常朴素,按照顺序一个一个 push.作者认为这样的方法不好的原因是在于这样一直 push 存在性能问题. 笔者认为仅仅是因为性能问题否定这个 for 循环是太草率.

那第二个squares 实现虽然通过”预分配”数组的空间来避免了性能问题,但是需要result[i - 1] 其实也是需要注意的一个细节,也是一个容易出错的一个点.

那作者认为比较好的实现是什么呢

const range = require("lodash.range")
const squaresLodash = (n) => range(1, n + 1).map(
    (n) => n * n);

const squares = (n) => [...Array(n).keys()].map(
    (n) => (n + 1) * (n + 1));

console.log(squaresLodash(5)); // logs [ 1, 4, 9, 16, 25 ]
console.log(squares(5)); // logs [ 1, 4, 9, 16, 25 ]

一个是通过第三方库 lodash 来生成 n 到 m 的数字,然后通过 map 函数来处理数据.另外一个利用 es6 的语法糖 ...展开数组的方式. 笔者还是比较认同第一个方法,虽然带来了学习三方库的成本,但是显然更加的可读;然而利用语法糖的方案,其实在阅读时候反而带来了费解.

4. 如果你真的需要重复有副作用的调用.

作者认为你应该抽象一个工具函数doNTimesLoop来做重复的动作.而且还建议如果你的 js 环境支持尾调优化的话可以采用递归的方式来实现.

const doNTimesLoop = (n, f) => {
    for (let i = 1; i <= n; i++) {
        f(i);
    }
}

const doNTimesRec = (n, f) => {
    const body = (m) => {
        if (m > n) return;
        f(m);
        return body(m + 1);
    }
    return body(1);
}
//both log "Hello world" five times
doNTimesLoop(5, x => console.log("Hello world"));
doNTimesRec(5, x => console.log("Hello world"));

到这里还没完

如果你看到这里觉得还蛮有意思的话,或者你这里觉得没什么意思的话,那我要告诉等你的是”从来评论才是本体”. 显然这边富有争议的文章会带来了激烈的讨论. 那我”简译”几个有意思的评论

benchmar打脸篇

原文作者在第三个问题”需要从 n 到 m 的数字”, 说对一个空数组做 result.push(i * i) 会存在性能问题;之后有给出ES6 语法糖 ...的解决方案.那有一个读者给出了一个简单的 benchmark

console.time("Array.push");
var array = [];
for (var i = 0; i < 1e7; i++) array[i] = i;
console.timeEnd("Array.push");
// Array.push: 220ms

console.time("new Array.push");
var array = new Array(1e7);
for (var i = 0; i < 1e7; i++) array[i] = i;
console.timeEnd("new Array.push");
// new Array.push: 47ms

console.time("Array.map");
var array = [ ...Array(1e7).keys() ].map((_, i) => i);
console.timeEnd("Array.map");
// Array.map: 1209ms

当然除了吐槽性能查之外,也和我一样觉得这样的语法糖适得其反,搞得更加晦涩难懂.当然这个出打脸的同学也知道”做人留一线日后好见面”, 说作者文章的其他部分还是很 spot-on (准确的).

那针对对 ES6 语法变的更加晦涩的部分,有一个路人给出了一个新的方案,也让人眼前一亮.

console.time("Array.from");
var array = Array.from({length:1e7},(_, i) => i);
console.timeEnd("Array.from");

但是性能依旧不堪,笔者在自己的 MacBook Pro (Retina, 13-inch, Late 2012),用 node v7.10.1 跑的 benchmark 结果如下

Array.push: 378.066ms
new Array.push: 112.525ms
new Array.push rev: 108.405ms
Array.map: 2505.947ms
Array.from: 1345.321ms

前后矛盾的作者,最后只好说 “Make it run, make it right, make it fast, make it small”. 早知如此就不应该一开始说别人有性能问题了 : p

隔壁老王也来打脸

其实作者写了这么多无非就是为了让 JS 写的更加函数式一点,这时候出来一位大神(姑且称之为老王).老王说采用函数式编程的方式,HaskellScheme,即使是 python 实现第一个对数组求和问题的时候都非常的清晰.但是使用 js 的 reduce 方法的话看起来就怪怪的. (total, current) => total + current光从代码层面来看, 为什么要把 totalcurrent 相加呢?

老王又说虽然for(;;)的形式是很容易出错,但是for infor of的写出来的代码也很清晰易懂啊.接着又不点名批评了下那些类似Ramada的库造就的如此晦涩的代码 apply(compose(fn1, fn2.bind(arg)), value).接着补刀道,毕竟 js 还是 js 不像我大Haskell写的如此的简洁 .

最后老王说了写 js 的时候如果一味的追求函数式编程,就和以前一味的追求写面向对象(OOP)的代码一样, 原本在 js 中很简单的实现一定要生搬硬套,结果就是代码反而变得更加啰嗦.(老王这里举的例子就是js 中实现设计模式中的”命令模式”, 而这些命令模式的类其实就可以用一个简单的函数就能替代.)

面对大神碾压视角的评论,原作者也先俯首称臣表示,你说的挺有意思的;虽然你提的for of是个不错的选择,但是我还是觉得用 reduce比较爽. 呵呵!

直接怼的选手

这位选手上来直接就说:”你说什么 for 循环税,老子还说 数组方法税呢.” 他的观点就是有时候那些刻意的”函数式”的代码对象与直接使用 for 循环来说反而更加的简洁和可维护性高;只是作者给的都是一些反例罢了.

真的快完了

简单的一个关于 for 循环的讨论炸出来了这么有意思的评论,首先国外的技术圈的氛围真的很好.

回到问题本身,我的观点是还是尽量使用这些所谓的数组函数,因为这些函数已经抽象出了一部分的逻辑;当你看到map,reduce,filter的时候你就知道代码的大概的意图,而且主要的逻辑的写在对应的回调里面了.如果你一定要用 for 循环的话,个人比较推荐使用下面两种形式

for (const player of players){
	// do with player
}
for (const [index,player] of players.entries()){
	// do with index, player
}

如果你在循环体里面又要continue 又要break的, 其实使用其他的循环方式可能更加合适(比如do whilewhile)

希望大家喜欢.

分享到: QQ空间 新浪微博 腾讯微博 微信 更多
comments powered by Disqus