【问题标题】:Iterate an array as a pair (current, next) in JavaScript在 JavaScript 中将数组作为一对(当前,下一个)迭代
【发布时间】:2015-11-05 12:55:26
【问题描述】:

在问题 Iterate a list as pair (current, next) in Python 中,OP 有兴趣将 Python 列表迭代为一系列 current, next 对。我也有同样的问题,但我想以最简洁的方式在 JavaScript 中完成,也许使用lodash

使用简单的for 循环很容易做到这一点,但感觉不是很优雅。

for (var i = 0; i < arr.length - 1; i++) {
  var currentElement = arr[i];
  var nextElement = arr[i + 1];
}

Lodash 几乎可以做到这一点:

_.forEach(_.zip(arr, _.rest(arr)), function(tuple) {
  var currentElement = tuple[0];
  var nextElement = tuple[1];
})

这个微妙的问题是在最后一次迭代中,nextElement 将是undefined

当然,理想的解决方案只是一个 pairwise lodash 函数,它只在必要时循环。

_.pairwise(arr, function(current, next) {
  // do stuff 
});

是否有任何现有的库已经这样做了?还是有另一种我没有尝试过的在 JavaScript 中进行成对迭代的好方法?


澄清:如果arr = [1, 2, 3, 4],那么我的pairwise函数将迭代如下:[1, 2][2, 3][3, 4],而不是[1, 2][3, 4]。这就是 OP 在the original question for Python 中询问的内容。

【问题讨论】:

  • 真的不知道你为什么要花这么多心思在这上面。惯用的 JavaScript 方法就是 array.forEach(function (item, index) { var next = array[index + 1]; ... });
  • 这可能并不重要,但我很好奇以这种方式迭代的总体目标是什么?
  • @sparrow -- 今天它在单元测试中断言一些存根的调用顺序。过去,我在其他语言的几个应用程序中需要成对迭代(例如 Python 中的生物信息学代码),但我从未对现有的 JavaScript 解决方案完全满意。

标签: javascript iteration lodash


【解决方案1】:

只要把“丑”的部分做成一个函数,然后就好看了:

arr = [1, 2, 3, 4];

function pairwise(arr, func){
    for(var i=0; i < arr.length - 1; i++){
        func(arr[i], arr[i + 1])
    }
}

pairwise(arr, function(current, next){
    console.log(current, next)
})

您甚至可以稍微修改它,以便能够迭代所有 i、i+n 对,而不仅仅是下一个:

function pairwise(arr, func, skips){
    skips = skips || 1;
    for(var i=0; i < arr.length - skips; i++){
        func(arr[i], arr[i + skips])
    }
}

pairwise([1, 2, 3, 4, 5, 6, 7], function(current,next){
    console.log(current, next) // displays (1, 3), (2, 4), (3, 5) , (4, 6), (5, 7)
}, 2)

【讨论】:

  • 也许更好的第三个参数是windowLength来调整滑动窗口的大小。这样一来,人们就不能只做pairwise 迭代,而是做一个像[1, 2, 3][2, 3, 4] 这样的滑动组。但我喜欢这里的总体思路。你会考虑在 lodash 上提交 PR 吗?
  • @mattingly890 是的,这可能会更有用,将其更改为 func(arr.slice(i,i+size)) 并接收数组。从未使用过 lodash,但请随意提交 ;)
  • 我可能会这样做:)
【解决方案2】:

在 Ruby 中,这称为each_cons(每个连续):

(1..5).each_cons(2).to_a # => [[1, 2], [2, 3], [3, 4], [4, 5]]

proposed for Lodash,但被拒绝了;但是,npm 上有一个 each-cons 模块:

const eachCons = require('each-cons')

eachCons([1, 2, 3, 4, 5], 2) // [[1, 2], [2, 3], [3, 4], [4, 5]]

Ramda 中还有一个 aperture 函数可以做同样的事情:

