是否有一种机制在ES6(ECMAScript 6)中循环x次而没有可变变量?


156

xJavaScript中循环时间的典型方法是:

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中有更好的机制。


3
为什么可变回路控制变量是个问题?只是一个原则?
多尔特2015年

1
@doldt -我想教的JavaScript,但我有延缓可变变量的概念,直到后来的实验
在。

5
我们确实在这里变得有些题外话,但是您确定在了解可变变量之前,转向ES6生成器(或任何其他新的高级概念)是一个好主意吗?:)
doldt,2015年

5
@doldt-也许,我正在实验。对JavaScript采用功能语言方法。
在。

Answers:


156

好!

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


24
似乎过于复杂(我特别对感到困惑g => g(g)(x))。像我的解决方案一样,高阶函数比一阶函数有好处吗?
帕夫洛

1
@naomik:感谢您抽出宝贵时间发布链接。非常感激。
皮内达

1
@AlfonsoPérez感谢您的评论。我会看看我是否可以在其中某处工作一些提示^ _ ^
谢谢你

1
@naomik告别TCO!我被毁了

10
这个答案似乎已经被接受并且评分很高,因为它必须付出很多努力,但是我认为这不是一个很好的答案。该问题的正确答案是“否”。像您一样列出解决方法会很有帮助,但是此后,您指出有更好的方法。您为什么不把答案放在最前面呢?您为什么要解释逗号运算符?你为什么要提起Clojure?通常,为什么一个带有2个字符的答案的问题有很多切线?简单的问题不仅仅是用户就某些整洁的编程事实进行演示的平台。
Timofey'Sasha'Kondrashov

264

使用ES2015 Spread运算符

[...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

26
更好:Array.from(Array(10), (_, i) => i*10)
Bergi

6
这应该是最好的答案。ES6!太棒了!
GergelyFehérvári17年

3
如果不需要迭代器(i),则可以同时排除键和值:[...Array(10)].forEach(() => console.log('looping 10 times');
Sterling Bourne

9
因此,您分配N个元素的整个数组只是为了扔掉它?
库格尔

2
有谁解决过库格尔的先前评论?我在想同一件事
Arman

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

这行得通,所以太好了!但是从某种意义上说,这有点麻烦,因为这需要额外的工作,而这并不是Array键的用途。
在。

@在。确实。但是我不确定[0..x]JS中是否有haskell的同义词比我的答案更简洁。
zerkms,2015年

您可能是正确的,没有什么比这更简洁了。
在。

OK,我明白了为什么这个作品给定之间的差异Array.prototype.keysObject.prototype.keys,但可以肯定的是在第一眼混乱。
马克·里德

1
@cchamberlain与ES2015中的TCO(虽然未在任何地方实施?)可能不太受关注,但实际上是:-)
zerkms

29

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

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

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

我可以作弊,自己做发电机。至少i++看不见:)

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

你应该很好

