Wednesday, December 16, 2015

Looking at 'this' in JavaScript

JavaScript looks a lot like C#. But the internal plumping is utterly different. I've always found this somewhat befuddling. As a C# developer, you kind of think you know how things are working, but often you don't. Which makes figuring out what's happening when things are not working really challenging. I've just started reading a book in Kyle Simpson's (@getify) accurately titled series, You Don't Know JavaScript, a slim monograph on this & Object Prototypes, and things are kind of clearer. Kind of.


So the following bit of C# and JavaScript look a lot alike:

C#:

public class Car {
  private string make;
  private string model; 
  private int year;

  public Car(string make, string model, int year) {
    this.make = make;
    this.model = model;
    this.year = year;
 }

  public override string ToString() {
    return string.Format("Car: {0} {1} {2}", make, model, year);
  }
}

JavaScript:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

Car.prototype.toString = function () {
  return "Car: " + this.make + " " + this.model + " " + this.year;
}

Invocation is pretty similar too:
static void Main()
{
  var car = new Car("Honda", "Civic", 2003);
  Console.WriteLine(car);
}

or

var car = new Car("Honda", "Civic", 2003);
console.log(car.toString());

But underneath this similarity is a whole lot of difference. C# has classes, and JavaScript does not. JavaScript has functions that can returned typed objects, but does this without a class mechanism. Put differently, the "this" in C# is tightly circumscribed within in a class membrane, and can only appear there. That makes its behavior very predictable. It refers to current instance of the class, and that's it.
JavaScript on the other hand has functions, and these functions might be intended to create classes or they might not. And so the language specification is powerless to limit the context of this, and must therefore define its behavior in a whole heap of contexts, which the first two chapters of Kyle Simpson's book enumerate. Let's take a look at a few of them.

Simpson deals with new last, and we've already taken a look at that. It's the friendly one, the one that makes sense to us classical programmers. But what about this:

var message = "hello";

function sayMessage() {
  console.log(this.message);
}

sayMessage();

That works. Try it. But this does not:

(function() {
  var message = "hello";
  function sayMessage() {
  console.log(this.message);
  }
  sayMessage();
})();

Why does one return "hello", and the other undefined? Because the fallback rule if nothing else determines the meaning of this is to use the global namespace. Yuck. It's kind of scary to have code that works outside of an IIFE but fails inside of one. Fortunately, you can "use strict" inside the function, and then the behavior is consistent:

var message = "hello";

function sayMessage() {
  "use string";
  console.log(this.message);
}

sayMessage();

And now the output is:
TypeError: Cannot read property 'message' of undefined

For my money, this is a heck of a good reason to "use strict". I can picture spending a few very unpleasant hours trying to figure that one out. Note that "use strict" has to cover the place where the this is used, so that this code still outputs "hello":
var message = "hello";

function sayMessage() {

  console.log(this.message);
}

"use string";
sayMessage();

The book covers a whole host of other scenarios: implicit binding, explicit binding, call and apply. I will leave you with just one more:
var anotherCar = { construct: Car};

anotherCar.construct("Ford", "Fiesta", 1978);

console.log(anotherCar.year);
>> 1978

Okay, so far so good. Rather than using "new" to supply the "this", we are attaching the this by hand by including the function directly to an object. But what if we then assign the function to another variable?
var anotherCar = { construct: Car};

var c = anotherCar.construct;
c("Ford", "Model T", 1908);

console.log(anotherCar.year);
>> undefined

Because c is a reference to the function itself, not to the copy of the function that lived inside the "anotherCar" object. The moral of the story: it's where the function is called that determines the behavior of "this".

No comments:

Post a Comment