Understanding the scope of variable and function, class in programming clearly is key to appreciating how your variables interact with the rest of your source code. In many languages, this can be quite straightforward, but JavaScript’s anonymous functions and event handling features, along with a little of quirks, mean that handling scope in your JavaScript applications can become frustrating.
This article discusses how JavaScript handles scope and how various JavaScript libraries provide methods for dealing with it and how they smooth out a few bumps. We’ll also look at how you can get back to basics and do some interesting scope wrangling without a library, a useful approach if you’re writing code that needs to stand alone.
- Demo
- Enlarge
- Reload
- New window
Generate your business videos by AI with voice or just text
Your first FREE AI Video App! Automate Your First AI Video. Create Your Professional Video In 5 Minutes By AI No Equipment Or Video Editing Skill Requred. Effortless Video Production For Content Marketers.
Understanding scope in programming is key to appreciating how your variables interact with the rest of your code. In some languages, this can be quite straightforward, but JavaScript's anonymous functions and event handling features, along with a couple of little quirks, mean that handling scope in your applications can become frustrating.
This article discusses how JavaScript handles scope and how various JavaScript libraries provide methods for dealing with it and how they smooth out a few bumps. We'll also look at how you can get back to basics and do some interesting scope wrangling without a library, a useful approach if you're writing code that needs to stand alone.
You Are Here
So what is "scope"? We might say that it refers to your current location. If you run some JavaScript like...
var iAmGlobal = 5 * 5;
... then you're running in the global scope, the big wide world, where you can't go any further out. For something like...
function doSomething() { var inner = 5 * 5; };
... you're now boxed in by this function, running within its scope. The phrase "boxed in" is appropriate; take a look at this code:
var g = "global"; function go() { var l = "local"; } go(); alert(l); // throws a reference error
You'll see that when we run the go
function, the l
variable is contained within that function's scope. It cannot be accessed from a higher level scope.
How it Works
As well as variable scope, JavaScript uses the this
keyword to get a reference to the current execution context. That
rather terrifying term boils down this: at any point in your JavaScript
code, you can ask "Help! Where am I?" and get back an object reference.
This reference is for the current context, the object that "owns" the
currently executing code.
Now, you might think, given what we've just learned about scope, the owner of the current code would be the scope in which it is executed. After all, in JavaScript, even functions are objects and can be passed around in variables. But no. Take this function, for instance:
function go() { console.debug(this); } go();
This gives you a reference to the top-level execution context; in a browser, that's the browser window itself.
There are a few exceptions to this. For example, if we create a JavaScript object and then call a method on it, then the scope is bound to the object:
var myObject = { go: function() { console.debug(this); } }; myObject.go(); // console.debugs a reference to myObject
Similarly, when using functions as constructors, you see the same behavior:
function MyClass() { this.go = function() { console.debug(this); } } var instance1 = new MyClass(); var instance2 = new MyClass(); instance1.go(); // console.debugs a reference to the MyClass instance1 instance2.go(); // console.debugs a reference to the MyClass instance2
However, notice in this case that the reference is to the individual object instance rather than the class definition, which contrasts with the previous object literal example in which we will always receive a reference to the same object.
With event handlers, things get a little more confusing. If you specify an event handler inline in HTML, then you end up with it referencing the global window object. However, if you use JavaScript to wire your events, then you get a reference to the DOM object that raised it; for example, a click handler on a button would have the button element as the reference.
Event handlers are a common situation in which you would want to bind a function to a different scope; many JavaScript libraries provide features to help do just that. Let's take a look at some common options.
Libraries
Many developers use JavaScript libraries to avoid having to deal with browser inconsistencies and to take advantage of the many shortcuts they offer. Scope handling is something most libraries give a helping hand with, so let's take a look at what a few of the major players offer.
Prototype
Prototype comes with a bind method that allows a developer to specify the bound context for a function.
var products = ['Shoes', 'Sweater', 'Jeans', 'Wig']; function showCount() { for(var i = 0; i < number; i++) { document.body.innerHTML += this[i] + '. '; } } var fn = showCount.bind(products); fn(2); // outputs Shoes. Sweater. to the document
It also supports passing arguments that are "remembered" when you call the function, and these can be used to create shortcut functions; basically a version of a function that defaults to passing in certain arguments:
var showOne = showCount.bind(products, 1); var showFour = showCount.bind(products, 4); showOne(); // outputs Shoes. showFour(); // output Shoes. Sweater. Jeans. Wig.
See Prototype's Function.curry
for more information on this particular aspect of Function.bind
. The second useful feature of Prototype's scope handling is bindAsEventListener
. This is very similar to bind
but ensures that the first argument passed to the event handler is the event object.
Event.observe( $('showCountButton'), 'click', showCountHandler.bindAsEventListener(products, 2) );
Here we're using Prototype's event functions to set up an event listener when the showCountButton
is clicked. We're passing our products
array as the context, which the function is bound to, but in this case the showCountHandler
would look something like this:
function showCountHandler(e, number) { for(var i = 0; i < number; i++) { document.body.innerHTML += this[i] + '. '; } Event.stop(e); }
So we have the products
array as this
, but we also have the e
event object automatically passed as the first parameter, which we can later use to stop the default event.
The two Prototype methods for binding context are handy because they're used in exactly the same way, so you have a very simple and consistent method of taming your context.
Ext JS
Ext JS is farther reaching than either Prototype or MooTools in that it provides a full end-to-end framework for UI and application creation. This means it also provides correspondingly more features to control scope. To compare it with Prototype, let's look at how to bind to a particular context:
var fn = showCount.createDelegate(products, 4);
This is identical in usage to Prototype's bind method. But is there a difference when dealing with event handlers?
Ext.get('showCountButton').on('click', showCountHandler.createDelegate(products, 4) );
That's right: there is no difference. Ext JS will normalize the event object into an Ext.EventObject
for you and then append your additional arguments after that. However,
there are two caveats to this. First, Ext doesn't just pass the event
object to the handler, but also passes the source of the event (in this
case, the showCountButton
) and any options that were passed to the on
method. So, our handler now looks like this:
function showCountHandler(e, source, options, number) {}
However, there is a shortcut to using createDelegate
, and it involves understanding the arguments of the on
method. We can do this like so:
Ext.get('showCountButton').on('click', showCountHandler, products, { number: 4 });
The third argument of on
is the scope under which the handler should run, which eliminates the need to use createDelegate
. However, in order to pass further parameters, we have to use the options
parameter. So our handler in this case would be:
function showCountHandler(e, source, options) { number = options.number; }
This is not quite as elegant on the handler side of things, but it's useful to know that Ext JS provides a variety of methods for accomplishing similar things, and you can use them accordingly when building your applications.
MooTools
The MooTools library provides two methods that are essentially like replacements for Prototype versions: bind
and bindWithEvent
, a.k.a. bindAsEventListener
. However, on top of these familiar features, it provides a couple more that lend some extra flexibility. My favorite is Function.create
:
var fn = showCount.create({ bind: products, arguments: 4 });
This is nice and succinct, and to turn this into an event handler, we do this:
showCount.create({ bind: products, arguments: 4, event: true });
We can pass additional options, such as delay
, which defers the execution of the function by a specified number of milliseconds, and periodical
, which fires the function every time the specified interval elapses.
One library conspicuous in its absence is jQuery, which doesn't offer any context binding facility. But JavaScript does have built-in features that allow you to manage context in many scenarios, and it also provides relatively simple methods of building your own solutions to more complicated problems.
On Your Own
I'm no snob: leveraging the hard work of the great developers who have spent a lot of time on their libraries makes total sense. They will have worked through all of the bugs and edge cases so that you don't have to. On the other hand, understanding what's happening on the JavaScript level is important, not only as an academic exercise but also for those occasions when you can't rely on a library.
Sometimes offering standalone and library-independent scripts is best; for example, if you would like to make your code available publicly and for widespread use. By relying on a library, you restrict the use of the code to people who use that library.
Let's take a look at how scope and context can be handled without using a library.
Call and Apply
JavaScript functions have two methods available to them that are of particular interest for handling context. Let's look at call
:
showCount.call(products, 4);
Apply
is very similar but is used when you don't know
how many arguments you will be passing. It takes an array as its second
parameter:
showCount.apply(products, [4]);
Both of these achieve the same goal, but your usage case will determine which would work best for you.
Event Handler Scope
We saw in the explanations of scope how event handlers cause problems, and we also saw how the various JavaScript libraries provide means of getting around this. If you're stuck with bare-bones JavaScript, then you simply have to write your own means of scoping event handlers, and we'll look at how to do that now.
Call
and apply
trigger the function
immediately: that's not what we're after. Instead, we want to return a
new function, which will then be called when the event fires. So:
Function.prototype.bindContext = function() { // when adding functions using prototype, "this" is the // object which the new function was called on var callingFunction = this; // pass the desired scope object as the first arg var scope = arguments[0]; // create a new arguments array with the first arg removed var otherArgs = []; for(var i = 1; i < arguments.length; i++){ otherArgs.push(arguments[i]); } // return a function remembering to include the event return function(e) { // Add the event object to the arguments array otherArgs.push(e || window.event); // Array is in the wrong order so flip it otherArgs.reverse(); // Now use apply to set scope and arguments callingFunction.apply(scope, otherArgs); } }
This is a basic implementation with no error handling, but it provides a useful base to expand on and for understanding the overall approach. Dealing with event handler scope is essential for most JavaScript applications, and no developer should be tied to a single framework, so an appreciation for handling this problem at a low level is useful for every coder.
Conclusion
When building any large JavaScript application, a solid understanding of scope is not only useful but pretty much necessary. While using a common JavaScript library is a useful shortcut, it's certainly never bad to get back to basics and roll your own solution in order to gain more control of JavaScript scope.
Further Resources
- An introduction to scope in Dojo.
- A discussion of YUI that touches on scope within event handlers.
- Scope from a C# and object-oriented perspective.
- A huge technical reference on scope and closures in JavaScript.
- Interesting scope "gotcha."
- Sent (0)
- New