Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are objects' values captured inside function calls?

Tags:

javascript

This code is supposed to pop up an alert with the number of the image when you click it:

for(var i=0; i<10; i++) {
    $("#img" + i).click(
        function () { alert(i); }
    );
}

You can see it not working at http://jsfiddle.net/upFaJ/. I know that this is because all of the click-handler closures are referring to the same object i, so every single handler pops up "10" when it's triggered.

However, when I do this, it works fine:

for(var i=0; i<10; i++) {
    (function (i2) {
        $("#img" + i2).click(
            function () { alert(i2); }
        );
    })(i);
}

You can see it working at http://jsfiddle.net/v4sSD/.

Why does it work? There's still only one i object in memory, right? Objects are always passed by reference, not copied, so the self-executing function call should make no difference. The output of the two code snippets should be identical. So why is the i object being copied 10 times? Why does it work?

I think it's interesting that this version doesn't work:

for(var i=0; i<10; i++) {
    (function () {
        $("#img" + i).click(
            function () { alert(i); }
        );
    })();
}

It seems that the passing of the object as a function parameter makes all the difference.


EDIT: OK, so the previous example can be explained by primitives (i) being passed by value to the function call. But what about this example, which uses real objects?

for(var i=0; i<5; i++) {
    var toggler = $("<img/>", { "src": "http://www.famfamfam.com/lab/icons/silk/icons/cross.png" });
    toggler.click(function () { toggler.attr("src", "http://www.famfamfam.com/lab/icons/silk/icons/tick.png"); });
    $("#container").append(toggler);
}

Not working: http://jsfiddle.net/Zpwku/

for(var i=0; i<5; i++) {
    var toggler = $("<img/>", { "src": "http://www.famfamfam.com/lab/icons/silk/icons/cross.png" });
    (function (t) {
        t.click(function () { t.attr("src", "http://www.famfamfam.com/lab/icons/silk/icons/tick.png"); });
        $("#container").append(t);
    })(toggler);
}

Working: http://jsfiddle.net/YLSn6/

like image 945
Rag Avatar asked Nov 22 '12 05:11

Rag


People also ask

Are objects references in JavaScript?

Objects in JavaScript are passed by reference. When more than one variable is set to store either an object , array or function , those variables will point to the same allocated space in the memory.

How do you call an object in JavaScript?

The call() method is a predefined JavaScript method. It can be used to invoke (call) a method with an owner object as an argument (parameter). With call() , an object can use a method belonging to another object.


2 Answers

Most of the answers are correct in that passing an object as a function parameter breaks a closure and thus allow us to assign things to functions from within a loop. But I'd like to point out why this is the case, and it's not just a special case for closures.

You see, the way javascript passes parameters to functions is a bit different form other languages. Firstly, it seems to have two ways of doing it depending on weather it's a primitive value or an object. For primitive values it seems to pass by value and for objects it seems to pass by reference.

How javascript passes function arguments

Actually, the real explanation of what javascript does explains both situations, as well as why it breaks closures, using just a single mechanism.

What javascript does is actually it passes parameters by copy of reference. That is to say, it creates another reference to the parameter and passes that new reference into the function.

Pass by value?

Assume that all variables in javascript are references. In other languages, when we say a variable is a reference, we expect it to behave like this:

var i = 1;
function increment (n) { n = n+1 };
increment(i); // we would expect i to be 2 if i is a reference

But in javascript, it's not the case:

console.log(i); // i is still 1

That's a classic pass by value isn't it?

Pass by reference?

But wait, for objects it's a different story:

var o = {a:1,b:2}
function foo (x) {
    x.c = 3;
}
foo(o);

If parameters were passed by value we'd expect the o object to be unchanged but:

console.log(o); // outputs {a:1,b:2,c:3}

That's classic pass by reference there. So we have two behaviors depending on weather we're passing a primitive type or an object.

Wait, what?

But wait a second, check this out:

var o = {a:1,b:2,c:3}
function bar (x) {
    x = {a:2,b:4,c:6}
}
bar(o);

Now see what happens:

console.log(o); // outputs {a:1,b:2,c:3}

What! That's not passing by reference! The values are unchanged!

Which is why I call it pass by copy of reference. If we think about it this way, everything makes sense. We don't need to think of primitives as having special behavior when passed into a function because objects behave the same way. If we try to modify the object the variable points to then it works like pass by reference but if we try to modify the reference itself then it works like pass by value.

This also explains why closures are broken by passing a variable as a function parameter. Because the function call will create another reference that is not bound by the closure like the original variable.

Epilogue: I lied

One more thing before we end this. I said before that this unifies the behavior of primitive types and objects. Actually no, primitive types are still different:

var i = 1;
function bat (n) { n.hello = 'world' };
bat(i);
console.log(i.hello); // undefined, i is unchanged

I give up. There's no making sense of this. It's just the way it is.

like image 119
slebetman Avatar answered Sep 28 '22 13:09

slebetman


It's because you are calling a function, passing it a value.

for (var i = 0; i < 10; i++) {
    alert(i);
}

You expect this to alert different values, right? Because you are passing the current value of i to alert.

function attachClick(val) {
    $("#img" + val).click(
        function () { alert(val); }
    );
}

With this function, you'd expect it to alert whatever val was passed into it, right? That also works when calling it in a loop:

for (var i = 0; i < 10; i++) {
    attachClick(i);
}

This:

for (var i = 0; i < 10; i++) {
    (function (val) {
        $("#img" + val).click(
            function () { alert(val); }
        );
    })(i);
}

is just an inline declaration of the above. You are declaring an anonymous function with the same characteristics as attachClick above and you call it immediately. The act of passing a value through a function parameter breaks any references to the i variable.

like image 23
deceze Avatar answered Sep 28 '22 13:09

deceze