function* times(n) {
  for (let i = 0; i < x; 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);

但这对我来说似乎有些矫kill过正,并且可能存在性能问题(因为无法使用消除尾声return yield*)。


1
我喜欢这个选项-漂亮又简单!
DanV

2
这很简单,而且一点都不像上面的许多答案那样分配数组
Kugel

@Kugel第二个可能在堆栈上分配,但是
Bergi '17

好一点,不确定尾声优化是否可以在这里工作@Bergi
Kugel



11

答案: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'

旁注-定义范围功能

一个相似/相关的问题,使用基本上非常相似的代码结构,可能是(核心)JavaScript中有一个便捷的Range函数,类似于下划线的range函数。

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

下划线

_.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函数将以上各项运行了100万次,以前的方法(Array(n).fill())证明速度稍快。


8
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不支持填充方法
morhook 2016年

8

这是另一个不错的选择:

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

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

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


2
这应该是公认的答案!一个小建议-使用Array.from,您已经免费获得所需的类似地图的功能:Array.from({ length: label.length }, (_, i) => (...)) 这样可以省去 创建一个空的临时数组的目的,这只是为了启动对地图的调用。
戴夫·摩尔斯

7

这不是我会教(或在我的代码中使用过的)东西,但是这是一个值得修改代码的解决方案,无需更改变量,不需要ES6:

Array.apply(null, {length: 10}).forEach(function(_, i){
    doStuff(i);
})

实际上,更多的是有趣的概念验证而不是有用的答案。


Coudn't Array.apply(null, {length: 10})只是Array(10)
2015年

1
@Pavlo,实际上,不。Array(10)将创建一个长度为10的数组,但其中未定义任何键,这使得forEach构造在这种情况下不可用。但是,如果您不使用forEach,确实可以简化它,请参阅zerkms的答案(尽管使用ES6!)。
多尔特2015年

创意@doldt,但我正在寻找简单易学的东西。
在。

5

我参加聚会很晚,但是由于这个问题经常出现在搜索结果中,所以我想添加一个我认为在可读性方面最好的解决方案,但又不会太长(这对于任何代码库IMO都是理想的) 。它会发生变化,但我会在KISS原则上进行权衡。

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

3
感谢您在我只能形容为高阶Lambda恋物癖派对中的理性声音。我也经历了无害的Google首次点击后的问答活动,并很快被这里的大多数答案鄙视了我的理智。您是清单中的第一个,我将认为是直接问题的直接解决方案。
Martin Devillers

唯一的问题是,如果您想times在循环中使用变量,这有点违反直觉。也许countdown会是一个更好的命名。否则,页面上最干净最清晰的答案。
托尼·布拉索纳斯

3

Afaik,ES6中没有类似于Ruby times方法的机制。但是您可以通过使用递归来避免突变:

let times = (i, cb, l = i) => {
  if (i === 0) return;

  cb(l - i);
  times(i - 1, cb, l);
}

times(5, i => doStuff(i));

演示:http : //jsbin.com/koyecovano/1/edit?js,console


我喜欢这种方法,我喜欢递归。但是,我希望可以使用更简单的方法来显示新的JavaScript用户循环。
在。

3

如果您愿意使用一个库,也可以使用lodash_.times下划线_.times

_.times(x, i => {
   return doStuff(i)
})

请注意,这将返回结果数组,因此实际上更像是红宝石:

x.times.map { |i|
  doStuff(i)
}

2

在函数范式repeat中,通常是无限递归函数。要使用它,我们需要惰性评估或延续传递样式。

延迟评估函数重复

const repeat = f => x => [x, () => repeat(f) (f(x))];
const take = n => ([x, f]) => n === 0 ? x : take(n - 1) (f());

console.log(
  take(8) (repeat(x => x * 2) (1)) // 256
);

我使用thunk(一个没有参数的函数)来实现Javascript中的延迟评估。

具有连续传递样式的功能重复

const repeat = f => x => [x, k => k(repeat(f) (f(x)))];
const take = n => ([x, k]) => n === 0 ? x : k(take(n - 1));

console.log(
  take(8) (repeat(x => x * 2) (1)) // 256
);

CPS起初有点吓人。但是,它总是遵循相同的模式:最后一个参数是延续(一个函数),它调用自己的主体:k => k(...)。请注意,CPS成为内而外的应用程序,即take(8) (repeat...)成为k(take(8)) (...)其中k的部分应用repeat

结论

通过将重复(repeat)与终止条件(take)分开,我们获得了灵活性-分离关注点直至苦涩的终点:D


1

该解决方案的优势

  • 最简单的阅读/使用(imo)
  • 返回值可以用作总和,也可以忽略
  • 普通的es6版本,还链接到TypeScript版本的代码

缺点 -变异。作为内部人,我不在乎,也许其他人也不会。

示例和代码

times(5, 3)                       // 15    (3+3+3+3+3)

times(5, (i) => Math.pow(2,i) )   // 31    (1+2+4+8+16)

times(5, '<br/>')                 // <br/><br/><br/><br/><br/>

times(3, (i, count) => {          // name[0], name[1], name[2]
    let n = 'name[' + i + ']'
    if (i < count-1)
        n += ', '
    return n
})

function times(count, callbackOrScalar) {
    let type = typeof callbackOrScalar
    let sum
    if (type === 'number') sum = 0
    else if (type === 'string') sum = ''

    for (let j = 0; j < count; j++) {
        if (type === 'function') {
            const callback = callbackOrScalar
            const result = callback(j, count)
            if (typeof result === 'number' || typeof result === 'string')
                sum = sum === undefined ? result : sum + result
        }
        else if (type === 'number' || type === 'string') {
            const scalar = callbackOrScalar
            sum = sum === undefined ? scalar : sum + scalar
        }
    }
    return sum
}

TypeScipt版本
https://codepen.io/whitneyland/pen/aVjaaE?editors=0011


0

解决功能方面:

function times(n, f) {
    var _f = function (f) {
        var i;
        for (i = 0; i < n; i++) {
            f(i);
        }
    };
    return typeof f === 'function' && _f(f) || _f;
}
times(6)(function (v) {
    console.log('in parts: ' + v);
});
times(6, function (v) {
    console.log('complete: ' + v);
});

5
“解决功能方面”,然后使用带有可变变量的命令式循环i。那么什至要使用times普通旧的原因是什么for
zerkms,2015年

重复使用var twice = times(2);
Nina Scholz 2015年

那么,为什么不使用for两次呢?
zerkms,2015年

我不怕用。问题是不要使用水准音。但结果始终是某种形式的缓存aka变量。
Nina Scholz 2015年

1
“是不使用水痘的东西” ---仍然使用它- i++。尚不清楚如何将不可接受的内容包装到函数中使其变得更好。
zerkms,2015年

0

发电机?递归?为什么这么讨厌“变形”?;-)

只要我们“隐藏”它,如果可以接受,那么只需接受使用一元运算符就可以使事情简单

Number.prototype.times = function(f) { let n=0 ; while(this.valueOf() > n) f(n++) }

就像在红宝石中一样:

> (3).times(console.log)
0
1
2

2
竖起大拇指:“为什么讨厌那么多变种人?”
Sarreph

1
为了简单起见,对Monkeypatch表示不喜欢使用ruby风格,所以不赞成。对那些坏坏猴子说不。
mrm

1
@mrm是“猴子修补程序”,这不只是扩展的情况?拥抱和扩展:)
CONNY

否。按照定义,将函数添加到Number(或String或Array或您未编写的任何其他类)中的方法是polyfill或monkey补丁-甚至不建议使用polyfill。阅读“猴子补丁”,“ polyfill”和推荐的替代词“ ponyfill”的定义。那正是你想要的。
mrm

要扩展Number,您可以这样做:类SuperNumber扩展Number {times(fn){for(let i = 0; i <this; i ++){fn(i); }}}
亚历山大

0

我用辅助函数包装了@Tieme的答案。

在TypeScript中:

export const mapN = <T = any[]>(count: number, fn: (...args: any[]) => T): T[] => [...Array(count)].map((_, i) => fn())

现在您可以运行:

const arr: string[] = mapN(3, () => 'something')
// returns ['something', 'something', 'something']
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.