Friday, September 16, 2005

Binding Scope in JavaScript

Binding Scope in JavaScript

JavaScript as a language has a reputation of being both flexible and quirky. One such area of its flexibility is in how functions can be assigned, nested and returned just like any other data-type. However, as functions can execute code while nested many objects and functions deep, keeping track of scope becomes very important. But, scope in JavaScript can be pretty loose. For this reason, keeping a function’s scope consistent can be tricky. The good news is, once you know what to look for and understand how to use some of the tools JavaScript offers, keeping scope bound to the data you want is actually very easy.

Starting Simple

Lets start off by making a simple script to test the scope of a function using the Firebug console.

window.name = "window";

action = function(greeting) {
  console.log(greeting + " " + this.name);
}

action("hello");

// hello window

What is Firebug?

Firebug is a Firefox plugin that offers a wealth tools for JavaScript developers. The specific tool we will be using throughout this tutorial is the JavaScript console, so we can get instant feedback on what our functions are up to.

In the above example, we assigned a variable to the global window object called name. We then defined a function in the window scope that will log a string to the console and then we gave it a run. The line of most interest is where we used the this variable. As our function is within the global window scope, this resolves to the window object, and as such, we can get to our namevariable and concatenate it to our greeting string.

This pattern works well until we have a lot of functions and variables floating around our program. Once this happens, we can employ objects in JavaScript to act as containers.

window.name = "window";

object = {
  name: "object",
  
  action: function(greeting) {
    console.log(greeting + " " + this.name);
  }
}

object.action("hello");

// hello object

In this example, we neatly packaged our name variable and action function into an object literal. We also see that the value of this changed to the containing object, which is no longer windowbut object. This is quite useful as we can keep a set of variables and functions abstracted into one namespace. All is looking well and good, until our function gets a bit more complex and JavaScript loses our scope.

Losing Scope and Getting it Back

Our function requires a bit more logic, as many functions do. To handle this, we will nest a function within our action function called nestedAction. It is here when things go awry.

window.name = "window";

object = {
  name: "object",
  
  action: function() {
    nestedAction = function(greeting) {
      console.log(greeting + " " + this.name);
    }
    
    nestedAction("hello");
  }
}

object.action("hello");

// hello window

Hmm, our nestedAction function is indeed contained within object, however this is instead pointing to window. This is due to how we are executing the nestedAction function. In JavaScript, scope is resolved during a functions execution. Although our action function is defined and executed within object, we are telling JavaScript to run it within the window scope. As such, the nestedAction function is actually executing globally even though it is defined locally within the action function.

In short, Once we executed a function nested within a function, JavaScript lost our scope and is defaulting to the best thing it can get, window. To get our scope back, JavaScript offers us two useful functions, call and apply.

object = {
  name: "object",

 action: function() {

   nestedAction = function(greeting) {

     console.log(greeting + " " + this.name);

   }

    

     nestedAction.call(this, "hello");
    nestedAction.apply(this, ["hello"]);
  }
}

object.action("hello");

// hello object
// hello object

That is more like it. By using call and apply, we are able to specify what scope ournestedAction should resolve to. The call and apply functions are sent to a function and carry two parameter types, the scope the function should resolve to and the parameters it should use at run-time. You will notice that the scope we are wiring the nestedAction function to resolve to isthis. We can do so as this resolves to object until we nest our code another function deeper.

You will also notice that call and apply perform exactly the same, except for one key difference. The call function first accepts the object to resolve scope to, and then the parameters to be passed to the function. On the other hand, apply accepts the object to resolve scope to and an array of parameters to be passed to the function. This difference is noteworthy, as one interface may be easier to implement into our code then another.

So, how flexible are call and apply at managing scope in JavaScript? Very flexible, as we can see with the following code:

window.name = "the window";

alice = {
  name: "Alice"
}

eve = {
  name: "Eve",
  
  talk: function(greeting) {
    console.log(greeting + ", my name is " + this.name);
  }
}

eve.talk("yo");
eve.talk.apply(alice, ["hello"]);
eve.talk.apply(window, ["hi"]);

// yo, my name is eve
// hello, my name is alice
// hi, my name is the window

In this example, the nefarious Eve is able to impersonate both Alice and the window object by passing apply to her talk function. Between the this and the previous example, we can definitely see how call and apply can be used to help JavaScript keep hold of scope during operation. However, we run into a serious issue with this technique. As we are still determining scope at execution time, we must execute the function before their scope is resolved. This becomes problematic if we want to pass functions around to be executed later, scope intact.

Binding Scope

A very handy addition to tag along with the Prototype JavaScript library is bind. The concept is simple, would it not be useful to hard-wire a function’s scope at definition instead of execution? Well, with a little bit of JavaScript hackery, we can build our own bind function and do just that. Take a look:

Function.prototype.bind = function(scope) {
  var _function = this;
  
  return function() {
    return _function.apply(scope, arguments);
  }
}

alice = {
  name: "alice"
}

eve = {
  talk: function(greeting) {
    console.log(greeting + ", my name is " + this.name);
  }.bind(alice) // <- bound to "alice"
}

eve.talk("hello");

// hello, my name is alice

The above example uses the same technique as Prototype, albeit syntactically different. What we are doing is leveraging JavaScript’s flexible interpretation of functions by wrapping our talkfunction into another function that is properly scoped. The explanation is as follows:

  1. We first define a new prototype to the Function class called bind, which takes scope as a parameter.
  2. As we are working with a function, this within our bind definition function will resolve to the function being sent the bind message.
  3. We then return a function, that when executed, will actually execute the original function while applying the scope specified with the arguments passed.
  4. Finally, we can cleanly bind our talk function to the appropriate scope at its definition and call it normally at its execution.

In this example, we can see how apply is a better fit then call. As JavaScript has a special variable called arguments which acts like an Array, we can pass it to apply and keep our function parameters intact. Now that we have a method for hard-wiring scope on definition, we can return to our previous problem and make it work how we anticipated.

Function.prototype.bind = function(scope) {
  var _function = this;
  
  return function() {
    return _function.apply(scope, arguments);
  }
}

object = {
  name: "object",
  
  action: function() {
    nestedAction = function(greeting) {
      console.log(greeting + " " + this.name);
    }.bind(this) // <- bound to "object"
    
    nestedAction("hello");
  }
}

object.action("hello");

// hello object
Thanks Robert

0 Comments:

Post a Comment

<< Home