YDJKS — this & Object Prototypes: Takeaways

My personal learnings from You Don’t Know JS: this & Object Prototypes

This is the 3rd post in a series of 6, where each post covers my personal takeaways from a book in the You Don’t Know JS series, by Kyle Simpson.

In the previous post, we covered the second installment in the series, Scopes & Closures. In this post, we’ll cover the third installment, this & Object Prototypes.

I’ll only cover my most important learnings from this book. If you want to get more information, you can check out the book directly on GitHub, or check out my personal notes on GitHub, in which I express my thoughts and less significant reflections on the book.

Before we dive in, here’s a table of contents for this article:

  • Purpose of this
  • What this isn’t
  • Dynamically scoped
  • Determining a function’s call site
  • 4 rules for determining this
  • Default Binding
  • Implicit Binding
  • Explicit Binding
  • new Binding
  • Order of precedence

Purpose of "this"

this enables function-reuse across multiple contexts.
By “context”, I really just mean object reference.

const person1 = { name: ‘Anton’ };
const person2 = { name: ‘Tanaka’ };
sayName.call(person1); // person1 is the context.
sayName.call(person2); // person2 is the context.
function sayName() {
console.log(this.name); /* `this` is the context given at
runtime. */
}

Alternatively, you could pass an object reference as an explicit parameter:

const person1 = { name: ‘Anton’ };
const person2 = { name: ‘Tanaka’ };
sayName(person1);
sayName(person2);
function sayName(context) {
console.log(context.name);
}

But the this mechanism is a more elegant, implicit approach to obtaining the context object-reference.

There are some additional advantages of accessing context this way, which we’ll explore later.

What "this" isn’t

First, let’s dispel misconceptions about this.

There are 2 predominant mis-models of this:

  • this refers to the containing function
  • this refers to the containing lexical scope.

Both models are incorrect. Let’s discuss them both.

“Is this the containing function?”
The first incorrect model is: this refers to the containing function.

Basically, some people think that this works like so:

foo.count = 0;
foo();
foo();
foo();
foo.count; // This is 3, right?
function foo() {
this.count++;
}

So, this doesn’t work. At the end of that code snippet, foo.count will still
be 0. Why? Because this.count was never referring to foo. It was referring
to the global object, window.

The first this.count incremented window.count, which was undefined.
Well, doing undefined + 1 returns NaN. Thus, the 2 subsequent this.count
operations were doing NaN + 1, which is still NaN.

So, by the end of this code snippet, we have window.count === NaN.

You can prove this by enabling strict mode. If strict mode is enabled, that code will throw a TypeError, because you cannot use the Property Accessor operator on undefined.

However, you can actually make that code “work” by doing foo.call(foo).

foo.count = 0;
foo.call(foo);
foo.call(foo);
foo.call(foo);
foo.count; // This is 3, now?
function foo() {
this.count++;
}

Functions are objects, so this will work. But this is kind of silly, to be honest.

This incorrect model is easy to dispel. Furthermore, it doesn’t make much sense, either. If you wanted the function to refer to itself, why wouldn’t you just use its name?

For example,

foo.count = 0;
foo();
foo();
foo();
foo.count; // This is 3.
function foo() {
foo.count++; // Just use the function's name
}

You could argue that such a variety of this would be useful for lambdas (anonymous functions), but that’s kind of a contrived case.

“Is this the containing lexical scope?”
This incorrect model sounds more correct, but it’s still wrong.

This model would mean that the following code is correct.

foo();
function foo() {
this.bar();
}
function bar() {
console.log('test');
}

If you believe that this refers to the containing lexical scope, you believe that

  1. this refers to foo's lexical scope.
  2. this.bar() performs an RHS lookup for bar in foo's scope.
  3. bar doesn’t exist in foo's scope.
  4. The engine checks for bar in the enclosing scope, the global scope.
  5. bar exists in the global scope.
  6. The engine evaluates this.bar to that global bar.
  7. test is logged.

This reasoning is wrong. Scope is never directly accessible to JavaScript code. Scope is a blackbox mechanism within the engine.

However, the above code actually works, but for different reasons.

  1. As discussed in the previous post, declaring identifiers in the global scope also adds them as properties of the global object.
  2. bar is declared in the global scope. So, it’s added as a property of the global object. In this case, the global object is the window.
  3. So, window.bar maps to bar.
  4. When the engine evaluates this.bar, this refers to the window.
  5. So, window.bar is invoked.

You can prove this reasoning again by enabling strict mode. If strict mode is enabled, the this won’t refer to the window. Instead, it will evaluate to undefined. You cannot perform the property-accessor operator on undefined, so it will throw a TypeError.

Dynamically scoped
Unlike all other identifiers in JS, this is not lexically scoped. It is
dynamically scoped. Its value is determined by its execution context, not
its lexical context.

