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…
Permalink