Zapiranje v jeziku JavaScript znotraj zank - preprost praktični primer

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

Izpisuje tole:

Moja vrednost: 3 Moja vrednost: 3 Moja vrednost: 3

Medtem ko bi želel, da se izpiše: Moja vrednost: 0 Moja vrednost: 1 Moja vrednost: 2


Enaka težava se pojavi, kadar je zamuda pri izvajanju funkcije posledica uporabe poslušalcev dogodkov:

-- begin snippet: js hide: false console: true babel: false -->

var buttons = document.getElementsByTagName("button");
// let's create 3 functions
for (var i = 0; i < buttons.length; i++) {
  // as event listeners
  buttons[i].addEventListener("click", function() {
    // each should log its value.
    console.log("My value: " + i);
  });
}
<button>0</button>
<br />
<button>1</button>
<br />
<button>2</button>

... ali asinhrone kode, npr. z uporabo obljub:

// Some async wait function
const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms));

for (var i = 0; i < 3; i++) {
  // Log `i` as soon as each promise resolves.
  wait(i * 100).then(() => console.log(i));
}

Kakšna je rešitev tega osnovnega problema?

Rešitev

Težava je v tem, da je spremenljivka i v vsaki od vaših anonimnih funkcij vezana na isto spremenljivko zunaj funkcije.

Klasična rešitev: Zapiranje

Spremenljivko znotraj vsake funkcije želite vezati na ločeno, nespremenljivo vrednost zunaj funkcije:

var funcs = [];

function createfunc(i) {
  return function() {
    console.log("My value: " + i);
  };
}

for (var i = 0; i < 3; i++) {
  funcs[i] = createfunc(i);
}

for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

Ker v javascriptu ni obsega blokov, temveč le obseg funkcij, z ovitjem ustvarjanja funkcije v novo funkcijo zagotovite, da vrednost "i" ostane takšna, kot ste načrtovali.


2015 Rešitev: forEach

Ker je funkcija Array.prototype.forEach relativno široko dostopna (v letu 2015), je vredno omeniti, da v primerih, ki vključujejo iteracijo predvsem nad poljem vrednosti, funkcija .forEach() zagotavlja čist in naraven način za pridobitev ločenega zaključka za vsako iteracijo. To pomeni, da lahko ob predpostavki, da imate neko vrsto polja, ki vsebuje vrednosti (reference DOM, predmete, karkoli), in da se pojavi težava z vzpostavitvijo povratnih klicev, značilnih za vsak element, naredite to:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

Ideja je, da bo vsak klic funkcije povratne zveze, ki se uporablja z zanko .forEach, svoje lastno zaprtje. Parameter, posredovan tej opravi, je element polja, ki je značilen za določen korak iteracije. Če je uporabljen v asinhronem povratnem klicu, ne bo trčil z nobenim od drugih povratnih klicev, vzpostavljenih v drugih korakih iteracije.

Če delate v programu jQuery, vam podobno možnost omogoča funkcija $.each().


Rešitev ES6: let

ECMAScript 6 (ES6) uvaja novi ključni besedi let in const, ki imata drugačen obseg kot spremenljivke na osnovi var. Na primer, v zanki z indeksom, ki temelji na let, bo imela vsaka iteracija skozi zanko novo vrednost i, pri čemer je vsaka vrednost scoped znotraj zanke, zato bi vaša koda delovala, kot ste pričakovali. Obstaja veliko virov, vendar bi priporočil 2alityjevo objavo Block-scoping kot odličen vir informacij.

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value: " + i);
  };
}

Pazite pa, da IE9-IE11 in Edge pred Edge 14 podpirajo let, vendar se zgoraj navedeno napačno izvaja (ne ustvarijo vsakič novega i, zato bi se vse zgornje funkcije prijavile 3, kot bi se, če bi uporabili var). Edge 14 to končno naredi pravilno.

Komentarji (23)

Poskusite:

var funcs = [];

for (var i = 0; i < 3; i++) {
    funcs[i] = (function(index) {
        return function() {
            console.log("My value: " + index);
        };
    }(i));
}

for (var j = 0; j < 3; j++) {
    funcs[j]();
}

Edit (2014):

Novejši odgovor o uporabi .bind je po mojem mnenju zdaj najboljši način za tovrstno početje. Obstaja tudi lo-dash/underscoreov _.partial, kadar ne potrebujete ali se ne želite ukvarjati z thisArg, ki ga uporablja bind.

Komentarji (3)

Vaš prvotni primer ni deloval, ker so se vse zapore, ki ste jih ustvarili v zanki, sklicevale na isti okvir. V bistvu imate tri metode na enem objektu z eno samo spremenljivko i. Vse so izpisale isto vrednost.

Komentarji (0)