Чи існує механізм циклічності x разів у 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
Чому проблема змінної керування змінною петлею є проблемою? Просто принцип?
doldt

1
@doldt - Я намагаюся навчити JavaScript, але експериментую із затримкою концепції змінних змінних на пізніше
at.

5
Ми тут перебуваємо поза темою, але ви впевнені, що перехід на генератори ES6 (чи будь-яку іншу нову концепцію високого рівня) - це гарна ідея, перш ніж вони дізнаються про змінні змінні? :)
doldt

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, the

Якщо у вас виникають проблеми з переглядом того, як працює останній приклад, це залежить від вашої обізнаності про одну з найстаріших бойових осей JavaScript; оператор коми - коротше кажучи, він обчислює вирази зліва направо і повертає значення останнього обчисленого виразу

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

У нашому вище прикладі я використовую

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

що просто стислий спосіб написання

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

Оптимізація виклику хвоста

Настільки ж сексуально, як і рекурсивні реалізації, на даний момент для мене було б безвідповідально рекомендувати їх, враховуючи, що жоден JavaScript VM, який я можу придумати, підтримує належне усунення хвостових викликів - вавило використовується для його транпіляції, але це було "зламано; буде повторно "статус понад рік.

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? Чому взагалі так багато дотичних до запитання з відповіддю на 2 символи? Прості запитання - це не просто платформа для користувачів, щоб зробити презентацію про деякі акуратні факти програмування.
Тимофей «Саша» Кондрашов

266

Використання оператора 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, для чого використовуються ключі.
у.

@at. дійсно. Але я не впевнений, що [0..x]в JS синонім haskell є більш стислим, ніж у моїй відповіді.
zerkms

ви можете помилитися, що немає нічого більш стислого, ніж це.
у.

Добре, я розумію , чому це працює , враховуючи відмінності Array.prototype.keysі Object.prototype.keys, але він упевнений , збиває з пантелику , на перший погляд.
Марк Рід

1
@cchamberlain з TCO в ES2015 (не впроваджено ніде?), це може викликати менше занепокоєння, але дійсно :-)
zerkms

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

@Kugel Другий, можливо, виділить на стеку, хоча
Бергі

Гарний момент не впевнений, чи спрацює тут оптимізація хвостових викликів @ 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/…
Hossam Mourad


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'

Бічна примітка - визначення функції діапазону

Подібне / пов’язане питання, що використовує принципово дуже схожі конструкції коду, може бути, чи є зручна функція діапазону в (основний) 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) );

Ця версія задовольняє вимогу ОП щодо незмінності. Також розглянути можливість використання reduceзамістьmap залежно від випадку використання.

Це також варіант, якщо ви не заперечуєте проти невеликої мутації у своєму прототипі.

Number.prototype.times = function(f) {
   return Array(this.valueOf()).fill().map((_,i)=>f(i));
};

Тепер ми можемо це зробити

((3).times(i=>console.log(i)));

+1 для арсельдона за .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
@Павло, насправді, ні. Масив (10) створить масив довжиною 10, але без жодних визначених в ньому ключів, що робить конструкцію forEach непридатною в цьому випадку. Але насправді це може бути спрощено, якщо ви не використовуєте дляEEach, дивіться відповідь zerkms (хоча для цього використовується ES6!).
doldt

творчий @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)
})

Зверніть увагу, що це повертає масив результатів, так що він справді більше схожий на цей рубін:

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

Я використовую грудку (функцію без аргументів), щоб досягти ледачої оцінки в 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

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

То чому б просто не використати forдвічі?
zerkms

я не боюся використовувати для. питання полягало в тому, щоб не використовувати змінну. але результат - це завжди якесь кешування змінної ака.
Ніна Шольц

1
"було щось не використовувати змінну" --- і ти все ще використовуєш її - 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 це "патч-мавпа", це не лише випадок розширення? Обійми та продовжимо :)
conny

Ні. Додавання функцій до Number (або String, Array або будь-якого іншого класу, який ви не авторували), за визначенням, є або поліфілами, або мавпами - і навіть не заповнюються. Прочитайте визначення понять «мавпа патч», «поліфіл» та рекомендовану альтернативу «поніфіл». Це те, що ти хочеш.
мрм

Щоб розширити число, ви зробили б: клас SuperNumber розширює число {раз (fn) {for (нехай i = 0; i <це; 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.