两个循环可以合并成一个吗?

3
我在使用以下函数将特定数字添加到数组中,后续想将该数组分配给一个变量。我使用两个for循环来实现此目的,但我感觉可能有更简洁的方法。我尝试将两个循环合并为一个,但输出结果不同。

有效示例:

function fill () {
    var array = [];
    for (var index = 0; index < arguments.length; index++) {
        for (var number = arguments[index][0]; number <= arguments[index][1]; number++)
            array.push(number);
    }
    return array;
};

/* Use */
var keys = fill([1, 10], [32, 34]);

/* Output */
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 32, 33, 34]

合并示例:

function fill () {
    var array = [];
    for (var index = 0, number = arguments[index][0];
      index < arguments.length && number <= arguments[index][1];
      index++ && number++) {
        array.push(number);
    }
    return array;
};

/* Use */
var keys = fill([1, 10], [32, 34]);

/* Output */
[1, 1]

是否可能将这两个循环合并为一个循环?如果不行,有没有办法用更少的代码编写上述函数?


3
我认为第一个例子已经尽可能地简短了。 - Cerbrus
1
当你说“少代码”时,你的目标是什么?通常对于像这样的小片段,更易读但略长的版本更可取。 - DBS
1
嵌套循环很好。我相信你正按照预期使用它们。 - Glen Despaux Jr
我正在重写一些旧的函数,这些函数是我刚开始学习JavaScript时写的。我只想用尽可能少的代码重写上述函数。 @DBS - Angel Politis
经过大约2年的努力,我终于成功地合并了循环,而不是采用其他方法。 - Angel Politis
6个回答

1
你在第一个例子中的代码很好。没有真正“干净”的方法来移除嵌套循环。
你可以使用forEach对它们进行迭代,但是即使其中一个被伪装成函数调用,你仍然会有嵌套循环:

function fill () {
    var array = [];
    Array.prototype.slice.apply(arguments) // Make an array out of arguments.
        .forEach(function(arg){
            for (var number = arg[0]; number <= arg[1]; number++){
                array.push(number);
            }
        });
    return array;
};

console.log(fill([1, 10], [32, 34]));

您需要使用Array.prototype.slice.applyarguments转换为实际数组。(这很丑陋)

因此,基本上嵌套循环并不一定是“邪恶的”。 您的第一个示例就是最好的。


1
你可能无法避免这两个循环,但这也不一定是一个目标。这些循环本身并没有什么害处 - 只有当你多次迭代相同的数据时,才需要重新考虑你的代码。
考虑完全不同的方法。

const range = (x , y) =>
  x > y
    ? []
    : [ x, ...range (x + 1, y) ]

const concat = (xs, ys) =>
  xs .concat (ys);

const flatMap = (f, xs) =>
  xs .reduce ((acc, x) => concat (acc, f (x)), [])

const apply = f => xs =>
  f (...xs)

const fill = (...ranges) =>
  flatMap (apply (range), ranges);

console.log
  (fill ( [1,10]
        , [32,34]
        , [40,49]
        , [100,100]
        )
  )

所以,是的,@Redu说“JavaScript是一种函数式语言”是正确的,但我认为他/她的回答没有提供一个组合良好的函数式答案。

上面的答案展示了具有个性化关注点的函数可以很容易地阅读、编写和组合,以实现复杂的计算。


我必须承认,乍一看,这段代码似乎更易读,并且可能作为介绍函数式JS的良好示例,但最终人们不会再关注这些,一旦他们获得了一些经验,就会简单地将它们链接起来。然而,对我来说,这个问题的最佳答案是Yury Tarabanko在我的回答中所展示的。支持函数式方法。 - Redu

1

JavaScript是一种函数式语言。为了现代编程目的,函数式方法对于程序员的利益最大。

var fillArray = (...args) => args.reduce((res,arg) => res.concat(Array(...Array(arg[1]-arg[0]+1)).map((e,i) => i + arg[0])),[]),
       filled = fillArray([1, 10], [32, 34]);
console.log(filled);

这里发生了什么...非常简单。我们通过fillArray函数完成工作。 fillArray函数接受不定数量的参数。因此,我们利用ES6剩余运算符...将它们全部收集到名为args的数组中。

var fillArray = (...args)

现在我们将源数组放入args数组中,然后对此数组应用reduce操作,初始值为空数组(res)。我们要做的是,对于每个源(arg)数组,创建一个新数组,然后将其连接到res数组中。好的,我们收到了[1,10]作为源,这意味着我们需要一个长度为arg[1]-arg[0]+1的数组。
Array(...Array(arg[1]-arg[0]+1))

我们还可以这样做,像Array(arg[1]-arg[0]+1).fill()一样。现在我们有了一个用"undefinite"填充所需长度的数组。然后是map。这非常简单,因为我们将其应用于这个未定义的数组。
.map((e,i) => i + arg[0]))

