Czy istnieje mechanizm pętli x razy w ES6 (ECMAScript 6) bez zmiennych mutacyjnych?

Typowy sposób pętli x razy w JavaScript to:

for (var i = 0; i < x; i++)
  doStuff(i);

Ale nie chcę używać operatora ++ ani mieć w ogóle zmiennych mutowalnych. Czy w ES6 istnieje sposób na pętlę x razy w inny sposób? I love Ruby ' s mechanism:

x.times do |i|
  do_stuff(i)
end

Coś podobnego w JavaScript / ES6? Mógłbym oszukać i zrobić własny generator:

function* times(x) {
  for (var i = 0; i < x; i++)
    yield i;
}

for (var i of times(5)) {
  console.log(i);
}

Oczywiście, że nadal używam i++. Przynajmniej jest poza zasięgiem wzroku:), ale mam nadzieję, że w ES6 jest lepszy mechanizm.

Author: at., 2015-05-26

15 answers

OK!

Poniższy kod jest napisany przy użyciu składni ES6, ale może być równie łatwo napisany w ES5 lub nawet mniej. ES6 jest a nie wymogiem tworzenia "mechanizmu pętli x razy"


Jeśli nie potrzebujesz iteratora w wywołaniu zwrotnym , jest to najprostsza implementacja

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

Jeśli potrzebujesz iteratora , możesz użyć nazwanej funkcji wewnętrznej z parametrem counter do iteracji dla ty

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

Przestań tu czytać, jeśli nie lubisz uczyć się więcej rzeczy ...

Ale coś powinno być z tym nie tak...

  • pojedyncza gałąź if wypowiedzi są brzydkie - co dzieje się na drugiej gałęzi ?
  • wiele wyrażeń/wyrażeń w ciałach funkcyjnych - czy procedury są mieszane ?
  • implicite Return undefined - wskazanie nieczystości, funkcja side-effecting

"nie ma lepszego sposobu ?"

Jest. Najpierw przyjrzyjmy się naszej wstępnej implementacji
// times :: Int -> (void -> void) -> void
const times = x => f => {
  if (x > 0) {
    f()               // has to be side-effecting function
    times (x - 1) (f)
  }
}

Jasne, to proste, ale zauważ, jak po prostu nazywamy {[14] } i nic z tym nie robimy. To naprawdę ogranicza rodzaj funkcji, którą możemy powtarzać wielokrotnie. Nawet jeśli mamy dostępny iterator, f(i) nie jest dużo bardziej uniwersalny.

Co jeśli zaczniemy od lepszej procedury powtarzania funkcji ? Może coś, co lepiej wykorzystuje wejście i wyjście.

Powtórzenie funkcji generycznych

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

Powyżej zdefiniowaliśmy ogólną funkcję repeat, która pobiera dodatkowe dane wejściowe, które są używane do rozpoczęcia wielokrotnego stosowania pojedynczej funkcji.

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

Realizacja times z repeat

Teraz jest to łatwe, prawie cała praca jest już wykonana.

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

Od nasza funkcja przyjmuje i jako wejście i zwraca i + 1, to skutecznie działa jako nasz iterator, który przekazujemy f za każdym razem.

Naprawiliśmy również naszą listę wypunktowanych problemów

  • No more ugly single branch if statements
  • ciała jednokrotne wskazują na ładnie rozdzielone zagadnienia
  • koniec z bezużytecznością, bezwarunkowo zwrócony undefined

Operator Przecinka JavaScript, the

In case you ' re having trouble widząc, jak działa ostatni przykład, zależy to od twojej świadomości jednej z najstarszych osi bitewnych JavaScript; operator przecinka – krótko mówiąc, oblicza wyrażenia od lewej do prawej i zwraca wartość ostatniego ocenianego wyrażenia

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

W powyższym przykładzie używam

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

Co jest tylko zwięzłym sposobem pisania

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

Optymalizacja Połączeń Ogonowych

Tak seksowne jak rekurencyjne implementacje czy, w tym momencie byłoby nieodpowiedzialne dla mnie polecanie ich, biorąc pod uwagę, że nie JavaScript VM mogę myśleć o wspieraniu właściwej eliminacji wywołań ogonowych-babel używany do transpilowania go, ale jest w stanie" broken; will reimplement " od ponad roku.

repeat (1e6) (someFunc) (x)
// => RangeError: Maximum call stack size exceeded
W związku z tym, powinniśmy ponownie przyjrzeć się naszej implementacji repeat, aby uczynić ją bezpieczną dla stosu.