More simply put, this's value is determined by its call site.

When a function is invoked, a stack frame is created to represent the invocation. The stack frame possesses runtime information like:

  • Where the function was invoked (derived from the call stack)
  • How the function was invoked
  • What arguments were passed

The stack frame also posses a this reference that’s used for the duration of the function’s execution.

Determining a function’s call site
In general, this's value is determined by its call site.

How do you determine a function’s call site?

Suppose you have a function, a. a's call site is in the stack frame before a on the call stack.

Consider this code:

function c() {
/* call stack: c
(main, anonymous function)
So, the call site is (main, anonymous function) */
  b();
}
function b() {
/* call stack: b
c
(main, anonymous function)
So, the call site is c */
  a();
}
function a() {
/* call stack: a
b
c
(main, anonymous function)
So, the call site is b */
  console.log('yeet');
}
c();

Additionally, most web browsers and IDEs have debugging tools for checking the call stack.

4 rules for determining “this”

There are 4 rules for determining the value of this.

1. Default Binding
2. Implicit Binding
3. Explicit Binding
4. new Binding

These work for the vast majority of cases. There are a few exceptions, which
we’ll discuss.

There is an order of precedence to these rules. Multiple rules can
apply simultaneously, but because there is an order of precedence, we can still
determine which rule is dominant.

Default Binding

Default binding applies for the common function call.

The common function call is a standalone function invocation. It looks like
this:

foo();  // This is a standalone function invocation.
function foo() {
console.log(this);
}

For these default cases, determining this is very simple.

If strict mode is enabled, this evaluates to undefined.

If strict mode is disabled, this evaluates to the global object.
For web browsers, that’s the window.

That’s it.

Implicit Binding

If the call site has a context object, this evaluates to that object
reference.

AKA, If the function is invoked as a property of an object, this is that
object.

AKA, If it looks like <object>.foo(), then this is the <object>.

Here’s a simple case:

var obj = {
name: 'Ramanujan',
sayName: function () {
console.log(this.name);
}
};
obj.sayName(); /* obj is the context object.
Therefore, `this` === obj */

However, also consider this:

function sayName() {
console.log(this.name);
}
var obj = {
name: 'Ramanujan',
sayName: sayName
};
obj.sayName(); /* Again, obj is the context object.
Therefore, `this` === obj */

The invoked function is sayName. It was originally declared as a standalone
function. You may think that causes it to be a standalone function, so default
binding should apply. Nope.

The declaration is irrelevant. Remember, the call site/execution context is
all that matters. And when sayName was invoked, it was invoked as a property of an object. It was invoked as obj.sayName(), so obj was the context object. So, within sayName, this resolved to obj. So, this.name resolved to obj.name, which is 'Ramanujan'.

Lapsing Implicit-Bindings
this bindings do not propagate through functions. Each this binding is
evaluated individually, on a function-by-function basis.

If you’re not vigilant, you can lose the implicit this binding.

Consider this code snippet.

var dog = {
breed: 'Boxer',
sayBreed: function () {
console.log(this.breed);
}
};
dog.sayBreed();

Now, consider this.

var dog = {
breed: 'Boxer',
sayBreed: function () {
console.log(this.breed);
sayBreedAgain();
    function sayBreedAgain() {
console.log(this.breed);
}
}
};
dog.sayBreed();

sayBreedAgain() won’t work.

The first console.log(this.breed) works fine, just as we’d expect.

The second one won’t. Why? You may think it should. sayBreedAgain
is declared and called within sayBreed, where this is bound to dog.

But look at sayBreedAgain()’s call site exactly. Do you see a context
object on that invocation? Is sayBreedAgain being invoked as a property of an
object? Do you see a function of the form ____.sayBreedAgain()?

No.

Therefore, sayBreedAgain’s this reference is determined by default
binding
. Strict mode is disabled, so sayBreedAgain’s this evaluates to the
global object, window. So, sayBreedAgain is logging window.breed, which is
undefined.

So, it logs undefined.

In practice, this happens most often with callbacks.

Consider,

var dog = {
breed: 'Boxer',
sayBreed: function () {
console.log(this.breed);
}
};
execFunc(dog.sayBreed);
function execFunc(func) {
func();
}

This also doesn’t work. You may think it should, since you can see
dog.sayBreed being passed into execFunc. However, look directly at the
call site
. It’s func().

Same thing as before. It’s not being invoked with a context object, so its
this reference is determined by default binding, not implicit binding.

This applies to Web APIs, too.

For most devs, their 1st experience with losing implicit bindings was probably
through passing a callback into setTimeout.

var dog = {
breed: 'Boxer',
sayBreed: function () {
console.log(this.breed);
}
};
setTimeout(dog.sayBreed, 1000);

We don’t know the internal implementation of setTimeout, but you can imagine that it looks something like:

