【问题标题】:When/why to use map/reduce over for loops何时/为什么在 for 循环中使用 map/reduce
【发布时间】:2015-06-20 19:48:23
【问题描述】:

所以我是第一次在 JavaScript 中进行一些对象操作,我有一个问题想知道是否有人可以回答。

当我有一个想要操作的对象时,我可以做一些嵌套 for 循环的事情,但是 JavaScript 中内置了一些函数,如 map/reduce/filter,以及 lodash/underscore 等库。

我认为后者(map/reduce/filter 和库)是更好的做法,但我只是好奇为什么。

我正在做一些非常基本的对象操作,可以通过一些放置良好的 for 循环来抓取和更改对象中正确的键/值来解决,但可以使用 JS 中的函数/库轻松完成。只是好奇它们如何变得更好——比如更好的性能/更简洁的代码/易用性/其他。

抱歉,没有代码。非常感谢任何人在这里帮助我了解更多信息。

编辑 - 因此从 map() 的示例中获取

我可以以 javascript.map 为例

 var kvArray = [{key:1, value:10}, {key:2, value:20}, {key:3, value: 30}];
var reformattedArray = kvArray.map(function(obj){ 
var rObj = {};
rObj[obj.key] = obj.value;
return rObj;
});

我可以做类似的事情

   var kvArray = [{key:1, value:10}, {key:2, value:20}, {key:3, value: 30}];
var reformattedArray = [];

for(var object in kvArray){
  //combine both values into object inside of kvArray[object]);
 };

代码少了很多 - 但还有其他值得了解的好处吗?

【问题讨论】:

  • 酷 - 我什至不知道这些功能。看到你的标题后,我不得不搜索它。然后,我发现了这一点 - 关于您的主题,这似乎是一本非常好的读物:From Map/Reduce to JavaScript Functional Programming
  • Map 生成一个新数组。 Reduce 会累积某种单一的值或对象。如果它们提供了您需要的输出类型,您可以使用它们而不是 for 循环或 .for each。所有这些都是为某些类型的输出设计的专用迭代器。 for 循环是其他循环的超集。更通用,但需要更多输入。
  • @ajmajmajma 因为这完全取决于他想要什么。也许使用map 最适合他的情况,也许下划线_.some 是或者也许filter...你能回答谁任何情况下的最佳实践?没有完美的方法对所有事情都是最好的。
  • 有几次我测试了.foreach() 与普通for 循环的性能,for 循环每次都胜出。我没有测试过.map().reduce(),但如果不相似,我会感到惊讶。可能的问题是所有辅助函数都必须为循环的每次迭代调用函数回调,这是一些额外的开销,但for 循环不需要这种开销。如果你真的想知道,你应该根据你的具体情况在几个浏览器中运行 jsperf 基准测试。
  • ...从以前的评论继续 - 任何性能差异通常都没有意义,因此您应该使用使代码最干净的任何东西。 for 循环还具有能够随时停止循环的优点,并且它适用于实际上不是数组的类数组结构(例如 HTMLCollection)。

标签: javascript


【解决方案1】:

mapreduce 等是类似容器的数据结构应该实现的功能,以便消费者可以使用它们而无需了解它们的内部结构。这些函数接受您的逻辑作为输入。这允许您在不影响其使用者的情况下对这些数据结构的内部进行更改。

map 优于 for 循环的真正原因是,随着应用程序的发展,它们更容易开发。如果您的需求发生变化以至于您现在有了一个对象怎么办?

import map from 'lodash/fp/map';
import mapValues from 'lodash/fp/mapValues';

const before = map(logic, data);
const after = mapValues(logic, data);

再一次,如果您的需求发生变化,现在您有了一棵树怎么办?好吧,现在您是一名优秀的开发人员,意识到负责遍历树的代码应该与业务逻辑分离。它应该编写一次,彻底测试一次,由拥有数据结构的团队维护,并接受业务逻辑作为输入。所有这些都是为了让消费者不必担心它的内部结构。

