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 functionthis
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
this
refers tofoo
's lexical scope.this.bar()
performs an RHS lookup forbar
infoo
's scope.bar
doesn’t exist infoo
's scope.- The engine checks for
bar
in the enclosing scope, the global scope. bar
exists in the global scope.- The engine evaluates
this.bar
to that globalbar
. 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.
- As discussed in the previous post, declaring identifiers in the global scope also adds them as properties of the global object.
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 thewindow
.- So,
window.bar
maps tobar
. - When the engine evaluates
this.bar
,this
refers to thewindow
. - 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-Bindingsthis
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 isundefined
.
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 seedog.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 itsthis
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 thatapply
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 bindingFunction.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:
- has its
this
reference bound to the given context object. - 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
new
Binding- Explicit Binding with
bind
- Explicit Binding with
call
orapply
- Implicit Binding
- 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!