这意味着每个项目将是当前索引 + 偏移量,即 arg[0]。 然后,我们将此数组连接到结果数组并传递给下一个源数组。因此,您可以看到它非常直观和易于维护。

3
更短?没错。可维护性?我认为不太行。 - Cerbrus
@Cerbrus 你说得没错.. 但事实上,上面的代码片段非常容易阅读和理解,只要抛开命令式编码并适应函数式方法。这可能需要一点时间,但一旦习惯了,你就不会再回到旧式的命令式 C 语言编码方式了。 :) - Redu
1
这段代码是一长串括号和点号。如果没有适当的语法高亮器,很难跟踪每个函数调用的结束位置。 - Cerbrus
2
JavaScript是一种函数式语言[citation needed]。当然,它拥有一些人们认为的函数式方面,但考虑到“函数式语言”没有正式的定义... 请参见https://dev59.com/F2865IYBdhLWcg3wIrJg。 - Heretic Monkey
1
你可以通过调用 reduce 来消除 var fillArray = (...args) => [].concat(...args.map(([start, end]) => Array(...Array(end - start + 1)).map((_, i) => i + start))) - Yury Tarabanko
显示剩余6条评论

0
在ES6中,您可以使用rest运算符并基于项目构建新数组。

function fill(...p) {
    return p.reduce((r, a) => r.concat(Array.apply(null, { length: a[1] - a[0] + 1 }).map(() => a[0]++)), []);
};

var keys = fill([1, 10], [32, 34]);
console.log(keys);


嗨,你好 ^_^ 我认为这个答案会从一些功能分解中受益。无论如何,对于正确/创造性的解决方案加1。 - Mulan

0

与另一个答案类似,但更加完整:

const arrays = [[1,10],[32,34],[9,12]]
const range = (a,b) => a >= b ? [] :
    [...Array(b-a).keys()].map(i => i+a)
const result = arrays.reduce( (a,c) =>
     a.concat( range(c[0], c[1]+1) ), [] )

// =>   [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 32, 33, 34, 9, 10, 11, 12 ]

如果您更喜欢传统的范围函数,则可以使用以下代码:
const arrays = [[1,10],[32,34],[9,12]]

function range(a,b) {
    var arr = []
    for (let i = a; i < b; i++)
        arr.push(i)
    return arr
}

const result = arrays.reduce( function(a,c) {
    return a.concat( range(c[0], c[1]+1) )
}, [] )

我认为我会将 +1 移到 range 函数中。或者可能更改 range,使您可以执行 range(c)。无论如何,这比单行代码更易读 :D - Cerbrus
@Cerbrus 典型的 range 函数包括下限,不包括上限,而不是两者都包括。这是一个通用的范围函数。请参见 clojurepython ... 我还会添加一个一元版本的 range 函数以满足自己的需求,但对于这个解决方案,只需要一个二元版本。 - Josh

-1

经过将近2年的时间和一些在这个线程中发布的很棒的答案,我找到了一种将这两个循环合并为一个的方法,但它并不美观!

代码:

function fill () {
    var
      ar = [],
      imax = arguments.length,
      
      /* The functions that calculate the bounds of j. */
      jmin = i => arguments[i][0],
      jmax = i => arguments[i][1] + 1;
    
    for (
      let i = 0, j = jmin(i);
      i < imax && j < jmax(i);
      
      /* If j reaches max increment i and if i is less than max set j to min. */
      ar.push(j++), (j == jmax(i)) && (i++, (i < imax) && (j = jmin(i)))
    );
    return ar;
};

/* Use */
var keys = fill([1, 10], [32, 34], [76, 79]);
console.log.apply(console, keys);


当然,如果您想以不同的方式回答您的问题,您可以自由更改接受的答案,但是您必须删除您的赞同吗?如果您在2年后回顾此代码,您能够一眼看出它的功能吗?这似乎并不是非常易读的。 - Cerbrus
抱歉,那不是我。我没有给任何答案点赞;可能是因为没有一个直接回答了问题,我猜测是这样的。很抱歉我不得不取消接受你的答案,但由于我找到了一个直接回答问题的解决方案,即使有点晚,我认为将其添加到未来读者中是一个好主意。关于代码的可读性,我同意,但我认为“臭部分”上面的注释已经充分解释了它。此外,由于现在只有一个循环,对于大量迭代,这个答案将比所有给出的答案快得多,所以这是一个优势。 - Angel Politis
Angel,我认为你高估了速度增益...它不会非常显著。 - Cerbrus
对于38,723,599次迭代,我的初始代码使用嵌套循环需要7次尝试后才能在大约15秒内完成,而这个答案只需要大约6秒就可以完成相同数量的尝试@Cerbrus。对于较少的迭代和仅有几个参数,这并不是那么多,但这肯定是一种改进。您的答案也表现良好,只需要大约7.5秒,所以我会为您失去的一个点赞。 - Angel Politis
看起来是这样,但问题的要求仍然是找到一种将循环合并成一个的方法。 - Angel Politis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接