Есть ли механизм для цикла 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.

15 93

15 ответов:

ОК!

приведенный ниже код написан с использованием синтаксиса ES6, но может быть так же легко написан в ES5 или даже меньше. ЕС6 составляет не требование создать "механизм для цикла 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, 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 чтобы сделать его безопасным для стека.

код тут использовать изменяемые переменные 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 operator:

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

обратите внимание, что если вам просто нужно повторить строку, вы можете использовать строку.прототип.повторите.

console.log("0".repeat(10))
// 0000000000
for (let i of Array(100).keys()) {
    console.log(i)
}

Я думаю, что лучшее решение-использовать 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);

но это кажется излишним для меня и может иметь проблемы с производительностью (поскольку устранение хвостового вызова недоступно для return yield*).

Ответ: 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'

Примечание - определение функции

аналогичный / связанный вопрос, который использует принципиально очень похожие конструкции кода, может быть, есть удобная функция диапазона в (core) 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).заполнение ()) доказано чуть быстрее.

Я думаю, что это довольно просто:

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

или

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

этот фрагмент будет console.logtest 4 раза.

Не то, что я бы учил (или когда-либо использовал в своем коде), но вот достойное codegolf решение без мутации переменной, нет необходимости в ES6:

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

на самом деле это скорее интересное доказательство концепции, чем полезный ответ.

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 к аркселдону за .fill предложение.

насколько мне известно, не существует никакого механизма в ЕС6, похожими на Руби 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, консоль

в функциональной парадигме 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
);

хп-это сначала было немного страшно. Однако он всегда следует одной и той же схеме: последний аргумент является продолжением (функцией), которая вызывает свое собственное тело: k => k(...). Обратите внимание, что Роспотребнадзор выворачивает приложение наизнанку, т. е. take(8) (repeat...) становится k(take(8)) (...) здесь k частично применяется repeat.

вывод

разделение повторения (repeat) из условия расторжения договора (take) мы приобретаем гибкость-разъединение забот до своего горького конца : D

если вы готовы использовать библиотеку, там же лодашь _.times или подчеркивание _.times:

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

обратите внимание, что это возвращает массив результатов, так что это действительно больше похоже на этот ruby:

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

обращаясь к функциональному аспекту:

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

генераторы? Рекурсия? почему так много ненависти на мутации? ; -)

Если это приемлемо, пока мы "скрываем" его, то просто примите использование унарного оператора, и мы можем сохранить вещи простыми:

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

Так же, как в ruby:

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

преимущества этого решения

  • простой для чтения / использования (ИМО)
  • возвращаемое значение может быть использовано как сумма, или просто проигнорировано
  • простая версия 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