Существует ли механизм зацикливания х раз в ES6 (ECMAScript 6) без изменяемых переменных?


157

Типичный способ зацикливания 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 есть лучший механизм.


3
Почему проблема с переменной переменной цикла? Просто принцип?
Дольдт

1
@doldt - я пытаюсь научить JavaScript, но я экспериментирую с задержкой концепции изменчивых переменных до позднего времени
в.

5
Здесь мы становимся действительно не по теме, но уверены ли вы, что переход к генераторам ES6 (или любой другой новой концепции высокого уровня) является хорошей идеей, прежде чем они узнают об изменчивых переменных? :)
Дольдт

5
@doldt - может быть, я экспериментирую. Принятие функционально-языкового подхода к JavaScript.
в.

Используйте let, чтобы объявить эту переменную в цикле. Его сфера заканчивается петлей.
ncmathsadist

Ответы:


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

Реализация 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


24
Кажется слишком сложным (я особенно запутался с g => g(g)(x)). Есть ли польза от функции более высокого порядка по сравнению с функцией первого порядка, как в моем решении?
Павло

1
@naomik: спасибо, что нашли время опубликовать ссылку. очень признателен.
Пинеда

1
@ AlfonsoPérez Я ценю это замечание. Я посмотрю, смогу ли я где-нибудь проработать небольшую подсказку ^ _ ^
Спасибо,

1
@naomik Прощай, ТШО ! Я опустошен.

10
Кажется, этот ответ принят и хорошо оценен, потому что он, должно быть, потребовал много усилий, но я не думаю, что это хороший ответ. Правильный ответ на вопрос «нет». Полезно перечислить обходной путь, как вы это сделали, но сразу после этого вы заявляете, что есть лучший способ. Почему бы вам просто не поставить этот ответ и убрать худший сверху? Почему вы объясняете запятые операторы? Почему вы воспитываете Clojure? Почему вообще так много касательных к вопросу с двухсимвольным ответом? Простые вопросы - это не просто платформа для пользователей, чтобы сделать презентацию о некоторых изящных фактах программирования.
Саша Кондрашов

267

Использование оператора 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)
Берги

6
Это должен быть лучший ответ. Итак, ES6! Очень круто!
Гергели Фехервари

3
Если вам не нужен итератор (i), вы можете исключить ключ и значение, чтобы сделать это:[...Array(10)].forEach(() => console.log('looping 10 times');
Стерлинг Борн

9
Таким образом, вы выделяете весь массив из N элементов, чтобы просто выбросить его?
Кугель

2
Кто-нибудь рассматривал предыдущий комментарий Кугеля? Мне было интересно то же самое
Арман

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

Это работает, так что это здорово! Но это немного уродливо в том смысле, что требуется дополнительная работа, и это не то Array, для чего используются ключи.
в.

@в. на самом деле. Но я не уверен, что [0..x]в JS синоним haskell более лаконичен, чем в моем ответе.
zerkms

Вы можете быть правы, что нет ничего более краткого, чем это.
в.

Хорошо, я понимаю, почему это работает с учетом различий между Array.prototype.keysи Object.prototype.keys, но это, на первый взгляд, сбивает с толку.
Марк Рид

1
@cchamberlain с TCO в ES2015 (хоть и не реализован где-нибудь?), это может быть менее
опасно

29

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


1
Мне нравится этот вариант - красиво и просто!
DanV

2
Это просто и точно, и не выделяет массив, как многие ответы выше
Кугель

@Kugel Второй может размещаться в стеке
Bergi

Хороший вопрос не уверен, будет ли здесь работать оптимизация хвостового вызова @Bergi
Kugel

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

Этот фрагмент будет console.log test4 раза.


Какая поддержка для заполнения?
Амир Африди

2
@AamirAfridi Вы можете проверить раздел совместимости браузера, также есть полифилл: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Хоссам Мурад


11

Ответ: 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 ()) оказался немного быстрее.


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

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

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


Не Array.apply(null, {length: 10})может быть просто Array(10)?
Павло

1
@ Павло, вообще-то нет. Array (10) создаст массив длиной 10, но без определенных ключей, что делает конструкцию forEach непригодной для использования в этом случае. Но на самом деле это можно упростить, если вы не используете forEach, см. Ответ zerkms (который использует ES6, хотя!).
Дольдт

Creative @doldt, но я ищу что-то простое и понятное.
в.

5

Я опаздываю на вечеринку, но так как этот вопрос часто появляется в результатах поиска, я просто хотел бы добавить решение, которое я считаю лучшим с точки зрения читабельности, но не длинное (которое идеально подходит для любой кодовой базы IMO) , Он мутирует, но я бы сделал компромисс для принципов KISS.

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

3
Спасибо за то, что вы были голосом разума в том, что я могу описать только как лямбда-фетиш-вечеринку высшего порядка. Я тоже попал в этот раздел вопросов и ответов по безобидному первому пути в Google и быстро осквернил мое здравомыслие большинством ответов здесь. Ваш первый в списке, который я бы посчитал простым решением прямой проблемы.
Мартин Девиллерс

Единственная проблема в том, что это немного нелогично, если вы хотите использовать timesпеременную внутри цикла. Возможно, countdownбудет лучше назвать. В противном случае самый чистый и понятный ответ на странице.
Тони Брасунас

3

Афаик, в ES6 нет механизма, подобного 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

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

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

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

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.

Вывод

Отделяя repetition ( 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

повторное использование, как var twice = times(2);.
Нина Шольц

Так почему бы просто не использовать forдважды?
zerkms

я не боюсь использовать для. вопрос был в том, чтобы не использовать вариабель. но результатом всегда является кэширование переменной.
Нина Шольц

1
"было что-то не использовать variabele" --- и вы все еще используете это - i++. Не очевидно, как обертывание чего-либо недопустимого в функции делает его лучше.
zerkms

0

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

Если это приемлемо, пока мы его «скрываем», тогда просто примите использование унарного оператора, и мы можем упростить ситуацию :

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

Прямо как в рубине

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

2
Недурно: "Почему так много хулиганит?"
Сарреф

1
Большие пальцы за простоту, большие пальцы за переход в рубиновый стиль слишком много с monkeypatch. Просто скажи нет тем плохим плохим обезьянам.
mrm

1
@mrm - это «исправление обезьяны», разве это не просто расширение?
Обнимаю

Нет. Добавление функций в Number (или String, Array или любой другой класс, который вы не создавали) по определению являются либо полифиллами, либо обезьяньими патчами - и даже полифилы не рекомендуются. Прочитайте определения «monkey patch», «polyfill» и рекомендуемую альтернативу «ponyfill». Это то, что ты хочешь.
17

Чтобы продлить Number, вы должны сделать: класс SuperNumber расширяет Number {times (fn) {for (пусть 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']

0

Я сделал это:

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.