2

(This question is related to this and this but answers there haven't helped me figure out what's wrong in my case.)

I am trying to create an array of clickable elements where each element is bound to a separate instance of some object.

I've simplified the real code I'm working on as much as possible for this question here:

//----------
// Setup part

// SomeObject just holds a number
var SomeObject = function(number) {
 this.number = number;
 
 this.getNumber = function() {
  return this.number;
 };
};

// contains SomeObject(1) through SomeObject(9)
var someArrayContainingObjects = [];
for(var i=1; i<=9; i++)
{
 someArrayContainingObjects.push(new SomeObject(i));
}

//----------
// Problem part
for(var y=0; y<3; y++)
{ 
 for(var x=0; x<3; x++)
 {
  var obj = someArrayContainingObjects[y*3 + x]; // Creating new variable in the loop every time explicitly with var statement?
  $("body").append(
   $("<button />")
   .text("Should output ("+obj.getNumber()+")")
   .click(function() {
    alert(obj.getNumber()); // Will always be 9
   })
  );
 }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

I thought by explicitly using var obj = ... inside the loop I would create a new context/scope/however it's called for each anonymous click() callback function I'm creating – so that when I click one of the objects, the appropriate number of the respective SomeObject is alert()ed and not always the number of the last SomeObject the loop takes from the array.

Could someone please explain to me why this code snippet does not work as expected, and what to change to have the code function correctly?

Community
  • 1
  • 1
Julian
  • 757
  • 1
  • 7
  • 20
  • 2
    There's no block scope in ES5 – Dave Newton Jul 31 '15 at 19:14
  • But there sure must be code out there that builds interfaces in a similar style - there must be at least some ugly workaround to make it work in the current JS specification? – Julian Jul 31 '15 at 19:16
  • 1
    @Julian you could wrap the obj declaration and button spec in an anonymous function, then immediately call it. – Matt Olson Jul 31 '15 at 19:19
  • I gave you one option, the function generator. There may be other, better (and worse) ways. Azium's answer uses an IIFE which is likely a better option. (I tend to fixate on function generators, but it's not useful in this case.) – Dave Newton Jul 31 '15 at 19:22
  • @MattOlson Thank you very much! :) It's ugly but it solves my issue. – Julian Jul 31 '15 at 19:27
  • Come on... This same question has been asked at least 1M times :) – Vidul Jul 31 '15 at 19:28
  • @Vidul If only I had found those other questions... not that I didn't search before posting mine. I just didn't know the correct terms to search for. – Julian Jul 31 '15 at 19:29
  • @Julian It's not particularly ugly; it's canonical JS because of its scoping rules. It gives you a lot of options, too; JS is pretty flexible for a mainstream language. – Dave Newton Jul 31 '15 at 19:31

1 Answers1

1

To create closure scope in JavaScript you need to invoke a function. In JavaScript we can also invoke functions as soon as you declare them. They are called immediately invoked function expressions

This way you can preserve your x and y values in the scope of the IIFE.

for(var y=0; y<3; y++) {    
  for(var x=0; x<3; x++) {
    (function (x, y) {
      var obj = someArrayContainingObjects[y * 3 + x]
      $("body").append(
        $("<button />")
          .text("Should output ("+obj.getNumber()+")")
          .click(function() {
            alert(obj.getNumber())
          })
      )
    }(x, y))
  }
}

Working codepen

Also, this is a big problem that people encounter when they try to write JavaScript as if it was a class based language. I would try to look into writing JS from a more functional perspective

azium
  • 17,620
  • 5
  • 44
  • 73