Smithy
Smithy

Reputation: 43

Javascript closures and global variables

I am just learning how closures work and started playing with some code. To my understanding closures are some kind of memory which saves the environment of the function at that state the closure was created and is still alive (when returned or bound to an event) even when the parent function has ended.

So I tried the code below which is working as expected. Two buttons which switch their corresponding textboxes from "check" to "saved".

//var v1;
//var v2;
//var v3;

function func1(src, action, arg) {
  document.getElementById(arg).value = "check";
  document.getElementById(src.id).onclick = function() {
    func2(src, action, arg);
  };
  //v1 = src;
  //v2 = action;
  //v3 = arg;
}

function func2(v1, v2, v3) {
  document.getElementById(v3).value = "saved";
  document.getElementById(v1.id).onclick = function() {
    func1(v1, v2, v3);
  };
}
<input type=text id="t1">
<input type=button id="btn1" value="Go 1" onclick="func1(this, 'edit', 't1')">
<input type=text id="t2">
<input type=button id="btn2" value="Go 2" onclick="func2(this, 'edit', 't2')">

But now comes the confusion. When I use globals to construct the closure in func2() the switches bug around. See code below:

var v1;
var v2;
var v3;

function func1(src, action, arg) {
  document.getElementById(arg).value = "check";
  document.getElementById(src.id).onclick = function() {
    func2();
  };
  v1 = src;
  v2 = action;
  v3 = arg;
}

function func2() {
  document.getElementById(v3).value = "saved";
  document.getElementById(v1.id).onclick = function() {
    func1(v1, v2, v3);
  };
}
<input type=text id="t1">
<input type=button id="btn1" value="Go 1" onclick="func1(this,'edit','t1')">
<input type=text id="t2">
<input type=button id="btn2" value="Go 2" onclick="func2(this,'edit','t2')">

Click on Go1 -> Textbox1 = check; Click on Go2 -> Textbox2 = check; But now click on Go1 -> Textbox2 (instead of 1) = saved.

So it seems that the variables v1, v2, v3 in the closure still uses the global values outside the closure. Can somebody please explain why and what to do so it works with globals?


Thanks to T.J.Crowder I updated my code to use private variables in the closures. But unfortunately it still does not work. Same behaviour like the second code block.

var v1;
var v2;
var v3;

function func1(src, action, arg) {
  document.getElementById(arg).value = "check";
  document.getElementById(src.id).onclick = function() {
    func2();
  };
  v1 = src;
  v2 = action;
  v3 = arg;
}

function func2() {
  var private1 = v1;
  var private2 = v2;
  var private3 = v3;

  document.getElementById(private3).value = "saved";
  document.getElementById(private1.id).onclick = function() {
    func1(private1, private2, private3);
  };
}
<input type=text id="t1">
<input type=button id="btn1" value="Go 1" onclick="func1(this,'edit','t1')">
<input type=text id="t2">
<input type=button id="btn2" value="Go 2" onclick="func2(this,'edit','t2')">

Upvotes: 0

Views: 3423

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074148

So it seems that the variables v1, v2, v3 in the closure still uses the global values outside the closure

Right, because nothing is shadowing them, so that's how they end up getting resolved.

In your first code block, the onclick handlers are using the src, action, and arg arguments supplied in the call to func1 and the v1, v2, and v3 arguments supplied in the call to func2.

In the second example, since you removed the arguments to func2, the only reason those identifiers are in scope for the onclick function it creates is because of the globals; if it weren't for the globals, you'd be getting ReferenceErrors because those identifiers would be unresolvable. So it's the globals that get used.

If you passed arguments to func2 in your second example that had the same names (not a great idea, but just for the sake of explanation), then the identifiers used by the onclick closure it creates would resolve against those arguments instead of against the globals.

It may be useful to explain what this line is doing:

document.getElementById(v1.id).onclick = function () { func1(v1,v2,v3); };

that line creates a function, which is a closure over the context in which it was created, the call to func2 (and that context refers to the context in which it was created, and so on up to the global context). The onclick closure doesn't have a copy of the variables that were in scope when it was created, it has an enduring reference to them. So when the function gets run, it's the value of the variables at that moment that gets used. In your first example, those values never change, because they're the values of the arguments passed to func2 during the call that created the closure, and nothing ever changes those. In your second example, though, those values are different because you're using the global variables, not the arguments passed to func2.

Because the closure has a reference to the context where it was created, the context of each call to func2 is preserved by the closure created within that call. So if the closures created by multiple calls are kept, multiple contexts are kept for the calls to func2, and so multiple copies of the arguments are kept (each used by the closure created for that context). A simpler example may help:

// A global variable
var global = "g";

// A function that creates and returns a closure
function foo(arg) {
    return function() {
        snippet.log("global = " + global +) ", arg = " + arg);
    };
}

// Create a closure over arg = 42
var f1 = foo(42);
f1(); // global = g, arg = 42

// Change global
global = "g+";
f1(); // global = g+, arg = 42

// Create a second closure over a second arg, 67
var f2 = foo(67);
f2(); // global = g+, arg = 67

// Change global again
global = "g++";
f1(); // global = g++, arg = 42
f2(); // global = g++, arg = 67
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Note how each closure has its own copy of arg, but they both share global.

This snippet demonstrates that what the closure has is a reference to arg, not a copy of its value:

function foo(arg) {
  return {
    showArg: function() {
      console.log("arg = " + arg);
    },
    incrementArg: function() {
      ++arg;
    }
  };
}
var o1 = foo(42);
o1.showArg(); // arg = 42
o1.incrementArg();
o1.showArg(); // arg = 43
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

You may find this article on my anemic little blog useful: Closures are not complicated


Here's a snippet that demonstrates capturing the then-current globals in the closure calling func2, but we end up just duplicating what we already have in src, action, and arg, so there's no point.

var v1;
var v2;
var v3;

function func1(src, action, arg) {
  var p1 = src, p2 = action, p3 = arg; // Just duplicates what we already have
  document.getElementById(arg).value = "check";
  document.getElementById(src.id).onclick = function() {
    func2(p1, p2, p3); // We could just use src, action, and arg here like your first example
  };
  v1 = src;
  v2 = action;
  v3 = arg;
}

function func2(v1, v2, v3) {
  document.getElementById(v3).value = "saved";
  document.getElementById(v1.id).onclick = function() {
    func1(v1, v2, v3);
  };
}
<input type=text id="t1">
<input type=button id="btn1" value="Go 1" onclick="func1(this,'edit','t1')">
<input type=text id="t2">
<input type=button id="btn2" value="Go 2" onclick="func1(this,'edit','t2')">

Upvotes: 2

Related Questions