closures - Javascript ökända loop-problem?


Jag har följande kodsnutt.

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function () {
            alert(i);
        };
        document.body.appendChild(link);
    }
}

Ovanstående kod är för att generera 5 länkar och binda varje länk med ett varningsevent för att visa det aktuella länk-id:t. Men det fungerar inte. När du klickar på de genererade länkarna säger de alla "länk 5".

Men följande kodsnutt fungerar som vi förväntar oss.

function addLinks () {
    for (var i=0, link; i<5; i++) {
        link = document.createElement("a");
        link.innerHTML = "Link " + i;
        link.onclick = function (num) {
            return function () {
                alert(num);
            };
        }(i);
        document.body.appendChild(link);
    }
}

De ovanstående 2 utdragen är citerade härifrån. Enligt författarens förklaring verkar closure göra magin.

Men hur det fungerar och hur closure får det att fungera är allting bortom min förståelse. Varför fungerar inte den första medan den andra fungerar? Kan någon ge en detaljerad förklaring om magin?

Svar #1

Citerar mig själv för en förklaring av det första exemplet:

JavaScripts scope är på funktionsnivå, inte på blocknivå, och att skapa en closure betyder bara att den omgivande scopet läggs till i den lexikala miljön för den inneslutna funktionen.

Efter att loopen avslutas, har funktionsnivåvariabeln i värdet 5, och det är det som den inre funktionen 'ser'.

I det andra exemplet kommer den yttre funktionsliteralen vid varje iterationssteg att utvärderas till ett nytt funktionsobjekt med sitt eget omfång och lokala variabel num, vars värde sätts till det aktuella värdet av i. Eftersom num aldrig ändras kommer det att förbli konstant under hela stängningens livstid: Nästa iterationssteg skriver inte över det gamla värdet eftersom funktionsobjekten är oberoende.

Tänk på att detta tillvägagångssätt är ganska ineffektivt eftersom två nya funktionsobjekt måste skapas för varje länk. Detta är onödigt, eftersom de enkelt kan delas om du använder DOM-noden för informationslagring:

function linkListener() {
    alert(this.i);
}

function addLinks () {
    for(var i = 0; i < 5; ++i) {
        var link = document.createElement('a');
        link.appendChild(document.createTextNode('Link ' + i));
        link.i = i;
        link.onclick = linkListener;
        document.body.appendChild(link);
    }
}

Svar #2

Vi har 5 divs på sidan, var och en med ett ID ... div1, div2, div3, div4, div5

jQuery kan göra detta ...

for (var i=1; i<=5; i++) {
    $("#div" + i).click ( function() { alert ($(this).index()) } )
}

Men att verkligen ta itu med problemet (och bygga upp detta långsamt) ...

STEG 1

for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        // TODO: Write function to handle click event
    )
}

STEG 2

for (var i=1; i<=5; i++) {
    $("#div" + i).click (
        function(num) {
            // A functions variable values are set WHEN THE FUNCTION IS CALLED!
            // PLEASE UNDERSTAND THIS AND YOU ARE HOME AND DRY (took me 2 years)!
            // Now the click event is expecting a function as a handler so return it
            return function() { alert (num) }
        }(i) // We call the function here, passing in i
    )
}

ENKEL ATT FÖRSTÅ ALTERNATIV

Om du inte kan förstå det så borde detta vara lättare att förstå och har samma effekt ...

for (var i=1; i<=5; i++) {

    function clickHandler(num) {    
        $("#div" + i).click (
            function() { alert (num) }
        )
    }
    clickHandler(i);
    
}

Detta bör vara enkelt att förstå om du kommer ihåg att en funktions variabelvärden sätts när funktionen anropas (men detta använder exakt samma tankegång som tidigare)

Svar #3

I princip, i det första exemplet binder du i inne i onclick-hanteraren direkt till i utanför onclick-hanteraren. Så när i utanför onclick-hanteraren ändras, ändras i inne i onclick-hanteraren också.

I det andra exemplet, istället för att binda det till num i onclick-hanteraren, skickar du det till en funktion, som sedan binder det till num i onclick-hanteraren. När du skickar det till funktionen kopieras värdet av i, det binds inte till num. Så när i ändras, förblir num detsamma. Kopieringen sker eftersom funktioner i JavaScript är "closures", vilket betyder att när något har skickats till funktionen, är det "stängt" för modifiering utifrån.

Svar #4

Andra har förklarat vad som pågår, här är en alternativ lösning.

function addLinks () {
  for (var i = 0, link; i < 5; i++) {
    link = document.createElement("a");
    link.innerHTML = "Link " + i;

    with ({ n: i }) {
      link.onclick = function() {
        alert(n);
      };
    }
    document.body.appendChild(link);
  }
}

I princip den fattiges let-bindning.

Relaterade artiklar