Javascript Closures - What are the negatives?
Closures bring a lot of benefits...but also a number of gotchas. The same thing that makes them powerful also makes them quite capable of making a mess if you're not careful.
Besides the issue with circular references (which isn't really as much of a problem anymore, since IE6 is hardly used at all outside of China), there's at least one other huge potential negative: They can complicate scope. When used well, they improve modularity and compatibility by allowing functions to share data without exposing it...but when used badly, it can become difficult if not impossible to trace exactly where a variable is set or changed.
JavaScript without closures has three* scopes for variables: block-level, function-level, and global. There is no object-level scope. Without closures, you know a variable is either declared in the current function, or in the global object (because that's where global variables live).
With closures, you no longer have that assurance. Each nested function introduces another level of scope, and any closures created within that function see (mostly) the same variables as the containing function does. The big problem is that each function can define its own variables at will that hide the outer ones.
Using closures properly requires that you (a) be aware of how closures and var
affect scope, and (b) keep track of which scope your variables are in. Otherwise, variables can be accidentally shared (or pseudo-variables lost!), and all sorts of wackiness can ensue.
Consider this example:
function ScopeIssues(count) {
var funcs = [];
for (var i = 0; i < count; ++i) {
funcs[i] = function() { console.log(i); }
}
return funcs;
}
Short, straightforward...and almost certainly broken. Watch:
x = ScopeIssues(10);
x[0](); // outputs 10
x[1](); // does too
x[2](); // same here
x[3](); // guess
Every function in the array outputs count
. What's going on here? You're seeing the effects of combining closures with a misunderstanding of closed-over variables and scope.
When the closures are created, they're not using the value of i
at the time they were created to determine what to output. They're using the variable i
, which is shared with the outer function and is still changing. When they output it, they're outputting the value as of the time it is called. That will be equal to count
, the value that caused the loop to stop.
To fix this before let
existed, you'd need another closure.
function Corrected(count) {
var funcs = [];
for (var i = 0; i < count; ++i) {
(function(which) {
funcs[i] = function() { console.log(which); };
})(i);
}
return funcs;
}
x = Corrected(10);
x[0](); // outputs 0
x[1](); // outputs 1
x[2](); // outputs 2
x[3](); // outputs 3
As of ES7, you can use let
instead of var
, and each iteration of the loop will basically get its own version of i
.
function WorksToo(count) {
var funcs = [];
for (let i = 0; i < count; ++i) {
funcs[i] = function() { console.log(i); }
}
return funcs;
}
x = WorksToo(10);
x[0](); // outputs 0
x[1](); // outputs 1
x[2](); // outputs 2
x[3](); // outputs 3
But that comes with complications of its own -- variables with the same name and purpose, in the same block of code, are now effectively disconnected. So you don't want to just always use let
either. The only real fix is to all-around be much more aware of scope.
Another example:
value = 'global variable';
function A() {
var value = 'local variable';
this.value = 'instance variable';
(function() { console.log(this.value); })();
}
a = new A(); // outputs 'global variable'
this
and arguments
are different; unlike nearly everything else, they are not shared across closure boundaries?. Every function call redefines them -- and unless you call the function like
obj.func(...)
,func.call(obj, ...)
,func.apply(obj, [...])
, orvar obj_func = func.bind(obj); obj_func(...)
to specify a this
, then you'll get the default value for this
: the global object.^
The most common idiom to get around the this
issue is to declare a variable and set its value to this
. The most common names i've seen are that
and self
.
function A() {
var self = this;
this.value = 'some value';
(function() { console.log(self.value); })();
}
But that makes self
a real variable, with all the potential oddness that entails. Fortunately, it's rare to want to change the value of self
without redefining the variable...but within a nested function, redefining self
of course redefines it for all the functions nested within it as well. And you can't do something like
function X() {
var self = this;
var Y = function() {
var outer = self;
var self = this;
};
}
because of hoisting. JavaScript effectively moves all the variable declarations to the top of the function. That makes the above code equivalent to
function X() {
var self, Y;
self = this;
Y = function() {
var outer, self;
outer = self;
self = this;
};
}
self
is already a local variable before outer = self
runs, so outer
gets the local value -- which at this point, is undefined
. You've just lost your reference to the outer self
.
* As of ES7. Previously, there were only two, and variables were even easier to track down. :P
? Functions declared using lambda syntax (new to ES7) don't redefine this
and arguments
. Which potentially complicates the matter even more.
^ Newer interpreters support a so-called "strict mode": an opt-in feature that aims to make certain iffy code patterns either fail entirely or cause less damage. In strict mode, this
defaults to undefined
rather than the global object. But it's still some whole other value than you usually intended to mess with.
You may get a raft of good answers. One certain negative is the Internet Explorer circular reference memory leak. Basically, "circular" references to DOM objects are not recognized as collectible by JScript. It's easy to create what IE considers a circular reference using closures. Several examples are provided in the second link.
- Microsoft KB Article re IE6 Memory Leak
- Mitigation Efforts in Later Versions
In IE6, the only way to reclaim the memory is to terminate the whole process. In IE7 they improved it so that when you navigate away from the page in question (or close it), the memory is reclaimed. In IE8, DOM objects are better understood by JScript and are collected as you'd expect they should be.
The suggested workaround for IE6 (besides terminating the process!) is not to use closures.