Poniższy kod używa zmiennych mutowalnych n i x, ale zauważ, że wszystkie mutacje są zlokalizowane na funkcja repeat - bez zmian stanu (mutacji) są widoczne spoza funkcji

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

To będzie mieć wiele z was mówiąc " ale to nie jest funkcjonalne !- Wiem, odpręż się. Możemy wdrożyć styl Clojure loop/recur interfejs do zapętlania stałej przestrzeni za pomocą czystych wyrażeń ; żadne z tych while rzeczy.

Tutaj mamy while z dala od naszej loop funkcji-szuka specjalnego recur typu, aby zachować pętlę bieganie. Gdy napotkany jest typ Nie-recur, pętla jest zakończona, a wynik obliczeń jest zwracany

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
 105
Author: user633183,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-08 19:12:33

Używając ES2015 operator spreadu:

[...Array(n)].map()

const res = [...Array(10)].map((_, i) => {
  return i * 10;
});

// as a one liner
const res = [...Array(10)].map((_, i) => i * 10);

Lub jeśli nie potrzebujesz wyniku:

[...Array(10)].forEach((_, i) => {
  console.log(i);
});

// as a one liner
[...Array(10)].forEach((_, i) => console.log(i));

Zwróć uwagę, że jeśli potrzebujesz tylko powtarzanego ciągu, możesz użyć String.prototyp.powtórz .

console.log("0".repeat(10))
// 0000000000
 122
Author: Tieme,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-23 11:01:23
for (let i of Array(100).keys()) {
    console.log(i)
}
 30
Author: zerkms,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-26 07:46:03

Myślę, że najlepszym rozwiązaniem jest użycie let:

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

, która utworzy nową (zmienną) i zmienną dla każdej oceny ciała i zapewni, że i jest zmieniana tylko w wyrażeniu przyrostowym w składni tej pętli, a nie gdziekolwiek indziej.

Mógłbym oszukać i stworzyć własny generator. Przynajmniej {[6] } jest poza zasięgiem wzroku:)
To powinno wystarczyć imo. Nawet w czystych językach wszystkie operacje (a przynajmniej ich interpretery) zbudowane są z prymitywy, które używają mutacji. Tak długo, jak to jest odpowiednio scoped, nie widzę, co jest w tym złego.

You should be fine with

function* times(n) {
  for (let i = 0; i < x; i++)
    yield i;
}
for (const i of times(5))
  console.log(i);

Ale nie chcę używać operatora ++ ani mieć w ogóle zmiennych mutowalnych.

Wtedy jedynym wyborem jest użycie rekurencji. Można zdefiniować tę funkcję generatora również bez mutowalnego i:

function* range(i, n) {
  if (i >= n) return;
  yield i;
  return yield* range(i+1, n);
}
times = (n) => range(0, n);

Ale wydaje mi się to przesadą i może mieć problemy z wydajnością (ponieważ eliminacja ogona jest niedostępne dla return yield*).

 21
Author: Bergi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-02 12:36:01

Odpowiedź: 09 Grudnia 2015

osobiście znalazłem zaakceptowaną odpowiedź zarówno zwięzłą (dobrą), jak i zwięzłą (złą). Doceniam to stwierdzenie może być subiektywne, więc proszę przeczytać tę odpowiedź i zobaczyć, czy zgadzasz się lub nie

Przykład podany w pytaniu był podobny do Rubiego:

x.times do |i|
  do_stuff(i)
end

Wyrażenie tego w JS używając poniższego kodu pozwoli na:

times(x)(doStuff(i));

Oto kod:

let times = (n) => {
  return (f) => {
    Array(n).fill().map((_, i) => f(i));
  };
};

To jest to!

Prosty przykład sposób użycia:

let cheer = () => console.log('Hip hip hooray!');

times(3)(cheer);

//Hip hip hooray!
//Hip hip hooray!
//Hip hip hooray!

Alternatywnie, zgodnie z przykładami zaakceptowanej odpowiedzi:

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'

Uwaga boczna-definiowanie funkcji zakresu

Podobne / powiązane pytanie, które używa zasadniczo bardzo podobnych konstrukcji kodu, może być, czy istnieje wygodna funkcja Range W (core) JavaScript, coś podobnego do funkcji range underscore ' a.

Utworzyć tablicę z N liczbami, zaczynając od x

Podkreślenie

_.range(x, x + n)

