Существует ли механизм для повторения циклов x раз в ES6 (ECMAScript 6) без изменяемых переменных?
Типичный способ зацикливания x
раз в JavaScript это:
for (var i = 0; i < x; i++)
doStuff(i);
Но я не хочу использовать ++
оператор или иметь какие-либо непостоянные переменные вообще. Так есть ли в ES6 способ зацикливаться x
раз по-другому? Я люблю механизм Руби:
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 есть лучший механизм.
24 ответа
ХОРОШО!
Приведенный ниже код написан с использованием синтаксиса 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)))
Внедрение times
с repeat
Ну, теперь это легко; почти вся работа уже выполнена.
// 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, о которой я могу думать, не поддерживает правильное устранение хвостовых вызовов - babel использовал ее для переноса, но он был "сломан"; статус более года.
repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
Таким образом, мы должны вернуться к нашей реализации repeat
чтобы сделать его безопасным для стека.
Код ниже использует изменяемые переменные n
а также x
но обратите внимание, что все мутации локализованы на 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
Использование 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));
Обратите внимание, что если вам просто нужна повторяющаяся строка, вы можете использовать String.prototype.repeat.
console.log("0".repeat(10))
// 0000000000
Вот еще одна хорошая альтернатива:
Array.from({ length: 3}).map(...);
Желательно, как отметил в комментариях @Dave Morse, вы также можете избавиться от map
вызов, используя второй параметр Array.from
работают так:
Array.from({ length: 3 }, () => (...))
Я думаю, что лучшим решением является использование let
:
for (let i=0; i<100; i++) …
Это создаст новый (изменяемый) i
переменная для каждой оценки тела и гарантирует, что i
изменяется только в выражении приращения в этом синтаксисе цикла, а не откуда-либо еще.
Я мог бы обмануть и сделать свой собственный генератор. По крайней мере
i++
вне поля зрения:)
Это должно быть достаточно IMO. Даже на чистых языках все операции (или, по крайней мере, их интерпретаторы) построены из примитивов, которые используют мутации. Пока это правильно определено, я не вижу, что с этим не так.
Вы должны быть в порядке с
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);
Но это кажется излишним для меня и может иметь проблемы с производительностью (так как устранение хвостового вызова не доступно для return yield*
).
const times = 4;
new Array(times).fill().map(() => console.log('test'));
Этот фрагмент будет console.log
test
4 раза.
Я думаю, что это довольно просто:
[...Array(3).keys()]
или же
Array(3).fill()
Ответ: 09 декабря 2015
Лично я нашел принятый ответ как кратким (хорошо), так и кратко (плохо). Оценить это утверждение может быть субъективным, поэтому, пожалуйста, прочитайте этот ответ и посмотрите, согласны вы или не согласны
Пример, приведенный в вопросе, был похож на пример Руби:
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'
Примечание: определение функции диапазона
Подобный / связанный вопрос, который использует принципиально очень похожие конструкции кода, может заключаться в том, есть ли удобная функция Range в (основном) 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()) оказался немного быстрее.
Я опаздываю на вечеринку, но поскольку этот вопрос часто появляется в результатах поиска, я просто хотел бы добавить решение, которое я считаю лучшим с точки зрения удобочитаемости, но не длинным (что идеально подходит для любой кодовой базы IMO). Он видоизменяется, но я бы пошел на компромисс с принципами KISS.
let times = 5
while( times-- )
console.log(times)
// logs 4, 3, 2, 1, 0
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
предложение.
Не то, чему я бы научил (или когда-либо использовал в своем коде), но вот решение, достойное кода, без изменения переменной, нет необходимости в ES6:
Array.apply(null, {length: 10}).forEach(function(_, i){
doStuff(i);
})
На самом деле это скорее интересная проверка концепции, чем полезный ответ.
Если вы хотите использовать библиотеку, есть также lodash_.times
или подчеркнуть_.times
:
_.times(x, i => {
return doStuff(i)
})
Обратите внимание, что это возвращает массив результатов, так что это действительно больше похоже на этот ruby:
x.times.map { |i|
doStuff(i)
}
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));
В функциональной парадигме 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
Преимущества этого решения
- Простейший для чтения / использования (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
Самый простой способ, который я могу придумать для создания списка / массива в пределах диапазона
Array.from(Array(max-min+1), (_, index) => index+min)
Я просто помещу это сюда. Если вы ищете компактную функцию без использования массивов и у вас нет проблем с изменчивостью / неизменяемостью:
var g =x=>{/*your code goes here*/x-1>0?g(x-1):null};
Мне кажется, самый правильный (спорный) ответ на этот вопрос зарыт в комментарии user3648957 и при этом самый лаконичный, всего в двух знаках: "нет". Нет такой функциональной альтернативы циклу for, как синтаксис Ruby. Мы могли бы хотеть, чтобы он был, но его просто нет.
Это прямо не указано в вопросе, но я бы сказал, что любое решение проблемы «зацикливания N раз» не должно выделять память, по крайней мере, не пропорциональную N. Этот критерий исключит большинство ответов, которые «родны для JavaScript'.
В других ответах показаны реализации, подобные реализации в Ruby, и это нормально, за исключением того, что вопрос явно запрашивает собственное решение javascript. И в этом вопросе уже есть очень приличное ручное решение, возможно, одно из самых читаемых из всех.
Решение функционального аспекта:
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);
});
Я обернул ответ @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']
Я это сделал:
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 количество изображений.
Генераторы? Рекурсия? Почему так много ненавидят? ;-)
Если это приемлемо, пока мы его "скрываем", тогда просто примите использование унарного оператора, и мы можем упростить ситуацию:
Number.prototype.times = function(f) { let n=0 ; while(this.valueOf() > n) f(n++) }
Прямо как в рубине
> (3).times(console.log)
0
1
2