Other great resources: Official JS API docs, Scripting Forum
Scopes & Closures
- Aubergine
Â
Scope
All variables and constants in Javascript are stored in a "scope". I'll explain why it's called a "scope" later.
There's a "global scope", that everything can see:
var str1 = "global";
The variable "str1" is defined in the "global scope" and has a value of "global".
Closures
Closures are functions.
Each closure has it's own local scope.
var str1 = "global"; Â function foo() { var str2 = "foo"; }
The variable "str2" is defined in the foo() closure's scope (the "foo scope") and has a value of "foo".
The "str2" variable can't be seen from the global scope, it can only be seen inside the foo() closure (the foo scope):
var str1 = "global"; Â function foo() { var str2 = "foo"; console(str2); // outputs "foo" } Â console(str2); // error - we can't see str2 from here
Scope chain
A closure can see it's own scope (eg. foo() can see the foo scope) and also the scopes of it's parents (eg. the global scope):
var str1 = "global"; Â function foo() { var str2 = "foo"; console(str1); // outputs "global" console(str2); // outputs "foo" } Â console(str1); // outputs "global" console(str2); // error - we can't see str2 from here
Inside the foo() closure, when "str1" is referenced Javascript looks first at scope of the closure (the foo scope). If it doesn't find what it's looking for, it looks at the parent scope which in this case is the global scope.
Inside foo(), the scope chain looks like this:
foo scope
↓
global scope
The foo scope contains "str2" and the global scope contains "str1".
Nested closures
We normally use variables to store things like numbers, arrays, strings or objects. But they can store closures (functions) as well.
var str1 = "global"; Â function foo() { var str2 = "foo"; console(str1); // outputs "global" console(str2); // outputs "foo" console(str3); // error - we can't see str3 from here var bar() = function() { var str3 = "bar"; console(str1); // outputs "global" console(str2); // outputs "foo" console(str3); // outputs "bar" } } Â console(str1); // outputs "global" console(str2); // error - we can't see str2 from here console(str3); // error - we can't see str3 from here
Inside bar(), the scope chain looks like this:Â
bar scope
↓
foo scope
↓
global scope
The bar scope contains "str3", the foo scope contains "str2" and the global scope contains "str1".
Overriding variables
As mentioned earlier, Javascript will search the scope chain to find a variable. It stats with the local scope (eg. the bar scope if it's search starts from inside the bar() closure) and then looks in the scope of each parent in turn, until it finds what it's looking for. This means that variables can be overriden within closures:
var str1 = "global";  function foo() {  var str1 = "foo global"; // we're overriding str1 in the foo scope  var str2 = "foo"; console(str1); // outputs "foo global" (overriden variable) console(str2); // outputs "foo" console(str3); // error - we can't see str3 from here var bar() = function() { var str3 = "bar"; console(str1); // outputs "foo global" (overriden variable) console(str2); // outputs "foo" console(str3); // outputs "bar" } }  console(str1); // outputs "global" (original variable) console(str2); // error - we can't see str2 from here console(str3); // error - we can't see str3 from here
By overriding "str1" inside the foo scope, we affect the output both within foo() and bar(). But note how in the global scope "str1" remains unchanged. That's because when we run some code at global level Javascript looks at the local scope, which is the global scope in this case, and sees the original variable. When we run code inside the foo() or bar() closures, Javascript finds the "str1" that's defined in the foo scope and uses that, it doesn't get as far as looking at the global scope.
Practical uses
You can use closures like a little private storage boxes. It helps keep things tidy.
Controlling what goes in savegame files
One reason to do this is to control what gets stored in savegame files. When a game is saved, the JS API will gather up all variables defined in the global scope and put them in to the savegame file. If you store your variables inside a closure, however, the JS API can't see them and won't store them in the savegame.
In the examples above, only the "str1" on the global scope (it's value is "global") will get stored in the savegame file.
Providing different functions based on API version
As mentioned earlier, variables can store other closures (functions) and this opens up some very powerful approaches to dealing with different API versions.
A good example of this can be found in getThreatsNear(x,y[,range]). In more recent versions of the JS API there's an enumRange() function that lets you get a list of enemy objects within range of a specific map position. But in earlier versions you'd have to take a more manual approach to getting that information, by building lists of objects for each enemy and then filtering to those within a specific range of the map position.
Now, you could just create two functions like this:
function oldApproach(x,y,range) { // do stuff the hard way } function newApproach(x,y,range) { // use enumRange() }
But there's problems with that approach:
- Both functions are available regardless of API version – it's quite easy to use the wrong one.
- To avoid using the wrong one, you need to work out which function to use every single time – that's crufty and slow
- If, at a later date, an new API feature provides an even better way to do things (superAwesomeLatestApproach) you'd have to go and update all places in your code that check what function to use
To get round this problem, we can define the two functions in a closure and have the closure output the right function depending on whether the enumRange() function is available or not:
function getThreatsNear(x,y,range) { Â var oldApproach = function(x,y,range) { // do stuff the hard way } var newApproach = function(x,y,range) { // use enumRange() } Â if (typeof enumRange != "undefined") { return newApproach(x,y,range); } else { return oldApproach(x,y,range); } Â } Â // then in your code use this: var threats = getThreatsNear(someX,someY,someRange);
This is a lot better because:
- It's no longer possible to use the wrong function because nothing outside getThreatsNear() can access the oldApproach() or newApproach() functions
- The rest of your code doesn't need to worry about which function to use (it's handled by the "if .. else" statement in getThreatsNear()
- If superAwesomeNewerApproach becomes available, we can deal with it in one single location - although it will make the "if ... else" statement more crufty
But even this approach is still somewhat inefficient. Every time you call getThreatsNear() it has to do that if statement. While it's not a lot of processing, it still seems wasteful to have to do that typeof check every single time the function is called. If you have lots of places doing that sort of thing, and you run that code over and over again, it all starts to add up to cause performance problems.
We can solve that problem like this:
var getThreatsNear = (function(){  var oldApproach = function(x,y,range) { // do stuff the hard way } var newApproach = function(x,y,range) { // use enumRange() }  // note we're returning a function, not running it (no "()" on the end)... if (typeof enumRange != "undefined") { return newApproach; } else { return oldApproach; }  })(); // <-- "()" here means we're running the anonymous function  // then in your code use this: var threats = getThreatsNear(someX,someY,someRange);
So how does that work?
We've created an anonymous, self-running function - here's a simplified version of the code above:
var getThreatsNear = (function(){ // stuff })();  // here's an even more simplified version: var getThreatsNear = (anonymousFunction)();  // which is equivalent to: var getThreatsNear = anonymousFunction();
We've not named the function, so it won't get stored anywhere. If it's not stored anywhere, how do we call it? It won't exist any more once that bit of code has run. To get round the problem we wrap the anonymous function in brackets, which makes Javascript compile it (not exactly true but easier to think of it that way) and then call it (that's what the "()" at the end are for).
The anonymous function internally defines two variables - oldApproach and newApproach. The values of those variables are functions, one that does things the old crufty way and one that takes advantage of the newer way using enumRange. It then returns one of those variables depending on whether the enumRange() function exists in the JS API. The variable "getThreatsNear" gets set to the return value of the anonymous function, and that return value is actually a function (either oldApproach or newApproach).
From that point onwards, when we call getThreatsNear() we're directly calling the right function - there's no "if ... else" statement being called because that was done earlier when we chose what variable to return from the anonymous function.
See also
- Closures – a Mozilla Developer Network (MDN) article which gives a good introduction to closures
- Functions and Function Scope – another MDN article that goes in to a bit more detail
Contents
Assimilate:
Â