1
<!DOCTYPE HTML>
<html><head><script>
function test() {
    array = [];
    for (i = 0; i < 10; i += 1) {
        array.push(
            {a:'start' + i, b:function() {return 'end' + i;}}
        );
    }
    return array;
}
window.onload = function () {
      alert(test()[5].a + ' ' + test()[5].b());
}
 </script></head><body></body></html>

In the above code, this alerts 'start5 end10'.

I'm trying to pass the value of i into the function at property b, but can't figure out how to do it.

I would like it to have access to i via the closure and alert 'start5 end5'. How do you pass the value of i into the function at property b so it can resolve correctly when test()[5].b() is executed?

ciso
  • 2,887
  • 6
  • 33
  • 58
  • 1
    _"I would like it to have access to i via the closure"_ - It _is_ accessing `i` via the closure, but for your requirement you need to introduce a new closure on each iteration so that each has a separate `i` variable. Or you could use some variation on `{a:'start' + i, i:i, b:function() {return 'end' + this.i;}}` – nnnnnn Jan 01 '14 at 00:47

4 Answers4

2

You fell victim to the closure monster. Nothing to be afraid of, you're not the first and will not be the last. The closure monster is always hungry :)

Your solution is not working because the unnamed functions stored in your array of objects is always referencing the live value of i (which will be 10 at the end of the loop).

The way to avoid this in JS is to create a different function context for each value of b. The usual way to do so is to create a "factory" function returning the actual function invokation that returns the proper b value.

function b_factory (b)
{
    return function ()
    {
        return 'end'+b;
    }
}

Notice how the returned function references a b variable that is outside its scope. It is this ability to provide a value for variables outside the scope of a given function that is called a lexical closure. In this case, the closure lies in b_factory's parameter.

Now to use this:

function test() {
    array = [];
    for (i = 0; i < 10; i += 1) {
        array.push(
            {a:'start' + i, b:b_factory(i)}
        );
    }
    return array;
}

What happens here is:

  • the unnamed function returned by b_factory is referenced by array[i].b, so it is not destroyed by the garbage collector
  • since the b parameter of b_factory is in turn referenced by the unnamed function, the execution context of b_factory (where b is to be found) is also kept alive in memory.
  • the value of b as seen from inside the unnamed function is the value of the parameter passed to b_factory, i.e. the value of i at the time of the call to b_factory.

Once you get familiar with this, you can dispense with the creation of a factory function and use an unnamed function to provide the closure, like so:

    array.push(
        {a:'start' + i, b:function(b) {return function() { return 'end'+b;}}(i)}

Note that in this case, the inner unnamed function is a bit of an overkill. Since the b field of your object will be a constant for the rest of the execution, your unnamed function has nothing at all to compute (everything is constant seen from its context). It is about as useful as defining an array of functions like these:

function give_me_one () { return 1; }
function give_me_two () { return 2; }
// ;)

You could define your b field as a string and use this code instead:

    array.push(
        {a:'start' + i, b:function(b) { return 'end'+b; }(i)}

Here the unnamed function still creates the same closure, but returns a constant string instead of a function. Since there is no persisting object to reference the b parameter, the closure gets garbage-collected immediately after the unnamed function execution.

The b field is now a string, so you should access it without the brackets, like so:

console.log(test()[5].a + ' ' + test()[5].b);

Of course this is just an exercise to demonstrate the use of closures. The most efficient way of initializing your object is just to handle b as you did handle a, i.e.

    array.push(
        {a:'start'+i, b:'end'+i}
kuroi neko
  • 8,479
  • 1
  • 19
  • 43
  • `b:function(b) { return 'end'+b; }(i)` is pretty pointless - simply `b: 'end' + i` would be enough as it is not changing anything in scope and is executed immediately (and would be nice to pack instant running function into brackets to point that it is instantly run) – lupatus Jan 01 '14 at 02:55
  • Did you read my post till the end? Creating a lambda function to return a constant value is just as pointless, and even more wasteful. That was the point I was trying to make. – kuroi neko Jan 01 '14 at 03:29
1

Try something like this:

var getB = function(i) {
    return function() {return 'end' + i;}
}
for (i = 0; i < 10; i += 1) {
    array.push(
        {a:'start' + i, b:getB(i)}
    );
}

Demonstration

p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
1

Its one of standard problems with understanding scope in JS - your function is referencing to i used in for loop (referencing to variable, not it's value), so you're getting end value of that variable when function is called after loop end. Its enough to create that function from within another function and pass i to factory function (variables in JS are passed by value). Additionally I don't see any definition of variable i in your code, you're just starting to use i without initializing it - it'll be bound to top level object (window object in this example, variable will be visible global) - avoid such situations as you can reach some strange problems when two functions will start to modify global variable, it can lead to endless loop or loops that ends to early.

function test() {
    array = [];
    for (var i = 0; i < 10; i += 1) {
        array.push({
            a: 'start' + i,
            b: (function (i) {
                return function() {return 'end' + i;};
            }(i))
        });
    }
    return array;
}
lupatus
  • 4,208
  • 17
  • 19
0
<!doctype html>
<html lang="zh">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Template Index</title>


</head>
<body>

  <script>
function test() {
    array = [];
    for (i = 0; i < 10; i += 1) {
        array.push(
          (function (start, end) {
            return {a:'start' + start, b:function() {return 'end' + end;}};
          } (i, i))

        );
    }
    return array;
}
window.onload = function () {
      alert(test()[5].a + ' ' + test()[5].b());
}

  </script>
</body>
</html>

in your code {a:'start' + i, b:function() {return 'end' + i;}}

a will get the correct number because it is concat with string 'start' when it loop through 0--9

but b is generate throw a function wich works with closure,

all function will access the same i in the loop, which is 10 after your loop

you need create a new function to hold the snapshot of i so every object has its own copy with

the value of i in the loop

qiu-deqing
  • 1,323
  • 7
  • 13
  • 2
    May I suggest that there is no need to introduce extra html tags that we have to scroll past before getting to the actual JS code? – nnnnnn Jan 01 '14 at 00:54