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

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

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

173924 次浏览

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

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

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

更像是一个有趣的概念证明,而不是一个有用的答案,真的。

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

好的!

下面的代码是使用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语句是丑陋的—在另一个分支上发生了什么?
  • 函数体和mdash中的多个语句/表达式;程序问题是否被混淆了?
  • 隐式返回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)))

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

如果你看不清最后一个例子是如何工作的,这取决于你对JavaScript最古老的战斧之一的认识;逗号操作符——简而言之,它从左到右计算表达式,而返回是最后一个计算表达式的值

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

在上面的例子中,我使用

(i => (f(i), i + 1))

这是一种简洁的写法吗

(i => { f(i); return i + 1 })

尾部呼叫优化

尽管递归实现很性感,但在这一点上,我推荐它们是不负责任的,因为我能想到的JavaScript VM都不支持正确的尾部调用消除- babel曾经编译过它,但它已经在“broken;将在一年多的时间里重新实施“状态”。

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

处理功能方面:

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);
});

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

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

这将为每个body求值创建一个新的(可变的)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*)。

答案: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中是否有一个方便的范围函数,类似于下划线的范围函数。

创建一个包含n个数字的数组,从x开始

下划线

_.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())被证明略快一些。

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建议。

使用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));

或者使用从操作符:

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

如果你愿意使用库,还有lodash _.times强调_.times:

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

注意,这将返回一个结果数组,所以它更像这样的ruby:

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

在函数式范式中,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一开始有点吓人。然而,它总是遵循相同的模式:最后一个参数是continuation(一个函数),它调用自己的主体:k => k(...)。请注意CPS将应用程序翻过来,即take(8) (repeat...)变成k(take(8)) (...),其中k是部分应用的repeat

结论

通过将重复(repeat)与终止条件(take)分离,我们获得了灵活性——将关注点分离到痛苦的一端:D

发电机吗?递归?为什么这么讨厌突变?: -)

如果只要我们“隐藏”它,它就可以接受,那么只要接受一元操作符的使用,我们就可以保持简单:

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

就像在ruby中:

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

我认为这很简单:

[...Array(3).keys()]

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

这个代码片段将console.log test 4次。

此解决方案的优点

  • 最容易阅读/使用(我觉得)
  • 返回值可以用作和,也可以忽略
  • 普通es6版本,也链接到打印稿版本的代码

< >强的缺点 ——突变。只是内在的我不在乎,也许其他人也不在乎

示例和代码

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
}
< br > < p > TypeScipt版本 https://codepen.io/whitneyland/pen/aVjaaE?editors=0011 < / p >

我来晚了,但由于这个问题经常出现在搜索结果中,我只想添加一个解决方案,我认为它在可读性方面是最好的,同时不长(这对于任何代码库都是理想的)。它会变异,但我愿意为KISS原则做出妥协。

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

下面是另一个不错的选择:

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

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

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

我用一个helper函数包装了@Tieme的答案。

在打字稿:

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']

我做了这个:

function repeat(func, times) {
for (var i=0; i<times; i++) {
func(i);
}
}

用法:

repeat(function(i) {
console.log("Hello, World! - "+i);
}, 5)


/*
Returns:
Hello, World! - 0
Hello, World! - 1
Hello, World! - 2
Hello, World! - 3
Hello, World! - 4
*/

i变量返回它循环的次数——如果你需要预加载x数量的图像,这很有用。

我把它放在这里。如果你正在寻找一个不使用数组的紧凑函数,并且你对可变性/不可变性没有问题:

var g =x=>{/*your code goes here*/x-1>0?g(x-1):null};
 

这是我能想到的在range内创建列表/数组的最简单的方法

Array.from(Array(max-min+1), (_, index) => index+min)

对我来说,这是许多级别的开发人员最容易理解的答案
const times = (n, callback) => {
while (n) {
callback();
n--;
}
}


times(10, ()=> console.log('hello'))

在我看来,这个问题最正确的答案(这是有争议的)隐藏在萨莎Kondrashov的注释中,也是最简洁的,只使用了两个字符:&;no&;。没有比Ruby的语法更好的for循环替代函数了。我们可能希望有一个,但就是没有。

问题中没有明确说明,但我认为任何“循环N次”问题的解决方案都不应该分配内存,至少不与N成正比。这个标准将排除大多数“原生javascript”的答案。

其他答案显示了Ruby中的实现,这很好,除了这个问题显式地要求本机javascript解决方案。这个问题已经有了一个非常不错的手卷解决方案,可以说是最易读的解决方案之一。