const R = require('ramda')

R.aperture(2, [1, 2, 3, 4, 5]) // [[1, 2], [2, 3], [3, 4], [4, 5]]

【讨论】:

    【解决方案3】:

    这个答案的灵感来自我在 Haskell 中看到的类似问题的答案:https://stackoverflow.com/a/4506000/5932012

    我们可以使用 Lodash 的助手来编写以下内容:

    const zipAdjacent = function<T> (ts: T[]): [T, T][] {
      return zip(dropRight(ts, 1), tail(ts));
    };
    zipAdjacent([1,2,3,4]); // => [[1,2], [2,3], [3,4]]
    

    (与 Haskell 等效项不同,我们需要 dropRight,因为 Lodash 的 zip 与 Haskell 的行为不同:它将使用最长数组的长度而不是最短的。)

    在 Ramda 中也是如此:

    const zipAdjacent = function<T> (ts: T[]): [T, T][] {
      return R.zip(ts, R.tail(ts));
    };
    zipAdjacent([1,2,3,4]); // => [[1,2], [2,3], [3,4]]
    

    虽然 Ramda 已经有一个覆盖这个的函数,叫做aperture。这稍微更通用,因为它允许您定义所需的连续元素数量,而不是默认为 2:

    R.aperture(2, [1,2,3,4]); // => [[1,2], [2,3], [3,4]]
    R.aperture(3, [1,2,3,4]); // => [[1,2,3],[2,3,4]]
    

    【讨论】:

      【解决方案4】:

      使用iterablesgenerator functions 的另一种解决方案:

      function * pairwise (iterable) {
          const iterator = iterable[Symbol.iterator]()
          let current = iterator.next()
          let next = iterator.next()
          while (!next.done) {
              yield [current.value, next.value]
              current = next
              next = iterator.next()
          }
      }
      
      console.log(...pairwise([]))
      console.log(...pairwise(['apple']))
      console.log(...pairwise(['apple', 'orange', 'kiwi', 'banana']))
      console.log(...pairwise(new Set(['apple', 'orange', 'kiwi', 'banana'])))

      优点:

      • 适用于所有可迭代对象,而不仅仅是数组(例如 Set)。
      • 不创建任何中间或临时数组。
      • 惰性评估,在非常大的可迭代对象上高效工作。

      打字稿版本:

      function* pairwise<T>(iterable:Iterable<T>) : Generator<Array<T>> {
          const iterator = iterable[Symbol.iterator]();
          let current = iterator.next();
          let next = iterator.next();
          while (!next.done) {
              yield [current.value, next.value];
              current = next;
              next = iterator.next();
          }
      }
      

      【讨论】:

      • 不错的答案!但我会将循环更改为 while (!next.done) 以避免在最后一次迭代中第二个元素是 undefined,正如 OP 要求的那样
      【解决方案5】:

      这是一个没有任何依赖关系的通用功能解决方案:

      const nWise = (n, array) => {
        iterators = Array(n).fill()
          .map(() => array[Symbol.iterator]());
        iterators
          .forEach((it, index) => Array(index).fill()
            .forEach(() => it.next()));
        return Array(array.length - n + 1).fill()
          .map(() => (iterators
            .map(it => it.next().value);
      };
      
      const pairWise = (array) => nWise(2, array);
      

      我知道它看起来一点也不好看,但是通过引入一些通用的实用函数,我们可以让它看起来更好:

      const sizedArray = (n) => Array(n).fill();
      

      我可以将sizedArrayforEach 结合使用来实现times,但这将是一个低效的实现。恕我直言,可以为这种不言自明的功能使用命令式代码:

      const times = (n, cb) => {
        while (0 < n--) {
          cb();
        }
      }
      

      如果您对更核心的解决方案感兴趣,请查看this 答案。

      不幸的是Array.fill 只接受单个值,而不接受回调。所以Array(n).fill(array[Symbol.iterator]()) 会在每个位置放置相同的值。我们可以通过以下方式解决这个问题:

      const fillWithCb = (n, cb) => sizedArray(n).map(cb);
      

      最终实现:

      const nWise = (n, array) => {
        iterators = fillWithCb(n, () => array[Symbol.iterator]());
        iterators.forEach((it, index) => times(index, () => it.next()));
        return fillWithCb(
          array.length - n + 1,
          () => (iterators.map(it => it.next().value),
        );
      };
      

      通过将参数样式更改为柯里化,pairwise 的定义看起来会更好:

      const nWise = n => array => {
        iterators = fillWithCb(n, () => array[Symbol.iterator]());
        iterators.forEach((it, index) => times(index, () => it.next()));
        return fillWithCb(
          array.length - n + 1,
          () => iterators.map(it => it.next().value),
        );
      };
      
      const pairWise = nWise(2);
      

      如果你运行它,你会得到:

      > pairWise([1, 2, 3, 4, 5]);
      // [ [ 1, 2 ], [ 2, 3 ], [ 3, 4 ], [ 4, 5 ] ]
      

      【讨论】:

        【解决方案6】:

        d3.js 提供了 built-in 版本,在某些语言中称为 sliding

        console.log(d3.pairs([1, 2, 3, 4])); // [[1, 2], [2, 3], [3, 4]]
        &lt;script src="http://d3js.org/d3.v5.min.js"&gt;&lt;/script&gt;

        # d3.pairs(array[, reducer]) <>

        对于指定数组中的每一对相邻元素,依次调用指定的reducer函数,传递元素i和元素i - 1。如果未指定reducer,则默认为创建二元素的函数每对的数组。

        【讨论】:

          【解决方案7】:

          我们可以将Array.reduce 包裹一点来执行此操作,并保持一切清洁。 不需要循环索引/循环/外部库。

          如果需要结果,只需创建一个数组来收集它。

          function pairwiseEach(arr, callback) {
            arr.reduce((prev, current) => {
              callback(prev, current)
              return current
            })
          }
          
          function pairwise(arr, callback) {
            const result = []
            arr.reduce((prev, current) => {
              result.push(callback(prev, current))
              return current
            })
            return result
          }
          
          const arr = [1, 2, 3, 4]
          pairwiseEach(arr, (a, b) => console.log(a, b))
          const result = pairwise(arr, (a, b) => [a, b])
          
          const output = document.createElement('pre')
          output.textContent = JSON.stringify(result)
          document.body.appendChild(output)

          【讨论】:

            【解决方案8】:

            这是一个简单的单行:

            [1,2,3,4].reduce((acc, v, i, a) => { if (i < a.length - 1) { acc.push([a[i], a[i+1]]) } return acc; }, []).forEach(pair => console.log(pair[0], pair[1]))
            

            或格式化:

            [1, 2, 3, 4].
            reduce((acc, v, i, a) => {
              if (i < a.length - 1) {
                acc.push([a[i], a[i + 1]]);
              }
              return acc;
            }, []).
            forEach(pair => console.log(pair[0], pair[1]));
            

            哪些日志:

            1 2
            2 3
            3 4
            

            【讨论】:

              【解决方案9】:

              这是我的方法,使用Array.prototype.shift

              Array.prototype.pairwise = function (callback) {
                  const copy = [].concat(this);
                  let next, current;
              
                  while (copy.length) {
                      current = next ? next : copy.shift();
                      next = copy.shift();
                      callback(current, next);
                  }
              };
              

              可以按如下方式调用:

              // output:
              1 2
              2 3
              3 4
              4 5
              5 6
              
              [1, 2, 3, 4, 5, 6].pairwise(function (current, next) {
                  console.log(current, next);
              });
              

              所以分解一下:

              while (this.length) {
              

              Array.prototype.shift 直接对数组进行变异,所以当没有剩余元素时,length 显然会解析为0。这是 JavaScript 中的“假”值,因此循环会中断。

              current = next ? next : this.shift();
              

              如果之前已设置next,则将其用作current 的值。这允许每个项目进行一次迭代,以便所有元素都可以与其相邻的后继元素进行比较。

              剩下的就很简单了。

              【讨论】:

              • 我对修改Array的原型犹豫不决,即使这是一个聪明的解决方案。 google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
              • 在这种情况下,可以将相同的逻辑公开为一个函数,该函数将数组和回调作为参数,例如arrayUtils.pairwise(array, function[...]); 就个人而言,只要非常小心,我并不完全反对修改标准对象的原型。我曾遇到过这种做法在背后咬我的例子,但在这种情况下,我不会认为它有那么大的问题。尽管如此,逻辑是存在的:)
              • 我也不相信 shift 在 JavaScript 中的性能。例如,请参阅stackoverflow.com/questions/6501160/… 的讨论
              • 您要求一种比使用for 更优雅的方法。 forshiftmutates 数组的基础上会更高效。可读性和性能并不总是齐头并进的。
              • 所以你引起了我的兴趣!事实证明(至少在 Chrome 中)for 循环的性能仅略高:jsperf.com/javascript-pairwise-function-for-vs-shift
              【解决方案10】:

              只需使用 forEach 及其所有参数即可:

              yourArray.forEach((current, idx, self) => {
                if (let next = self[idx + 1]) {
                  //your code here
                }
              })
              

              【讨论】:

                【解决方案11】:

                我的两分钱。基本切片,生成器版本。

                function* generate_windows(array, window_size) {
                    const max_base_index = array.length - window_size;
                    for(let base_index = 0; base_index <= max_base_index; ++base_index) {
                        yield array.slice(base_index, base_index + window_size);
                    }
                }
                const windows = generate_windows([1, 2, 3, 4, 5, 6, 7, 8, 9], 3);
                for(const window of windows) {
                    console.log(window);
                }
                

                【讨论】:

                  【解决方案12】:

                  希望它可以帮助某人! (和喜欢)

                  arr = [1, 2, 3, 4];
                  output = [];
                  arr.forEach((val, index) => {
                    if (index < (arr.length - 1) && (index % 2) === 0) {
                      output.push([val, arr[index + 1]])
                    }
                  })
                  
                  console.log(output);

                  【讨论】:

                    【解决方案13】:

                    修改后的zip

                    const pairWise = a => a.slice(1).map((k,i) => [a[i], k]);
                    
                    console.log(pairWise([1,2,3,4,5,6]));

                    输出:

                    [[1,2],[2,3],[3,4],[4,5],[5,6]]

                    通用版本是:

                    const nWise = n => a => a.slice(n).map((_,i) => a.slice(i, n+i));
                    
                    console.log(nWise(3)([1,2,3,4,5,6,7,8]));

                    【讨论】:

                      【解决方案14】:

                      Lodash 确实有一个方法可以让你这样做:https://lodash.com/docs#chunk

                      _.chunk(array, 2).forEach(function(pair) {
                        var first = pair[0];
                        var next = pair[1];
                        console.log(first, next)
                      })
                      

                      【讨论】:

                      • 这根本不是我们要求的。
                      • 抱歉复制粘贴错误。用正确的方法更新。
                      • 这仍然是错误的。给定[1,2,3,4] 的输入,您的代码将输出[1,2], [3, 4],而不是所需的[1,2], [2,3], [3, 4]
                      • 我认为在我写完这个答案之后,原始答案已更新以澄清。我再回答一个。
                      猜你喜欢
                      • 2011-07-23
                      • 2011-07-07
                      • 1970-01-01
                      • 2011-02-17
                      • 2018-08-18
                      • 1970-01-01
                      • 2022-08-17
                      相关资源
                      最近更新 更多