相反,消费者应该这样做。

import { mapTreeValues } from 'tree'

const before = map(logic, data);
const after = mapTreeValues(logic, data);

长话短说,for 应该只被数据结构的所有者用来实现诸如mapreduce 等功能。这些功能不应该与业务逻辑耦合,而是应该接受它作为输入。还记得单一职责原则吗?

除此之外:为了抢占与自定义迭代的比较,这些高阶函数有利于提高组合的易用性。例如:

getUser().then(updateUser)
getUsers().then(map(updateUser))

【讨论】:

    【解决方案2】:

    .map() 允许您通过迭代原始数组并允许您运行某种自定义转换函数来创建新数组。 .map() 的输出是一个新数组。

    var orig = [1,2,3,4,5];
    var squares = orig.map(function(val) {
        return val * val;
    });
    console.log(squares);   // [1,4,9,16,25]
    

    .reduce() 允许您遍历一个数组以累积单个结果或对象。

    var orig = [1,2,3,4,5];
    var sum = orig.reduce(function(cum, val) {
        return cum + val;
    }, 0);
    console.log(sum);    // 15
    

    这些是专门的迭代器。当这种类型的输出正是您想要的时,您可以使用它们。它们不如for 循环灵活(例如,您不能像使用for 循环那样在中间停止迭代),但是对于特定类型的操作和知道的人来说,它们的输入更少他们,他们可能更容易看到代码的意图。

    我自己没有测试过.map().reduce()for 循环的性能,但我看到.forEach() 的测试表明.forEach() 在某些浏览器中实际上速度较慢。这可能是因为.forEach() 循环的每次迭代都必须调用您的回调函数,而在普通的for 循环中,您不必进行这样的函数调用(代码可以直接嵌入其中)。在任何情况下,这种类型的性能差异实际上很少见,您通常应该使用使代码更清晰、更易于维护的构造。

    如果您真的想优化性能,您必须在 jsperf 之类的工具中编写自己的测试用例,然后在多个浏览器中运行它,看看哪种方式最适合您的特定情况。


    普通for 循环的另一个优点是它可以与支持索引但不支持.reduce().map() 的类数组对象一起使用。

    而且,for/of 循环可用于任何实现迭代器协议的对象,例如 HTMLCollection。

    【讨论】:

    • 关于性能的段落极具争议:当今的 JS 引擎是复杂的玩具,从技术上讲,for 循环可能会优化 100 倍,因为它的主体在相同的函数范围内。参考(俄语,但阅读质量非常非常,所以请使用谷歌翻译):mrale.ph/blog/2015/04/12/jsunderhood.html 所以说某事通常更慢/更快是推测性的(并且令人困惑)
    • @zerkms - 帮助我了解什么是“极具争议”?
    • 从我的角度来看,for.map()/.forEach() 和其他人的性能的理想答案是:它们是相同的,它们的复杂性是线性的(与一个集合)。
    • @zerkms - 我报告了我看到的测试结果。我并没有说它在所有浏览器中都是一样的,并且永远不会改变。此外,我并没有说.map().reduce() 会执行相同的操作,尽管我说如果它们的工作方式相似,我不会感到惊讶。您似乎混淆了 JS 引擎可以做什么与大多数 JS 引擎的实际测量性能实际上是什么。如果你想展示.forEach()for 循环更快,请分享。
    • @zerkms - 我只能通过我看到的测试结果。我不怀疑有可能使.forEach()for 循环一样快或更快,但该理论与V8 目前在Chrome 中实际所做的不同。 Firefox 似乎真的可以做到你所说的。
    【解决方案3】:

    总结高阶数组方法(mapreducefilter 等 - 我将这些称为 HOMs)与 for 循环之间的区别,并包括其他几点:

    • 计数器变量:for 循环引入了引入错误的变量,例如:OBOE;块范围错误(由于 letvar 声明范围的差异而进一步复杂化)
    • 可用性:HOM 仅适用于数组对象 (Array.isArray(obj)); for 循环可用于实现迭代器协议的对象(包括数组),
    • 提前执行退出:HOM 没有;循环对此有 breakreturn 语句,
    • 连续 async 迭代执行:在 HOM 中不可能。请注意,控制台日志记录执行之间只有一个循环可以延迟:
    // Delays for a number of milliseconds
    const delay = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms));
    
    const items = ['a', 'b', 'c'];
    const printItem = async (item) => {
      await delay();
      console.log(item);
    }
    
    const testForLoopParallelism = async () => {
      for (const item of items) {
        await printItem(item);
      }
    };
    const testHigherOrderParallelism = () => {
      return Promise.all(items.map(async item => await printItem(item)));
    }
    
    const run = async () => {
      // Prints consecutively at a rate of ~1s, for a total of ~3s
      console.time('for');
      await testForLoopParallelism();
      console.timeEnd('for');
    
      // Prints all concurrently, for a total of ~1s
      console.time('HOM');
      await testHigherOrderParallelism();
      console.timeEnd('HOM');
    };
    
    run();
    

    不太重要但值得注意:

    • 冗长:数组 HOM 可能比 for 循环短
    • 主观上更容易阅读:这对于 HOM 和 for ... in 循环都有争议
    • 主观上更好理解:对于 Javascript 新手来说,循环可能比 HOM 更广为人知
    • 性能:可能存在性能差异,需要根据具体情况在高性能代码库中考虑这些差异
    • 不变性辅助:可能有助于提供不变性 - 尽管可以使用 HOM 或 for 循环创建突变,例如,
    items.map((item) => {
      items.push(item);
      return `${item}x`;
    });
    

    【讨论】:

      【解决方案4】:

      刚碰到这个,发现没有一个答案突出 for-loopmap 之间的一个重要区别,即何时使用一个而不是另一个:

      1. 使用map,您无法摆脱使用for-loop 的迭代。

      例如,你不能这样做

      const arr = [5, 6, 9, 4];
      arr.map(elem=>{
         if(elem === 5){
           break; //This is not allowed
        }
      })
      

      【讨论】:

        【解决方案5】:

        我知道我正在回复一个旧答案,但只是想为未来的读者指出。

        Map reduce 和 filter 函数来自函数式编程世界。

        这些是 Lisp、Haskell 和其他语言(ml?)等语言中的一流内置运算符。 函数式语言倾向于在不可变数据上运行运算符,而不是让代码在数据上运行以对其进行操作(比如循环)。 因此,与提供 for 和 while 循环相比,它们提供了更简单但功能强大的接口,例如 map、filter 和 reduce。

        它还可以帮助他们满足其他要求,例如不变性等。这就是为什么地图会给你一张新地图而不是改变旧地图的原因。从并发的角度来看,它们非常好,尽管在某些情况下它们可能会更慢。

        这种方法通常可以减少多线程或高并发应用程序中的代码错误。 当多个参与者对同一条数据采取行动时,不变性有助于防止代码相互踩踏。

        由于 javascript 试图通过提供函数式编程语言的一些功能来实现部分功能,因此在其中实现 map、filter 和 reduce 函数可能也是有意义的。

        YMMV 取决于您使用提供的工具做什么。

        如果您的代码使用 for 循环效果更好,请使用它。

        但是,如果您发现异步代码大量使用公共数据,而您最终会费尽心思尝试调试循环。 打个招呼,映射、归约和过滤。

        【讨论】:

        • 我滥用了这个...Artistic License。如果你是那种书呆子,请忽略。
        • 感谢您找到问题的根源:不变性。并不总是关于哪些代码运行得更快或看起来更漂亮。
        • 当文件中某处有全局变量时,它也能避免奇怪的错误。
        【解决方案6】:

        forEach():为每个数组元素执行一次提供的函数(回调)。不返回任何内容(undefined),但允许此回调改变调用数组。

        ma​​p():为每个数组元素执行一次提供的函数(回调),并使用此执行结果创建一个新数组。它不能改变调用数组的内容。

        结论 需要返回新数组时使用map(),要更改原始数组时使用forEach()for

        【讨论】:

          【解决方案7】:

          在搜索其他内容时遇到了这个问题。因此,即使它是一个旧线程,也要尝试回答它,因为无论如何这些概念都适用。

          如果您考虑性能和灵活性,“for”循环总是胜过其他循环,只是因为它没有为每次迭代调用函数的开销并且可以用于任何目的。

          但是,forEach、map、reduce 等函数还有其他好处(我们称它们为函数式方法)。它主要是可读性、可维护性。

          下面是for循环的几个缺点

          1. 将新变量引入范围,仅用于计数器/迭代
          2. 由于对计数器变量的无意更改,难以调试错误。这变得更加困难,并且随着带有 in 循环的循环数量的增加,犯错的机会也会增加
          3. 开发人员习惯于使用循环变量作为 i、j、k。一旦循环增加了某些代码行,就很容易忘记代码正在执行哪个计数器和哪个内部循环
          4. 在 ES6 中,“let”至少引入了有限/本地范围。但在此之前,for循环引入的变量有一个函数作用域,导致更多的意外错误

          为避免所有这些,建议在您知道自己必须做什么时使用诸如 forEach、map、reduce 之类的函数(不要忘记这些函数方法中的大多数都提供不变性)。为了更好更简洁的代码,在性能方面做出了小小的牺牲。

          在 ES6 中,大部分函数式方法都由语言本身支持。它们已经过优化,我们不必依赖像 lodash 这样的库(除非有显着的性能提升)。

          【讨论】:

            【解决方案8】:

            这就像问我更喜欢篮球还是足球。两者都有其积极的一面。

            如果您有 10 位开发人员查看您的 for 循环,那么 10 人中有 9 人会立即知道您在做什么。也许有一半的人需要查看map() 方法是什么,但他们也会知道发生了什么。所以在这方面,for循环更容易被其他人阅读。

            另一方面,map() 将为您节省两三行代码。

            就性能而言,您会发现map() 是在内部使用类似于 for 循环的东西构建的。如果您通过大量迭代运行它们,您可能会在性能速度方面看到几毫秒的差异;但最终用户永远无法识别它们。

            【讨论】:

            • 很公平,我只是好奇,因为似乎大多数人使用这些函数或库,我想知道是否有一些可怕的理由不使用它们。谢谢!
            • “所以在这方面,for 循环更容易被其他人阅读。” --- 如果你不教你的后辈——你将永远使用古老的技术,因为“否则对他们来说很难”
            • 我几乎不认为 for 循环应该被视为“古老的技术”,特别是因为它通常比专门的迭代器快很多倍。 map 和 reduce 已经存在了很长时间,它们只是 javascript 中的新功能。所以年龄与它无关。认为自己受过教育:-)
            • You might see a few milliseconds of difference when it comes to performance speeds。几毫秒乘以几千或几百万个项目加起来非常快。在我自己的测试中(在决定 .map 与 for 时),我发现使用 map 迭代大量数据将花费近一个数量级的时间。为了一致性起见,我在其他地方都写了 for 循环。这也是有利的,因为来自其他语言的开发人员可以直接进入并知道发生了什么。
            • map 更出色,因为它将数据结构与业务逻辑分开,从而更容易验证和重构其中一个/两个。您使用的任何其他数据结构也应该实现map 函数。这将允许您独立发展数据结构和业务逻辑。
            猜你喜欢
            • 1970-01-01
            • 2016-02-18
            • 2017-09-26
            • 1970-01-01
            • 2020-06-26
            • 1970-01-01
            • 2021-06-06
            • 2018-07-25
            • 2021-09-07
            相关资源
            最近更新 更多