ES6(ECMAScript 6)中是否有一种不使用可变变量来循环x次的机制?

256

在 JavaScript 中循环 x 次的典型方式是:

for (var i = 0; i < x; i++)
  doStuff(i);

但是我不想使用 ++ 操作符或任何可变变量。那么在 ES6 中,有没有其他的方法可以循环 x 次?我喜欢 Ruby 的机制:

但是我不想使用 ++ 操作符或任何可变变量。那么在 ES6 中,有没有其他的方法可以循环 x 次?我喜欢 Ruby 的机制:

x.times do |i|
  do_stuff(i)
end

在JavaScript / ES6中有类似的东西吗? 我可以通过一些技巧来创建自己的生成器:

function* times(x) {
  for (var i = 0; i < x; i++)
    yield i;
}

for (var i of times(5)) {
  console.log(i);
}
当然我还在使用i++,至少它已经不显眼了,但我希望在ES6中有一个更好的机制。

4
可变的循环控制变量为什么是个问题?只是一个原则吗? - doldt
3
@doldt - 我正在试图教授JavaScript,但我正试验将可变变量的概念延迟到更晚的阶段。 - at.
6
我们离题有些远了,但是在学习可变变量之前,您确定转向ES6生成器(或任何其他新的、高级的概念)是一个好主意吗? :) - doldt
7
@doldt - 或许吧,我正在尝试。采用函数式编程的方法处理 JavaScript。 - at.
1
使用let关键字在循环中声明变量,其作用域仅限于循环内部。 - ncmathsadist
24个回答

411

使用ES2015扩展运算符

[...Array(n)].map()

const res = [...Array(10)].map((_, i) => {
  return i * 10;
});

// as a one liner
const res = [...Array(10)].map((_, i) => i * 10);

或者如果你不需要结果:

[...Array(10)].forEach((_, i) => {
  console.log(i);
});

// as a one liner
[...Array(10)].forEach((_, i) => console.log(i));

或者使用ES2015 Array.from 操作符:

Array.from(...)

const res = Array.from(Array(10)).map((_, i) => {
  return i * 10;
});

// as a one liner
const res = Array.from(Array(10)).map((_, i) => i * 10);

请注意,如果您只需要重复一个字符串,可以使用String.prototype.repeat
console.log("0".repeat(10))
// 0000000000

32
更好的写法:Array.from(Array(10), (_, i) => i*10),意为创建一个包含从 0 到 90 的数(步长为 10)的数组。 - Bergi
5
如果您不需要迭代器(i),您可以排除键和值,以此来实现:[...Array(10)].forEach(() => console.log('循环10次')); - Sterling Bourne
18
所以你分配了一个包含N个元素的整个数组,只是为了将其丢弃? - Kugel
4
有人回复了Kugel之前的评论吗?我也在想同样的事情。 - Arman
4
因为Array(10)函数返回一个长度为10的空数组实例。该数组实例本质上是在内存中分配的,但为空。如果你试图对它进行map()操作,它将失败,因为数组是空的。然而,当你尝试展开它时,展开运算符将返回与数组长度相同的项目数。由于数组为空,这些项目是未定义的(不存在),所以展开将给你10个元素 === undefined。因此,使用(_, i) => {}语法来始终忽略第一个(始终为undefined)参数。 - Xunnamius
显示剩余5条评论

188

好的!

下面的代码使用ES6语法编写,但同样可以使用ES5甚至更少的语法编写。使用ES6不是创建“循环x次机制”的要求。


如果您在回调函数中不需要迭代器,这是最简单的实现方式。

const times = x => f => {
  if (x > 0) {
    f()
    times (x - 1) (f)
  }
}

// use it
times (3) (() => console.log('hi'))

// or define intermediate functions for reuse
let twice = times (2)

// twice the power !
twice (() => console.log('double vision'))

如果您确实需要迭代器,可以使用带有计数器参数的命名内部函数来为您进行迭代。

const times = n => f => {
  let iter = i => {
    if (i === n) return
    f (i)
    iter (i + 1)
  }
  return iter (0)
}

times (3) (i => console.log(i, 'hi'))


