11
function first(){
    var items = document.getElementsByTagName("li");

    for(var x = 0; x < items.length; x++){
        items[x].onclick = function() {
            console.log(x);
        }
    }
}

function second(){
    var items = document.getElementsByTagName("li");

    for(var x = 0; x < items.length; x++){
        (function(val) {
            items[val].onclick = function() {
                console.log(val);
            }
        })(x);
    }
}

function third(){
    var items = document.getElementsByTagName("li");

    for(let x = 0; x < items.length; x++){
        items[x].onclick = function() {
            console.log(x);
        }
    }
}

There are 4 elements in the list. Outputs of the 3 functions:

first: 4 4 4 4
second: 0 1 2 3
third: 0 1 2 3

I am not able to understand the output from the third function. In the second function, each call to the IIFE creates a new function object and hence, a new val variable. But in the third function, there is a single copy of the variable x, then how is the output: 0 1 2 3

Please correct me if I am wrong.

Shashank Shekhar
  • 351
  • 3
  • 11
  • That's one of the purposes of using [`let`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let), it's only defined in the scope it's created in. The `items.length` number of anonymous functions all refer to difference instances of `x` because you defined it with `let`. – Spencer Wieczorek Mar 02 '17 at 13:37
  • 1
    @SpencerWieczorek "... it's only defined in the scope it's created in." While true, this doesn't actually address the question. As James Thorpe points out below, the fact that `let` works "as expected" within closures when used to initialize a `for` loop is an *exception* to the normal semantics of `let`, not a consequence of them. `const` is also scoped to its block, but we can't do `for (const a = i; a < n; a++)` - this will raise an error on the second loop iteration because we're trying to assign a value to an already-initialized `const`. – user1974458 Mar 12 '19 at 00:05
  • In short, `let` and `const` are both scoped to the containing block, but `let` also has a special behaviour inside `for` loops that scopes it to the loop iteration *instead* of to the block. – user1974458 Mar 12 '19 at 00:07
  • @user1974458 Comments aren't intended to be answers to the question. Also I state this in my answer below: "There is a different instance on each iteration of the loop.". It's not that it's a special behavior, it's that each iteration is considered a different block for the scope. – Spencer Wieczorek Mar 12 '19 at 05:12
  • @SpencerWieczorek I did read your answer and I had the same problem with it: that each iteration is considered a different block for the scope *is* a special behaviour. It is unique to `let` in this case. Try using `const` instead; you'll find it doesn't work, because when the loop is initialized with `const`, there is only one scope for the block, not one per iteration. Scoping for `let` and `const` is, so far as I know, otherwise identical; it differs only in `for` loops - hence, special behaviour. – user1974458 Mar 13 '19 at 06:06
  • @user1974458 That is actually a [special behavior with `const` in specifically the normal for loop syntax](https://stackoverflow.com/questions/31987465/ecmascript-2015-const-in-for-loops). If you use `for/of` or `for/in` you will see that `const` will scope to each iteration. – Spencer Wieczorek Mar 13 '19 at 21:51
  • @SpencerWieczorek I am aware. :) I think we're nit picking now as to which one is called a "special" behaviour and which is not; my point was only that you can't correctly answer the original question by stating that this is a consequence of block scoping, as there is no single behaviour for block scoping in Javascript for loops. "Block scoping" can easily imply - as it does in e.g. C++ and also with `const` in Javascript - a single binding for the loop variable (rather than one per iteration), and this would not answer the OP's question. – user1974458 Mar 14 '19 at 22:32
  • 2
    In other words, the reason for the behaviour is not that `let` is scoped to the block but that it's scoped to the iteration. Technically the iterator variable isn't even being defined *in* the block; it's being defined in the loop construct, and how those bindings work goes on a case by case basis. Personally I find the inconsistency between `let` and `const` surprising and counterintuitive. The only reason we can't use `const` to initialize a `for` loop is that there is only one binding for the loop. Then they go and give us multiple bindings if we use `let` instead! – user1974458 Mar 14 '19 at 22:35

3 Answers3

6

In the documentation for let from MDN they have an example covering this exact case in the Cleaner Code in inner functions section:

for (let i = 1; i <= 5; i++) {
  let item = document.createElement('li');
  item.appendChild(document.createTextNode('Item ' + i));

  item.onclick = function(ev) {
    console.log('Item ' + i + ' is clicked.');
  };
  list.appendChild(item);
}

The example above works as intended because the five instances of the (anonymous) inner function refer to five different instances of the variable i. Note that it does not work as intended if you replace let with var, since all of the inner functions would then return the same final value of i: 6. Also, we can keep the scope around the loop cleaner by moving the code that creates the new elements into the scope of each loop.

The same thing applies to your case, because you use let each anonymous function refers to a different instance of x. There is a different instance on each iteration of the loop. This happens because let has a block-level scope instead of the global function scope that var has.

Spencer Wieczorek
  • 21,229
  • 7
  • 44
  • 54
0

From the docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let

let allows you to declare variables that are limited in scope to the block, statement, or expression on which it is used. This is unlike the var keyword, which defines a variable globally, or locally to an entire function regardless of block scope.

When it is var it gets hoisted like all variables.

When it is let the scope is the block it is defined in.

Mike Cheel
  • 12,626
  • 10
  • 72
  • 101
  • 6
    That doesn't _entirely_ answer the Q - `for` loops also have special semantics for `let` declared variables, in that each one is scoped not just to the `for` loop, but to each iteration – James Thorpe Mar 02 '17 at 13:37
  • @JamesThorpe I wish I could upvote your comment about nine more times. I've been reading answer after answer about the behaviour of let inside for loops and how it affects closures, and most of the answers are incomplete or simply wrong. Yours covers it. – user1974458 Mar 11 '19 at 23:56
0

This is one of the trickiest examples of let keyword.

The fact that Let binds variables to the block(& in this case for-loop) means that it binds the variable to every iteration of the loop. So, when loop is finished, you have 4 items (items from item[0] to item[3]) listening to click event.

In fact the for-loop in the third function produces the following:

items[0].onclick = function() {
        console.log(0);
   }
items[1].onclick = function() {
        console.log(1);
   }
items[2].onclick = function() {
        console.log(2);
   }
items[3].onclick = function() {
        console.log(3);
   }

Make sure to read more about Let here in MDN Some other exciting cases could be found there.

behkod
  • 2,647
  • 2
  • 18
  • 33