Mittwoch, 6. November 2013

JavaScript WTFs: Scoping and Invocation Context for Methods

Scoping of variables in JavaScript is weird. The invocation context for methods is even weirder!

Function Scope and Lexical Scope


Being used to languages like Java, C++, C#, Pascal etc., the first thing that brought a lot of WTFs to my mind when learning JavaScript was scoping.

The following code sample makes me want to cry out: This is wrong! This will cause a compilation error! a is not in scope, when it is accessed!

function someFunction() {
  if (true) {
     var a = 5;
  }
  console.log(a);
}


Well, in JavaScript this is not true. In JavaScript all variables defined in a functions are visible throughout the whole function, no matter if they were defined in a block or not. So this code will actually execute and output 5. This is called function scoping. JavaScript Scope Quiz (where this example was taken from) actually shows pretty well, how the scoping for variables works and where it probably differs from your expectation.

Reading about scoping in JavaScript I found the following:
"Like most modern programming languages, JavaScript uses lexical scoping. This means that functions are executed using the variable scope that was in effect when they were defined, not the variable scope that is in effect when they are invoked."
('JavaScript: The Definitive Guide, Sixth Edition', D. Flanagan, O'Reilly 2011)

With that in mind, it totally makes sense that the following example (also taken from the JavaScript Scope Quiz) will output 12:

function getFunc() {
    var a = 7;
    return function(b) {
        console.log(a+b);
    }
}
var f = getFunc();
f(5);


So far so good. Although it might seem strange that the context of a function 'already swiped from the function stack' is still available, this seems to be a consistent approach which you can get used to. But wait, there's a big WTF waiting - have a look!

Scoping and Invocation Context for Methods


Consider this:

var someObject = {
  a: 1,
  displayValue: function() {
    console.log(this.a);
  } 
}

var someOtherObject = {
  a: 2,
  listener: null,
  registerListener: function(listener) {
    this.listener = listener;
  },
  callListener: function() {
    this.listener();
  }
}

someOtherObject.registerListener(someObject.displayValue);
someOtherObject.callListener();
 
someObject has a method to display the value of a. This method is registered as a callback via someOtherObject.registerListener(...) and invoked via someOtherObject.callListener().

From the things you read so far, you might be tempted to think that someOtherObject.callListener() will call someObject.displayValue() and thus, using the context of where displayValue was defined, the ouput would be 1 ... wrong! Go ahead and paste it to the JavaScript console of you browser. The console log will display 2.

Let's see what happened here. First someObject.displayValue() is registered via someOtherObject.registerListener(...). When someOtherObject.callListener() is invoked, it will call someObject.displayValue(). Superisingly, a is not retrieved from someObject, but from someOtherObject!

What do we learn from this? The this keyword always references the object whose code is currently executed. WTF? Yes, this is inconsistent in terms of lexical scoping and yes, this is error prone because methods can have really nasty side effects depending on the context in which they are invoked.

But of course, besides turning to the gods and crying "How could you let this happen? How could you do this to us?", there are some workarounds to get this fixed, two of which I want to show you.

Make Object Context Explicit


Luckily you can provide the invocation context in a explicit way by using the call function. This is shown in the following example:

var someObject = {
  a: 1,
  displayValue: function() {
    console.log(this.a);
  }
}

var someOtherObject = {
  a: 2,
  listenerObject: null,
  listenerMethod: null,
  registerListener: function(listenerObject, listenerMethod) {
    this.listenerObject = listenerObject;
    this.listenerMethod = listenerMethod;
  },
  callListener: function() {
    this.listenerMethod.call(this.listenerObject);
  }
}

someOtherObject.registerListener(someObject, someObject.displayValue);
someOtherObject.callListener();


Now we get the 1 that we expected before!

For registering a listener we provide a listenerObject in addition to the function to be called. Invoking call(...) on listenerMethod() with listenerObject as first parameter invokes listenerMethod() in the context of listenerObject - meaning this will be someObject and not someOtherObject.

You might want to make sure, that the object passed as invocation context (someObject in our example) is never null, undefined or a primitive value because then the global context (null or undefined) or a wrapper object (primitive value) would be used as invocation context. By the way: any parameter passed to call(...) after this first would be passed to listenerMethod().

This workaround is nice as long as you have control how the methods on your object are invoked. But if you don't - e.g. because an external library will invoke you method as a callback - or if you just forget to do it the right way, this will most certainly lead to unexpected program behavior.

Make this a Local Variable in a Function


There is another another way to avoid this problem, which seems to be commonly used. This time we use a little trick to remember the desired context without explicitly having to pass it when invoking a method:

var someObject = {
  a: 1,
  createDisplayValueCallback: function() {
    var that = this;
    return function() {
       console.log(that.a);
    }
  }
}

var someOtherObject = {
  a: 2,
  listener: null,
  registerListener: function(listener) {
    this.listener = listener;
  },
  callListener: function() {
    this.listener();
  }
}

someOtherObject.registerListener(someObject.createDisplayValueCallback());
someOtherObject.callListener();


As you see this time the code for someOtherObject is entirely unchanged. Instead we created the method createDisplayValueCallback() which returns a function that does what displayValue() did before. Notice that it operates on that (rather than this), which is defined as a local variable in createDisplayValueCallback() and assigned the value of this. This time the lexical scoping does work as expected, so when the function returned by createDisplayValueCallback() is called, that will be the same as when assigned in createDisplayValueCallback().

You could even add the following method to someObject, if you wanted to leave the possibility to call displayValue() as before:

  displayValue: function() {
    this.createDisplayValueCallback()();
  }


Note that this will again only work if called with the correct invocation context.
 
Of course, there are cases (like this example) where this approach does decrease understandability. But it might be the only chance to get things work the way you want if you have to rely on callbacks from other libraries. And also there are cases where it looks a little better - like this snippet:

...
  init: function() {
    var that = this;
    this.listenee.registerListener(function() {
      that.doSomething();
    });
  },

  doSomething: function() {
    ...
  } 
...

Conclusion


Be aware of scoping in JavaScript - especially when working with objects. And of course: make sure you didn't do it the wrong way by writing meaningful unit tests with a high coverage!

Did you find this blog post useful? Did I write crap? Any other solutions? Questions? Please let me know!