Stop reading here if you don't like learning more things ...
但是这些东西应该感觉有点不对劲...
- 单个分支的if语句很丑陋--在另一个分支上会发生什么? - 函数体中多个语句/表达式 -- 过程关注点被混合了吗? - 隐含返回undefined -- 表示不纯、具有副作用的函数
"难道没有更好的方法吗?"
是的,让我们首先重新审视我们最初的实现。
// times :: Int -> (void -> void) -> void
const times = x => f => {
  if (x > 0) {
    <b>f()</b>               // has to be side-effecting function
    times (x - 1) (f)
  }
}

当然,这很简单,但请注意我们只是调用了f()并没有对其做任何操作。这真的限制了我们可以多次重复的函数类型。即使我们有迭代器可用,f(i)也没有更多的灵活性。
如果我们从更好的函数重复过程开始呢?也许是一些更好地利用输入和输出的东西。
通用函数重复

// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
  if (n > 0)
    return repeat (n - 1) (f) (f (x))
  else
    return x
}

// power :: Int -> Int -> Int
const power = base => exp => {
  // repeat <exp> times, <base> * <x>, starting with 1
  return repeat (exp) (x => base * x) (1)
}

console.log(power (2) (8))
// => 256

上面我们定义了一个通用的repeat函数,它需要一个额外的输入来开始重复应用单个函数。
// repeat 3 times, the function f, starting with x ...
var result = repeat (3) (f) (x)

// is the same as ...
var result = f(f(f(x)))

使用repeat实现times

现在很容易了,几乎所有的工作都已经完成。

// repeat :: forall a. Int -> (a -> a) -> a -> a
const repeat = n => f => x => {
  if (n > 0)
    return repeat (n - 1) (f) (f (x))
  else
    return x
}

// times :: Int -> (Int -> Int) -> Int 
const times = n=> f=>
  repeat (n) (i => (f(i), i + 1)) (0)

// use it
times (3) (i => console.log(i, 'hi'))

由于我们的函数以i作为输入并返回i + 1,因此这有效地作为我们每次传递给f的迭代器。
我们已经解决了问题列表:
  • 不再有丑陋的单分支if语句
  • 单表达式主体表示关注点得到了很好的分离
  • 不再有无用的、隐式返回的undefined

JavaScript逗号运算符, the

如果您对最后一个示例的工作方式感到困惑,那是因为它依赖于您对JavaScript最古老的争议之一的认识; 逗号运算符 - 简而言之,它从左到右评估表达式并返回最后一个评估的表达式的值。

(expr1 :: a, expr2 :: b, expr3 :: c) :: c

在我们上面的例子中,我正在使用
(i => (f(i), i + 1))

这只是一种简洁的写法。
(i => { f(i); return i + 1 })

尾调用优化

虽然递归实现看起来很吸引人,但是在当前情况下,我不建议使用它们,因为我想不出任何JavaScript VM支持正确的尾调用消除 - babel曾经用于转译它,但已经处于“损坏;将重新实现”状态超过一年了。

repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded

作为如此,我们应该重新审视我们的repeat实现,以使其具有堆栈安全性。
下面的代码确实使用了可变变量nx,但请注意,所有变异都局限于repeat函数 - 没有状态更改(变异)从函数外部可见。

// repeat :: Int -> (a -> a) -> (a -> a)
const repeat = n => f => x =>
  {
    let m = 0, acc = x
    while (m < n)
      (m = m + 1, acc = f (acc))
    return acc
  }

// inc :: Int -> Int
const inc = x =>
  x + 1

console.log (repeat (1e8) (inc) (0))
// 100000000

我知道这会让很多人说:“但那不是功能性的!” - 放心,我们可以使用纯表达式实现Clojure风格的loop/recur接口来进行常量空间循环; 不用那些while东西。
在这里,我们使用loop函数抽象出while - 它寻找特殊的recur类型来保持循环运行。当遇到非recur类型时,循环结束并返回计算结果。

const recur = (...args) =>
  ({ type: recur, args })
  
const loop = f =>
  {
    let acc = f ()
    while (acc.type === recur)
      acc = f (...acc.args)
    return acc
  }

