Skip to content

Instantly share code, notes, and snippets.

@eanplatter
Last active October 17, 2016 14:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eanplatter/b3a121c88b96afe92eec3ef3bb18343d to your computer and use it in GitHub Desktop.
Save eanplatter/b3a121c88b96afe92eec3ef3bb18343d to your computer and use it in GitHub Desktop.
Scope issues with JavaScript

Hello Student!

Here is your original code:

<button id="btn-0">Button 1!</button>
<button id="btn-1">Button 2!</button>
<button id="btn-2">Button 3!</button>

<script type="text/javascript">
  var prizes = ['A Unicorn!', 'A Hug!', 'Fresh Laundry!'];
  for (var btnNum = 0; btnNum < prizes.length; btnNum++) {
      // for each of our buttons, when the user clicks it...
      document.getElementById('btn-' + btnNum).onclick = function() {
          // tell her what she's won!
          alert(prizes[btnNum]);
      };
  }
</script>

With this code, as you've noticed, you have a situation where each of your buttons are saying undefined when clicked.

The first step I would take to figure this problem out, so to break down the pieces of what you're alerting to see what is really going on:

  //
  document.getElementById('btn-' + btnNum).onclick = function() {
    // tell her what she's won!
    console.log(prizes) // ["A Unicorn!", "A Hug!", "Fresh Laundry!"]
    console.log(btnNum) // 3
    alert(prizes[btnNum]);
  };
  //

When we console.log prizes everything looks good, but look at the btnNum! It's 3! As we know, since arrays start at 0 when it comes to counting indexes, prizes[3] does not exist, or in other words it is undefined!

What is going on!?

This tends to be one of the more tricky issues with loops in JavaScript. Essentially, the issue is that loops in JavaScript do not have their own scope, or in other words, the variables you declare while making a loop exist outside the loop.

The loop runs instantly, 0, 1, 2 BAM DONE. Then you go to click the onclick handler for the button, and the button says "Hrmm, ok let me see, I gotta go find the prizes arr, oh there it is, go it! Now I need to find the btnNum variable... hmmm, oh there it is! looks like it is the number 3!" It is 3 because the loop is long gone, and has finished seconds ago.

This is because our onclick handler is using old information.

Ok, so... now what?

To solve this problem, we need a way to essentially take a snapshot of the variable while the loop is happening, that we can use later. The easiest way to do this is by passing the value into our onclick function.

  //
  document.getElementById('btn-' + btnNum).onclick = function(thePrize) {
    // tell her what she's won!
    alert(thePrize);
  }(prizes[btnNum]);
  //

So we changed a couple things, first we added a parameter to our function named thePrize. What this does is introduces a new variable into the scope of the function (thePrize) which we can reference later.

The second thing we added was (prizes[btnNum]) at the end of the definition of our function. This is a strange practice called an Immediately Invoked Function Expression, or IIFE (pronounced If-ee). Essentially we are running the function as we define it. This ensures that we are saving the value of prizes[btnNum]) at the time the loop is running, rather than later.

Now, if you were to try this code you might notice an issue. It automatically alerts all 3 values in a row! Without having us click the button. This is because we are using an IIFE, or in other words, we are immediately running the function as the loop is running.

Functions within Functions

There is one last piece of information we need to make this work. While we were able to use a function's scope to save the right variable, we had to run the function to do it. To solve this we will instead return another function, rather than running the alert. This inner function will allow us to keep the saved value we want, while also deferring its alert until we're ready.

  //
  document.getElementById('btn-' + btnNum).onclick = function(thePrize) {
    // tell her what she's won!
    return function () {
      alert(thePrize)
    }
  }(prizes[btnNum]);
  //

All we did here was return an anonymous function that alerts the prize for us! By using the outer function's variable scope, we were able to get the correct value! Wahoo!

This is a wonderful example of how to use closures in JavaScript. Closures are essentially functions that remember the variables defined around them, even if they're executed a long time after they're defined. They're a powerful feature of JavaScript, and as you can see, useful for times when you can't seem to get the right values.

For more reading on how closures work in JavaScript see MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures For more reading on how JavaScript uses blocks, and how they don't have scopes check out this MDN article: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/block

This is a pretty great question!

Thanks, Ean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment