是否有一种机制可以在没有可变变量的情况下在 ES6(ECMAScript 6)中循环 x 次?

IT技术 javascript generator ecmascript-6 ecmascript-harmony
2021-02-07 05:09:37

x在 JavaScript 中循环时间的典型方法是:

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

但我根本不想使用++运算符或有任何可变变量。那么有没有办法在 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 中有更好的机制。

6个回答

使用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
有没有人解决过 Kugel 之前的评论?我想知道同样的事情
2021-03-21 05:09:37
@sebpiq 因为 Array(10) 函数返回一个空数组实例,长度设置为 10。数组实例本质上是在内存中分配的,但为空。如果您尝试对其进行 map() ,它将失败,因为数组为空。但是,当您尝试展开它时,展开运算符将返回与数组长度相同数量的项目。由于数组是空的,这些项目是未定义的(不存在),所以 spread 会给你 10 个元素 === 未定义。因此 (_, i) => {} 语法总是忽略第一个(始终未定义的)参数。
2021-03-29 05:09:37
所以你分配整个N 个元素的数组只是为了把它扔掉?
2021-03-31 05:09:37
如果您不需要迭代器 (i),您可以同时排除键和值来实现: [...Array(10)].forEach(() => console.log('looping 10 times');
2021-04-12 05:09:37
更好的: Array.from(Array(10), (_, i) => i*10)
2021-04-13 05:09:37

行!

下面的代码是使用 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'))


如果您不喜欢学习更多东西,请停止阅读这里...

但是这些东西应该让人感觉有些不对劲……

  • 单个分支if语句很难看——在另一个分支上会发生什么?
  • 函数体中的多个语句/表达式——过程问题是否混合?
  • 隐式返回undefined— 指示不纯的、有副作用的函数

“没有更好的办法吗?”

有。让我们首先回顾一下我们最初的实现

// times :: Int -> (void -> void) -> void
const times = x => f => {
  if (x > 0) {
    f()               // 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)))

实现timesrepeat

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

// 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 逗号运算符,

如果您在查看最后一个示例的工作方式时遇到问题,这取决于您对 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

在这里,我们抽象while出我们的loop函数——它寻找一种特殊的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

@naomik 告别TCO我很伤心。
2021-03-14 05:09:37
@AlfonsoPérez 我很欣赏这句话。我会看看我是否可以在某处工作^_^
2021-03-28 05:09:37
@Timofey 这个答案是 2 年来多次编辑的汇编。我同意这个答案确实需要一些最终编辑,但是您的编辑删除了太多。我很快就会重新审视它,认真考虑您的评论和编辑建议。
2021-03-31 05:09:37
似乎过于复杂(我特别困惑g => g(g)(x))。与一阶函数相比,高阶函数是否有好处,就像在我的解决方案中一样?
2021-04-09 05:09:37
似乎这个答案被接受并且评价很高,因为它必须付出很多努力,但我认为这不是一个好的答案。问题的正确答案是“否”。像您一样列出解决方法会很有帮助,但紧接着您声明有更好的方法。你为什么不把那个答案放在最上面,把最糟糕的那个去掉?为什么要解释逗号运算符?为什么要提到 Clojure?一般来说,为什么一个有 2 个字符的答案的问题有这么多切线?简单的问题不仅仅是用户就一些简洁的编程事实进行演示的平台。
2021-04-13 05:09:37
for (let i of Array(100).keys()) {
    console.log(i)
}
这行得通,太棒了!但是从需要额外工作的意义上来说有点难看,这不是Array密钥的用途。
2021-03-17 05:09:37
你可能是对的,没有比这更简洁的了。
2021-03-23 05:09:37
@在。的确。但我不确定[0..x]在 JS 中是否有比我的答案更简洁的 haskell 同义词
2021-03-24 05:09:37
OK,我明白了为什么这个作品给定之间的差异Array.prototype.keysObject.prototype.keys,但可以肯定的是在第一眼混乱。
2021-03-25 05:09:37
@cchamberlain 与 ES2015 中的 TCO(虽然没有在任何地方实施?)这可能不太受关注,但确实:-)
2021-04-08 05:09:37

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

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

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

我可以作弊并制作自己的发电机。至少i++是看不见的:)

这应该足够了,imo。即使在纯语言中,所有操作(或至少,它们的解释器)都是由使用变异的原语构建的。只要它的范围适当,我就看不出有什么问题。

你应该没问题

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*)。

@Kugel 第二个可能会在堆栈上分配,但
2021-03-14 05:09:37
好点是不确定尾调用优化在这里是否有效@Bergi
2021-03-23 05:09:37
这很简单,而且不会像上面的许多答案那样分配数组
2021-04-04 05:09:37

这是另一个不错的选择:

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

最好,正如@Dave Morse 在评论中指出的那样,您还可以map通过使用Array.from函数的第二个参数来摆脱调用,如下所示:

Array.from({ length: 3 }, () => (...))
这应该是公认的答案!一个小建议 - 您已经通过 Array.from 免费获得了您需要的类似地图的功能: Array.from({ length: label.length }, (_, i) => (...)) 这节省了创建一个空的临时数组来启动对地图的调用。
2021-03-29 05:09:37
2021-04-03 05:09:37