const repeat = $n => f => x =>
  loop ((n = $n, acc = x) =>
    n === 0
      ? acc
      : recur (n - 1, f (acc)))
      
const inc = x =>
  x + 1

const fibonacci = $n =>
  loop ((n = $n, a = 0, b = 1) =>
    n === 0
      ? a
      : recur (n - 1, b, a + b))
      
console.log (repeat (1e7) (inc) (0)) // 10000000
console.log (fibonacci (100))        // 354224848179262000000


39
似乎过于复杂了(特别是g=>g(g)(x)这部分我感到困惑)。使用高阶函数与一阶函数相比,是否有优势?就像在我提供的解决方案中一样。 - Pavlo
1
@AlfonsoPérez 我很感激你的评论。我会看看是否能在其中添加一点提示 ^_^ - Mulan
1
@naomik 再见 TCO! 我感到心痛。 - user6445533
36
这个答案似乎被认为是受欢迎的和被接受的,因为它必须花费了很多精力,但我并不认为这是一个好的答案。回答这个问题的正确答案是“不”。像你所做的那样列出解决方法是有帮助的,但你在那之后说有更好的方法。为什么你不直接给出那个答案,删除置顶的不好的回答呢?为什么要解释逗号运算符?为什么要提到Clojure?为什么对于一个只需要2个字符的问题,要涉及这么多的次要问题?简单的问题不应该成为用户展示一些有趣编程知识的平台。 - Sasha Kondrashov
3
@Timofey 这个答案是在两年的时间里进行了几次编辑的汇编。我同意这个答案确实需要一些最终编辑,但你的编辑删除了太多内容。我会很快重新审视它,并真诚地考虑你的评论和编辑建议。 - Mulan
显示剩余4条评论

49

这里有另一个不错的选择:

Array.from({ length: 3}).map(...);

如评论中的 @Dave Morse 所指出,更好的做法是使用 Array.from 函数的第二个参数来替换 map 的调用,实现如下:

Array.from({ length: 3 }, () => (...))

2
Array.from 在 MDN 上的文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/from - Purplejacket
9
这应该是被采纳的答案!一个小建议 - 您已经通过 Array.from 获得了所需的类似于地图的功能,这可以免费获得:Array.from({ length: label.length }, (_, i) => (...))这样可以避免创建空临时数组,然后再启动对 map 的调用。 - Dave Morse

47
for (let i of Array(100).keys()) {
    console.log(i)
}

1
这个可以工作,所以很好!但是从某种意义上来说有点丑陋,因为需要额外的工作,而这不是Array键的用途。 - at.
@at. 确实。但是我不确定在JS中是否有比我的答案更简洁的Haskell的[0..x]的同义词。 - zerkms
你可能是正确的,没有比这更简洁的了。 - at.
我认为这比最佳答案更快,而且不容易出现最大递归溢出错误。 - cchamberlain
1
@cchamberlain 在 ES2015 中使用 TCO(尽管还没有在任何地方实现?)可能是较少关注的问题,但确实如此 :-) - zerkms
显示剩余7条评论

39

我认为最好的解决方案是使用let

for (let i=0; i<100; i++) …

那将会为每个循环体的评估创建一个新的(可变的)i变量,并确保i仅从循环语法中的增量表达式中更改,而不是从其他任何地方更改。

我可以有点作弊,自己制作生成器。至少i++看不见了 :)

在我看来,这就足够了。即使是纯粹的语言,所有操作(或者至少它们的解释器)都是由使用突变的原始基元构建的。只要正确作用域,我看不出有什么问题。

你应该没问题了

function* times(n) {
  for (let i = 0; i < n; i++)
    yield i;
}
for (const i of times(5)) {
  console.log(i);
}

但我不想使用++操作符或者任何可变变量。

那么你唯一的选择就是使用递归。你也可以定义一个没有可变i的生成器函数:

function* range(i, n) {
  if (i >= n) return;
  yield i;
  return yield* range(i+1, n);
}
times = (n) => range(0, n);

