JavaScript WTF #2: the this keyword (part 1)

Java language specification defines “this” keyword in one sentence. EcmaScript 6 definition of “this” is about a page of pseudo-code spread over several paragraphs, and there is a good reason for that.

JavaScript’s this is defined by chain of nested “execution contexts”. It may depend on both the execution path and the static location of the caller in code, and can refer to virtually anything, or nothing at all. This makes this arguably the most confusing concept in the language. The list of gotchas related to “this” is long and quite impressive: so long in fact, that I had to divide this WTF into parts.

Undefined “this” for indirect method calls

In JavaScript this is set to undefined inside free function calls (in strict mode), and to the method-holding object inside method calls. It makes perfect sense, but the caveat is that if we call an expression whose value is a function, this will always be undefined, even if said function is actually a method of some object. Consider the following code:

'use strict'
var obj = { 
    x:42, 
    printX() { console.log(this.x); } // method
};

function invoke(action) { action(); /* free call */ }

obj.printX();                 // 42
invoke(obj.printX);           // Cannot read property 'x' of undefined

The call on line 9 is a method call, it works as expected. The call on line 10 passes the result of the expression obj.printX as a parameter to the invoke() function. In this case obj is only used to retrieve the pointer to the printX function, and then discarded. When printX is called on line 7 via the action variable, it is considered a free function call, not a method call. Therefore, its “this” pointer is set to undefined, hence the error.

This “feature” caused so much pain, that EcmaScript 6 defined a special work-around for it in the form of arrow functions, to be discussed in part 2.

In certain cases, it may be tricky to distinguish when the call is a method call and when it is not:

'use strict';
obj.printX();            // 42 - method call
(obj.printX)();          // 42 - method call
(false || obj).printX(); // 42 - still method call
(false || obj.printX)(); // Cannot read property 'x' of undefined - free call

Dangerous behavior in default mode

If you forget to add 'use strict' in the beginning of your script, it will run in default, non-strict mode. In default mode, “this” is set to “global object” for free function calls. In web browsers, global object is window. If your code happens to modify a property whose name coincides with one of the window object’s properties, it can lead to rather spectacular results:

var obj = { 
    location: "right: 20px",
    setLocation(side, pixels) {
        this.location = side + " " + pixels + "px";
    }
}

function goLeft(setter) {
    setter("left", 10);
}

goLeft(obj.setLocation); // oops...

When run in a browser (try it!), this code navigates to URL “Left%2010px”, because line 4 modifies window.location instead of obj.location. It is highly unlikely this was the intended behavior.

The exotic kludge of bind()

To get around the problem of undefined “this” for function expressions, JavaScript has a special function bind() that returns an “exotic function object” (it’s an actual term from the specification). When invoked, this object sets this to the value specified.

'use strict'
var obj = { 
    x:42, 
    printX() { console.log(this.x); } 
};

function invoke(action) { action(); }
invoke(obj.printX.bind(obj)); // 42

The trouble is, with “bind” you can set this to any value, sometimes with unexpected results:

'use strict'

var logger = {
    messages: [],
    log(text) {  this.messages.push(text); },
    out(level, text) { this.log(level.toUpperCase() + ": " + text); },
    clear() { console.log("Messages logged: " + this.messages.length); this.messages = []; }
};

function doStuff(out) {
    out("debug", "this is a test");
}

doStuff(logger.out.bind(logger));  // Logs to logger.messages
logger.clear();                    // Messages logged: 1

doStuff(logger.out.bind(console)); // Logs to console, bypasses logger.messages
logger.clear();                    // Messages logged: 0

doStuff(logger.out.bind(Math)); // Logs nowhere; attempts to calculate logarithm of some text
logger.clear();                 // Messages logged: 0

To be continued…

Leave a Reply

Your email address will not be published. Required fields are marked *