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

…Continued from Part 1.

Arrow functions

ES6 introduces arrow functions, which can take several forms:

()=>expression
x=>expression
(x,y)=>expression
()=>{operators}
x=>{operators}
(x,y)=>{operators}

Besides being a shorter notation for function(), arrow functions have special rule for this. They inherit this from their immediate lexical context. This makes arrow functions ideal for callbacks. Consider the following code:

'use strict'

var obj = {
    value: 42,
    delayInc() { 
         setTimeout(function() { 
             console.log("Incremented value: " + ++this.value); // oops...
             console.log(this);
	 }, 10);
    },
}
obj.delayInc();

This prints

Incremented value: NaN
Timeout {
  _called: true,
  _idleTimeout: 10,
  _idlePrev: null,
  _idleNext: null,
  _idleStart: 96,
  _onTimeout: [Function],
  _timerArgs: undefined,
  _repeat: null,
  value: NaN }

Why NaN? The hint is in the subsequent lines: this value of the callback is (somewhat arbitrarily) set to a Timeout object. In other languages, the Timeout object would probably have been passed as a parameter, but in JavaScript we have this, which can be set to anything, so why not use it? Consequently, this.x in the callback refers to Timeout.x, which is undefined, and not to obj.x as one might have expected. Incrementing undefined yields NaN, and obj.x remains unchanged.

Pre-EcmaScript 6 fix to this problem was to introduce an extra variable:

'use strict'

var obj = {
    value: 42,
    delayInc() { 
         var $this = this;
         setTimeout(function() { 
             console.log("Incremented value: " + ++$this.value); 
             console.log($this);
	 }, 10);
    },
}

obj.delayInc();

This works as expected:

Incremented value: 43
{ value: 43, delayInc: [Function: delayInc] }

With arrow functions we can get rid of the extra variable and shorten the code:

'use strict'

var obj = {
    value: 42,
    delayInc() { 
         setTimeout(() => { 
             console.log(this);
             console.log(++this.value); 
	 }, 10);
    },
}

obj.delayInc();

This snippet behaves exactly like previous one, printing

Incremented value: 43
{ value: 43, delayInc: [Function: delayInc] }

Unfortunately, inheriting this from the lexical context makes arrow functions unsuitable in some other contexts. Consider:

'use strict'

var obj = {
    value: 42,
    inc: ()=>++this.value,
    inc2() { return ++this.value; }
}

console.log(obj.inc(), obj.inc2());

This prints

NaN 43

The arrow function inc() takes this from the surrounding context, setting it to the global object. Similarly looking regular member function inc2() has its this set to obj at the time of invocation.

Unlike Java and C#, where lambdas are nearly equivalent to regular methods, in JavaScript they behave quite differently.

This in static context

In most languages “static” methods don’t have this. JavaScript is, again, different:

class Foo {
    static f() { 
	console.log(this, this === Foo);
    }
}

Foo.f();

This prints

[Function: Foo] true

In static contexts this points to the surrounding class, which is actually a function.

Conclusion

Simply put, there are two many rules governing the value of this in JavaScript. If I write a simple function printing this, it can print virtually anything, depending on how we invoke it, and it is by design.

function f() { console.log(this); }

f();                      // 'global object' or undefined in strict mode
var obj = { f }; obj.f(); // obj
new f();                  // newly created object of type f
f.call({x:42});           // { x: 42 }
f.bind({x:42})();         // { x: 42 }

ES6 arrow and static functions add even more rules and special cases.

Such versatility makes JavaScript this a really powerful mechanism. Think through how many hoops you would have to jump to call method of one C# class on an instance of another C# class. On the other hand, why would you want to do it? Even if C# had such a feature, it probably would have been considered extremely dangerous and not suitable for use under normal circumstances. Anyway, huge power and flexibility of JavaScript this comes at the expense of simplicity and safety: it provides a number of excellent devices to shoot oneself in the foot.

2 Comments


    1. Thanks. I only wish there were less difficult features to write about 🙂

      Reply

Leave a Reply

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