但我认为这似乎有点过度,而且可能会出现性能问题(因为对于return yield*,尾调用消除不可用)。


3
这很简单明了,不像其他回答一样需要分配一个数组。 - Kugel
@Kugel 第二个可能会在堆栈上分配。 - Bergi
好的观点,不确定尾调用优化是否适用于这里 @Bergi - Kugel

17
const times = 4;
new Array(times).fill().map(() => console.log('test'));

这段代码将会使用 console.log 方法输出 test 这个字符串 4 次。


填充(fill)的支持情况如何? - Aamir Afridi
2
@AamirAfridi 您可以检查浏览器兼容性部分,还提供了一个 polyfill: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/fill - Hossam Mourad

17

我认为这很简单:

[...Array(3).keys()]
或者
Array(3).fill()

14

答案:2015年12月9日

个人认为,这个被接受的答案既简洁(好)又简练(不好)。明白这种说法可能是主观的,所以请阅读这个答案并看看您是否同意或不同意。

问题中给出了类似于 Ruby 的示例:

x.times do |i|
  do_stuff(i)
end

使用以下JS表达式可以实现此功能:

times(x)(doStuff(i));

这是代码:

let times = (n) => {
  return (f) => {
    Array(n).fill().map((_, i) => f(i));
  };
};

就是这样了!

一个简单的使用示例:

let cheer = () => console.log('Hip hip hooray!');

times(3)(cheer);

//Hip hip hooray!
//Hip hip hooray!
//Hip hip hooray!

或者,按照被接受答案的例子:

let doStuff = (i) => console.log(i, ' hi'),
  once = times(1),
  twice = times(2),
  thrice = times(3);

once(doStuff);
//0 ' hi'

twice(doStuff);
//0 ' hi'
//1 ' hi'

thrice(doStuff);
//0 ' hi'
//1 ' hi'
//2 ' hi'

旁注 - 定义范围函数

一个类似/相关的问题,使用基本上非常相似的代码结构,可能是是否有一个方便的范围函数在(core) JavaScript中,类似于underscore的range函数。

创建从x开始的n个数字的数组

Underscore

_.range(x, x + n)

ES2015

有几种选择:

Array(n).fill().map((_, i) => x + i)

Array.from(Array(n), (_, i) => x + i)

使用 n = 10,x = 1 的演示:

> Array(10).fill().map((_, i) => i + 1)
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

> Array.from(Array(10), (_, i) => i + 1)
// [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]

我进行了一个快速的测试,使用我们的解决方案和doStuff函数分别运行了上述每个操作一百万次,前一种方法(Array(n).fill())略微更快。


14

我晚到了,但由于这个问题经常出现在搜索结果中,我想补充一个解决方案,我认为这是可读性最好的解决方案,同时不会太长(在我看来,这对任何代码库都是理想的)。它会进行变异,但我会为KISS原则做出这种权衡。

let times = 5
while( times-- )
    console.log(times)
// logs 4, 3, 2, 1, 0

6
感谢您在这场高阶 lambda 偏爱派对中保持理性的声音。我也是通过 Google 搜索无意间进入了这个问答环节,但迅速被大多数答案所亵渎,而您的答案是列表中我认为直接解决问题的第一个答案。请允许我向您致敬。 - Martin Devillers
唯一的问题是,如果你想在循环内部使用 times 变量,这有点违反直觉。也许 countdown 是更好的命名。否则,这是页面上最干净、最清晰的答案。 - Tony Brasunas
只有在不需要索引值的情况下,这才是可接受的答案;否则,在某些情况下,向下循环可能是不可接受的。 - jsbisht

9
Array(100).fill().map((_,i)=> console.log(i) );

这个版本满足了OP对不可变性的要求。根据您的用例,也可以考虑使用reduce代替map

如果您不介意在原型中稍微变异,那么这也是一个选项。

Number.prototype.times = function(f) {
   return Array(this.valueOf()).fill().map((_,i)=>f(i));
};

现在我们可以做这个

((3).times(i=>console.log(i)));

+1给arcseldon,感谢.fill建议。


由于IE、Opera或PhantomJS不支持fill method,因此被投票否决。 - morhook

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