function setTimeout(callback, delayInMS) {
/* Wait for delayInMS */
callback();
}

For the last time, I’ll say it again. Look directly at the function’s call site.
It’s callback(). It’s not being invoked with a context object, so its this
reference is determined by default binding, not implicit binding.

You can “fix” this with explicit binding, like so.

var dog = {
breed: 'Boxer',
sayBreed: function () {
console.log(this.breed);
}
};
setTimeout(dog.sayBreed.bind(dog), 1000);

Only the immediate object matters
If you have an invocation like parentObj.childObj.grandChildObj.foo(),
grandChildObj is considered the context object.

var parentObj = {
a: 4,
  childObj: {
a: 5,
    grandChildObj: {
a: 6,
      foo: function () {
console.log(this.a);
}
}
  }
};
parentObj.childObj.grandChildObj.foo(); // 6

Explicit Binding

Explicit binding allows you to set the context object explicitly.

All JS function have various methods available to them (by virtue of their
[[Prototype]]). 3 of these methods enable explicit binding:

  • Function.prototype.call
  • Function.prototype.apply
  • Function.prototype.bind (ES5+)

Function.prototype.call
The syntax is Function.prototype.call(<context>, arg1, arg2, …).

Observe:

var obj = {
a: 1,
b: 2,
c: 3
};
printProps.call(obj, 'a', 'b', 'c');
function printProps(key1, key2, key3) {
console.log(this[key1]);
console.log(this[key2]);
console.log(this[key3]);
}

Note: If you pass a primitive value as the context, the primitive is “boxed”
into its object-form (e.g. String, Boolean, Number, etc.)

For example, instances of Number have a method available to them:
toExponential(), which returns the given number in scientific notation (in
string form).

Observe:

var num = 32;
toExponential.call(num);
function toExponential() {
return this.toExponential();
}

Function.prototype.apply
This is very similar to Function.prototype.call. The only difference is that
apply accepts a single array of arguments. call accepts a
comma-separated list of arguments.

var obj = {
a: 1,
b: 2,
c: 3
};
printProps.apply(obj, ['a', 'b', 'c']);
function printProps(key1, key2, key3) {
console.log(this[key1]);
console.log(this[key2]);
console.log(this[key3]);
}

Hard binding
Function.prototype.call and Function.prototype.apply don’t directly allow
us to set the this binding of a function invoked downstream.

In other words, there’s no direct way to solve this problem:

var dog = {
breed: 'German Shepherd',
sayBreed: function () {
console.log(this.breed);
}
};
execFunc(dog.sayBreed);
function execFunc(func) {
func(); // This isn't called with a context object!
}

Like, you can’t force downstream functions to change the way they invoke
callbacks.

You can’t modify setTimeout to do this:

function setTimeout(callback, delayInMS) {
/* Wait for `delayInMS` */
callback.call(dog); // LOL
}

So what do you do?! Well, there’s a simple pattern: use a wrapper function.

var dog = {
breed: 'German Shepherd',
sayBreed: function () {
console.log(this.breed);
}
};
var hardBoundSayBreed = function () {
dog.sayBreed.call(dog);
};
execFunc(hardBoundSayBreed);
function execFunc(func) {
func();
}

This was such a common pattern that ES5 added a utility to do this:
Function.prototype.bind

Function.prototype.bind
Function.prototype.bind(<context>, arg1, arg2, …) returns a function that:

  1. has its this reference bound to the given context object.
  2. will be called with arg1, arg2, … (partial application)

The arg1, arg2, ... is optional.

var dog = {
breed: 'German Shepherd',
sayBreed: function () {
console.log(this.breed);
}
};
execFunc(dog.sayBreed.bind(dog));
function execFunc(func) {
func();
}

As of ES6, if a function is hard-bound, that’s reflected in <Function>.name.

const obj = {};
var bar = foo.bind(obj);
console.log(bar.name); // "bound foo"
function foo() { }

Also, this naming stacks!

const obj = {};
var bar = foo.bind(obj);
var baz = bar.bind(obj);
console.log(baz.name); // "bound bound foo"
function foo() { }

“new” Binding

Let’s keep this short and simple.

If a function is prefixed with new, any instances of this within the
function refer to the newly-created object.

Example:

var dog = new Dog('Albert', 'Dachshund');
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}

Order of Precedence

  1. new Binding
  2. Explicit Binding with bind
  3. Explicit Binding with call or apply
  4. Implicit Binding
  5. Default Binding

Here’s an example of a bind binding trumping a call binding.

var waldo = { name: 'waldo' };
var ezra = { name: 'ezra' };
var sayWaldoName = sayName.bind(waldo);
sayWaldoName();
sayWaldoName.call(ezra);
function sayName() {
console.log(this.name);
}

That’s it for now. Stay tuned for next time!