ES2015

kilka alternatyw:

Array(n).fill().map((_, i) => x + i)

Array.from(Array(n), (_, i) => x + i)

Demo używając 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 ]

W szybkim teście uruchomiłem, przy czym każde z powyższych uruchomiło się milion razy za pomocą naszego rozwiązania i funkcji doStuff, poprzedniego podejścia(Array (N).fill()) okazał się nieco szybszy.

 11
Author: arcseldon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-24 13:49:05

Myślę, że to dość proste:

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

Lub

Array(3).fill()
 7
Author: Gergely Fehérvári,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-04 17:46:02
const times = 4;
new Array(times).fill().map(() => console.log('test'));

Ten fragment będzie console.log test 4 razy.

 7
Author: Hossam Mourad,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-26 19:39:50

Nie coś, czego bym nauczył (lub kiedykolwiek używał w moim kodzie), ale oto rozwiązanie godne codegolf bez mutacji zmiennej, bez potrzeby ES6:

Array.apply(null, {length: 10}).forEach(function(_, i){
    doStuff(i);
})
Bardziej interesujący dowód koncepcji, niż przydatna odpowiedź.
 5
Author: doldt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-26 07:45:07
Array(100).fill().map((_,i)=> console.log(i) );

Ta wersja spełnia wymagania OP dotyczące niezmienności. Rozważ również użycie reduce zamiast map w zależności od przypadku użycia.

Jest to również opcja, jeśli nie masz nic przeciwko małej mutacji w swoim prototypie.

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

Teraz możemy to zrobić

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

+1 do arcseldona za sugestię .fill.

 5
Author: Tom,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-22 23:35:46

Afaik, w ES6 nie ma mechanizmu podobnego do metody Ruby times. Ale można uniknąć mutacji używając rekurencji:

let times = (i, cb, l = i) => {
  if (i === 0) return;

  cb(l - i);
  times(i - 1, cb, l);
}

times(5, i => doStuff(i));

Demo: http://jsbin.com/koyecovano/1/edit?js, console

 2
Author: Pavlo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-17 13:33:24

W paradygmacie funkcjonalnym repeat jest zwykle nieskończoną funkcją rekurencyjną. Aby go użyć, potrzebujemy albo leniwej oceny, albo kontynuowania stylu przechodzenia.

Leniwe powtarzanie funkcji

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

Używam thunk (funkcji bez argumentów) do osiągnięcia leniwej oceny w Javascript.

Powtórzenie funkcji z kontynuacją stylu przejścia

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 na początku jest trochę przerażający. Jednak zawsze wynika z tego samego wzoru: ostatnim argumentem jest kontynuacja (funkcja), która wywołuje własne ciało: k => k(...). Należy pamiętać, że CPS wywraca aplikację na lewą stronę, tzn. take(8) (repeat...) staje się k(take(8)) (...), Gdzie k jest częściowo zastosowanym repeat.

Podsumowanie

Oddzielając powtórzenie (repeat) od warunku zakończenia (take) uzyskujemy elastyczność-rozdzielenie trosk aż do jego gorzkiego końca: d

 2
Author: ftor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-21 12:05:30

Jeśli chcesz skorzystać z biblioteki, jest też lodash _.times lub podkreślenie _.times:

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

Zwróć uwagę, że zwraca tablicę wyników, więc jest bardziej podobny do tego ruby:

x.times.map { |i|
  doStuff(i)
}
 1
Author: ronen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-16 21:25:28

Adresowanie aspektu funkcjonalnego:

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);
});
 0
Author: Nina Scholz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-05-26 11:17:38

Generatory? Rekurencja? Dlaczego tak nienawidzisz mutacji? ;-)

Jeśli jest akceptowalny tak długo, jak go "ukrywamy", to po prostu akceptujemy użycie operatora jednoargumentowego i możemy zachować rzeczy proste :

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

Tak jak w ruby:

> (3).times(console.log)
0
1
2
 0
Author: conny,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-14 05:10:16

Zalety tego rozwiązania

  • ]}
  • wartość zwracana może być użyta jako suma lub po prostu zignorowana
  • prosta wersja es6, również link do Wersja maszynopisu kodu

Wady - Mutacja. Bycie wewnętrznym tylko mnie to nie obchodzi, może inni też nie.

Przykłady i Kod

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 wersja
https://codepen.io/whitneyland/pen/aVjaaE?editors=0011

 0
Author: Lee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-27 02:47:52