Understanding this, one example at a time
- 🕥30 min read -I’ve been struggling with understanding javascript this
keyword
resolution mechanics for a long time.
I read tons of material on the topic, but never really had the complete picture.
This write-up is an attempt to build up a mental model,
that covers the full range of this
keyword resolution mechanics
in javascript.
We are going to check different cases highlighting this
keyword resolution mechanics from different angles
and will combine it all together in the final
example at the end of the article.
So let’s dive right in.
Interview case
Let’s look at an interview example, that I’ve personally seen many times:
const obj = {
x: 1,
method() {
console.log(this.x);
},
};
obj.method(); // 1
const { method } = obj;
method(); // undefined
Here we make 2 calls. The first one contains a dot in the signature:
obj.method(); // 1
The second - doesn’t:
method(); // undefined
We see they yield different results, hence our first guess is
that the call signature somehow affects this
keyword resolution.
In short, this
keyword resolves to the “left of the last dot”
part of a call signature.
Let’s refer to that part as <baseValue>
.
obj.method()
// can be represented as
<baseValue>.method()
// hence in "obj.method" body
console.log(this.x);
// becomes
console.log(<baseValue>.x);
// i.e.
console.log(obj.x); // 1
Same thing would apply, for example, to a nested object
method call like obj1.obj2.obj3.method()
:
const obj1 = {
obj2: {
obj3: {
x: 1,
method() {
console.log(this.x);
}
}
}
}
obj1.obj2.obj3.method()
// can be represented as
<baseValue>.method();
// hence in "obj1.obj2.obj3.method" body
console.log(this.x)
// becomes
console.log(<baseValue>.x)
// i.e.
console.log(obj1.obj2.obj3.x); // 1
In the dot-free method()
call there is no “dot” signature
so we can literally prepend <undefined>
as its <baseValue>
:
method()
// or
<undefined>.method()
// can be represented as
<baseValue>.method()
// hence in "method" body
console.log(this.x)
// becomes
console.log(<baseValue>.x)
// i.e.
console.log(undefined.x)
// in non-strict mode becomesconsole.log(window.x) // undefined
As you can see there is an additional conversion step
from primitive to non-primitive <baseValue>
, which is skipped in strict mode.
If
this
is evaluated within strict mode code, then thethis
value is not coerced to an object. Athis
value ofundefined
ornull
is not converted to the global object and primitive values are not converted to wrapper objects. Thethis
value passed via a function call (including calls made usingFunction.prototype.apply
andFunction.prototype.call
) do not coerce the passedthis
value to an object (9.2.1.2, 19.2.3.1, 19.2.3.3).
And since our code example above is in non-strict mode already it
continues with that additional step, i.e. converting
primitive undefined
to global window
object.
We will return to the topic of strict mode later.
For convenience, let’s refer to the “left of the last dot” rule as just the “dot” rule.
Hidden method
Let’s try to use the “dot” rule to explain this next case.
const _hiddenMethod = function() {
console.log(this.x);
};
const obj = {
x: 1,
method() {
_hiddenMethod();
}
};
obj.method(); // undefined !!!const { method } = obj;
method(); // undefined
Different results this time.
So when we call obj.method()
it then calls hiddenMethod()
,
thus we can build a chain of calls:
GlobalScriptCall() -> obj.method() -> hiddenMethod()
GlobalScriptCall() is our pseudocode way of saying global level invocation, i.e. main program run.
And here is a dilemma:
which call do we need to apply the “dot” rule to,
to resolve this
keyword?
GlobalScriptCall
?
obj.method
?
hiddenMethod
?
Or maybe all three?
The answer is:
The call that directly contains the this
expression in its body.
But why?
For each call in the call chain you have your own version of <baseValue>
which would resolve this
keyword of that specific invocation.
So, here it is unsurprisingly the hiddenMethod()
call and
when we apply the “dot” rule, we get:
hiddenMethod()
// is same as
<baseValue>.hiddenMethod()
// becomes
<undefined>.hiddenMethod()
// non-strict mode converts it into
<window>.hiddenMethod()
// hence in hiddenMethod body
console.log(this.x)
// becomes
console.log(window.x) // undefined
“Brace noise”
Now onto our next example
const obj = {
x: 1,
method() {
// iife1
(function () {
// iife2
(function () {
// iife3
(function () {
// iife4
(function () {
// iife5
(function () {
console.log(this.x);
})();
});
});
});
})();
},
};
obj.method(); // undefined
const { method } = obj;
method(); // undefined
The rules are still the same, but visually the braces might add some confusing noise.
Here we are dealing with a lot of nested iife’s.
But let’s dissect the obj.method()
call.
Here is the chain of calls all the way down to the call
containing console.log(this.x)
that we want to resolve:
GlobalScriptCall() -> obj.method() -> iife1() -> iife2() -> iife3() -> iife4() -> iife5()
Again we need to focus on the call containing this
expression directly in its function body.
Here it’s iife5
.
Let’s apply the same “dot” rule here:
// iife5
(function() {
console.log(this.x);
})();
// i.e.
<baseValue>.(function() {
console.log(this.x);
})();
// becomes
<undefined>.(function() {
console.log(this.x);
})();
// in non-strict mode gets converted into
<window>.(function() {
console.log(this.x);
})();
// hence in function body
console.log(this.x)
// becomes
console.log(window.x) // undefined
So it might seem confusing, but the function object literal (function() {...})
here is working exactly like any other function name like method
in a call signature.
We evaluate it, applying the “dot” rule directly to the function literal signature.
The resolution mechanics is the same.
Arrow function
You might have noticed that arrow functions are not present in previous examples.
This is a deliberate choice because arrow function is evaluated differently.
Arrow function call gets <baseValue>
of the call
that created it.
So arrow function call disregards its own <baseValue>
and takes
its creator call <baseValue>
after applying the “dot” rule to it.
Let’s look at an example:
const obj = {
x: 1,
method: () => {
console.log(this.x);
},
};
obj.method(); // undefined
So even though we expect <baseValue>
of the obj.method()
call
to be obj
console.log(this.x)
still yields undefined
.
Why?
Because if we look at the chain of calls,
GlobalScriptCall() -> obj.method()
and we look at where obj.method
is created,
we see that it was created during GlobalScriptCall()
call.
How so?
If you look close you will see that
const obj = {
x: 1,
method: () => { console.log(this.x); }};
this highlighted portion is defined in the global level,
even before the obj
is finalized as a literal.
So we get the <baseValue>
of GlobalScriptCall()
to be used as our new this
value.
And later we will learn that <baseValue>
of GlobalScriptCall()
is always hardcoded to global object, i.e. window
in browser
and window.x
is undefined
, hence the final result.
Nested arrow functions
To consolidate what we just learned about arrow function
this
keyword resolution, let’s try to apply that to
this next case with nested arrow functions:
iiafe - immediately invoked arrow function expression
const obj = {
x: 1,
method() {
// iiafe1
(() => {
// iiafe2
(() => {
// iiafe3
(() => {
console.log(this.x);
})();
})();
})();
},
};
obj.method(); // 1
const { method } = obj;
method(); // undefined
Starting with obj.method()
call analysis:
Let’s find the call in call chain,
containing this
expression in its body:
GlobalScriptCall() -> obj.method() -> iiafe1() -> iiafe2() -> iiafe3()
It’s iiafe3()
in our case
Now resolution algorithm can go like this:
- Is
iife3
an arrow function ? yes - Where was
iife3
defined ?iife2
- Is
iiafe2
an arrow function ? yes - Where was
iife2
defined ?iife1
- Is
iife1
an arrow function ? yes - Where was
iife1
defined ?obj.method
- Is
obj.method
an arrow function ? no - Apply the “dot” rule to
obj.method
:
obj.method();
// i.e
<obj as baseValue>.method()
// hence in method body and all nested arrow functions
console.log(this.x)
// becomes
console.log(obj.x) // 1
Let’s look at remaining method()
call:
Our slightly different call chain:
GlobalScriptCall() -> method() -> iiafe1() -> iiafe2() -> iiafe3()
Offending call is still iiafe3
- Is
iife3
an arrow function ? yes - Where was
iife3
defined ?iife2
- Is
iiafe2
an arrow function ? yes - Where was
iife2
defined ?iife1
- Is
iife1
an arrow function ? yes - Where was
iife1
defined ?method
- Is
method
an arrow function ? no - Apply the “dot” rule to
method
:
method();
// i.e
<undefined as baseValue>.method();
// in non-strict mode becomes window
<window as baseValue>.method()
// hence in method body and all nested arrow functions
console.log(this.x)
// becomes
console.log(window.x) // undefined
Indirection
This next example describes a pretty confusing form of function invocation, - an indirect function invocation.
const obj = {
x: 1,
method() {
console.log(this.x);
},
};
obj.method(); // 1
(obj.method, obj.method)(); // undefined
(z = obj.method)(); // undefined
// prettier-ignore
(obj.method)(); // 1
Results may be surprising, because a completely separate evaluation is happening before function call evaluation.
Grouping operator is changing the precedence of expressions, making function call secondary to other expression evaluations, that would otherwise happen after call evaluation.
Let’s analyze
call expr
|-------------------------|
(obj.method, obj.method)();
|----------------------|
comma sequence expr
Here we see a comma sequence expression and call expression.
Comma sequence expression evaluates its operands from left to right and returns the evaluation of last operand.
In our case both operands are the same
obj.method, obj.method
After evaluation last operand returns a value - the underlying method
function object,
that obj.method
signature points to.
So we apply the “dot” rule to it.
(function method() {console.log(this.x)})();
// which is the same as
<undefined as baseValue>.(function method() {console.log(this.x)})();
// which gets converted to window in non-strict mode
<window>.(function method() {console.log(this.x)})(); // in non-strict mode
// hence
console.log(this.x);
// becomes
console.log(window.x) // undefined
The same logic applies to (z = obj.method)()
assignment expression case.
We evaluate assignment expression, which returns the value of last operand evaluation, i.e.
obj.method
, the rest is the same.
The last one might also be confusing (obj.method)()
because it yields the same output as without parentheses.
But we should take into account that grouping only changes expression priority and doesn’t trigger extra expression value return as in the previous two expressions.
That’s why we can consider both obj.method()
and (obj.method)()
to be identical,
hence the respective results.
Call / Apply
call
/apply
is a way to provide <baseValue>
explicitly.
const obj = {
method() {
console.log(this.x);
}
x: 1
};
const obj2 = {
x: 2
}
obj.method.call(obj2)
obj.method.call(undefined)
For obj.method.call(obj2)
:
obj.method.call(obj2)
// is same as
<obj2 as baseValue>.method()
// hence in method body
console.log(this.x)
// becomes
console.log(obj2.x) // 2
and for obj.method.call(undefined)
:
obj.method.call(undefined)
// is same as
<undefined as baseValue>.method()
// or in non-strict mode
<window>.method()
// hence in method body
console.log(this.x)
// becomes
console.log(window.x) // undefined
As you might have noticed, we can pass whatever value as <baseValue>
into call(<baseValue>)
/apply(<baseValue>)
.
And of course there is a respective conversion mechanism in place:
undefined
or null
in non-strict mode is converted to the global window
object,
other values are converted into their object wrapper alternatives.
obj.method.call(null); // window
obj.method.call(1); // wrapper object: Number {1}
obj.method.call("string"); // wrapper object: String {"string"}
obj.method.call(true); // wrapper object: Boolean {true}
// ... etc
Here is the full conversion table
! Important note:
In the case of arrow function, call
or apply
is skipped.
Instead, the arrow function this
keyword is resolved as
previously described by evaluating <baseValue>
of a call
where arrow function was defined in the chain of calls:
So here we ignore the .call
part
const obj = {
x: 1,
method() {
// iiafe
(() => console.log(this.x)).call({ x: 2 }); }
};
obj.method(); // 1
and the example gets simplified to just
const obj = {
x: 1,
method() {
// iiafe
() => console.log(this.x); }
};
obj.method(); // 1
And then we proceed with applying the “dot” rule to the call where the arrow function was defined.
So in the chain of calls
GlobalScriptCall() -> obj.method() -> iiafe.call({ x: 2 })
We start with iiafe.call({ x: 2 })
, because
iiafe
contains this
expression directly in its body:
- Is
iiafe
an arrow function ? yes, skip.call({ x: 2 })
part - Where was
iiafe
defined ?obj.method
- Is
obj.method
an arrow function ? no - Apply the “dot” rule to
obj.method
:
obj.method();
// i.e.
<baseValue>.method()
// hence inside and in nested calls
console.log(this.x)
// becomes
console.log(obj.x) // 1
Bind
bind
is just a wrapper function with a hardcoded, fixed this
value.
const obj = {
method() {
console.log(this.x);
}
x: 1
};
const obj2 = {
x: 2
}
const boundToObj2 = obj.method.bind(obj2);
boundToObj2() // 2
boundToObj2
can essentially be represented as:
function boundToObj2() {
return obj.method.call(obj2);
}
boundToObj2
, when called, is just invoking obj.method
with predefined <baseValue>
, which is always obj2
.
So whatever you do, however you try, you won’t be able to change that.
Be it call
, apply
or another bind
on top, that tries to change the this
.
Nothing will affect this inner .call(obj2)
with explicitly passed obj2
.
Or in other words:
boundToObj2(); // 2
boundToObj2.call(obj); // still 2, call(obj) affects nothing
const reboundBack = boundToObj2.bind(obj); // bind(obj) affects nothing
reboundBack(); // nope, still 2
reboundBack.apply(obj); // nopes, still 2 and apply(obj) is having no affect at all
! Important note:
In the case of arrow function, bind
call is completely ignored.
Instead, the arrow function this
keyword is resolved as previously described
by evaluating <baseValue>
of a call where arrow function was defined in the chain of calls:
So we ignore the .bind
part
const obj = {
x: 1,
method() {
const boundFn = (() => console.log(this.x)).bind({ x: 2 });
boundFn();
},
};
obj.method(); // 1
and our example gets simplified to
const obj = {
x: 1,
method() {
const boundFn = () => console.log(this.x);
boundFn();
},
};
obj.method(); // 1
And then we proceed with applying the “dot” rule to the call where the arrow function was defined.
So in the chain of calls
GlobalScriptCall() -> obj.method() -> boundFn()
We start with boundFn
, because
boundFn
contains this
expression directly in its body:
- Is
boundFn
an arrow function ? yes, skip.bind({ x: 2 })
part - Where was
boundFn
defined ?obj.method
- Is
obj.method
an arrow function ? no - Apply the “dot” rule to
obj.method
:
obj.method();
// i.e.
<baseValue>.method()
// hence inside and in nested calls
console.log(this.x)
// becomes
console.log(obj.x) // 1
Great. Now let’s move to our next case. Callbacks.
Callback
What are callbacks exactly?
And why do we talk about this
keyword resolution in callbacks separately?
Because one thing that makes callback a callback is the inversion of control
In other words we hand function invocation control over to some other abstraction, 3rd party or whatever.
That 3rd party can invoke it whenever and however it deems necessary.
And as we already know, one of the keys to correctly
resolving the this
keyword is knowing how exactly the call is made, i.e.
what is the call signature.
Is it a regular invocation? Call/Apply
? Or maybe it’s assigned to an object property
and called with that object <baseValue>
?
The answer is we don’t know, and we have to know or guess how our callback is invoked, so we can move on with our analysis.
For example let’s check how this
is resolved in case of setTimeout
as a case example.
const obj = {
x: 1
method() {
setTimeout(
// iife callback
function() {
console.log(this.x)
},
100
);
}
}
obj.method(); // undefined
const {method} = obj;
method(); // undefined
Here we can assume that setTimeout
internally might
be calling passed function after a delay like this:
// pseudo code
function setTimeout(callback, delay, ...args) {
wait(delay);
callback(...args);
}
So setTimeout
call by itself doesn’t matter for us
we can completely disregard it as long
as we know how callback
is eventually invoked.
So if we build a chain of calls for obj.method()
call,
we would get this
GlobalScriptCall() -> obj.method() -> setTimeout(iife) -> iife()
And at this point it doesn’t matter if we tweak the
setTimeout()
call trying to affect iife()
this
keyword resolution,
because as we now know iife()
is just called directly as is,
with its own independent <baseValue>
as in <baseValue>.iife()
GlobalScriptCall() -> obj.method() -> setTimeout.call(null, iife) -> iife()
GlobalScriptCall() -> obj.method() -> setTimeout.apply([], iife) -> iife()
GlobalScriptCall() -> obj.method() -> setTimeout.bind({})(iife) -> iife()
All of the above setTimeout
call variations don’t have any affect and iife()
will
be resolved by applying standard “dot” rule to iife()
call
- is
iife()
an arrow function? no - apply “dot” rule to
iife()
call rightaway
iife()
// is same as
<undefined as baseValue>.iife(...args)
// in non-strict mode becomes
<window>.iife(...args)
// so in iife body
console.log(this.x)
// becomes
console.log(window.x); // undefined
Same procedure for method()
invocation.
GlobalScriptCall() -> method() -> setTimeout(iife) -> iife()
The rest of the resolution logic is same…
Arrow function callback
But what if we have an arrow function as a callback?
How does that work out?
Let’s bring back our example, a little tweaked this times:
iiafe - immediately invoked arrow function expression
const obj = {
x: 1
method() {
setTimeout( // iiafe callback
() => {
console.log(this.x)
},
100
);
}
}
obj.method(); // 1
const {method} = obj;
method(); // undefined
We build the chain of calls
GlobalScriptCall() -> obj.method() -> setTimeout(iiafe) -> iiafe()
- is
iiafe
an arrow function? yes - What call did create it?
obj.method
- apply “dot” rule to
obj.method()
call
You see what just happened?
Up to this point you might have thought that for arrow functions, the resolution call is just the previous call in the call chain but that’s why I brought up this example, to showcase the difference.
Indeed setTimeout()
call is the previous call, and you could apply “dot” rule
to it, but the truth is we need to resolve iiafe
and it was created/declared inside of
obj.method()
body, even though visually being passed to setTimeout(iiafe)
as argument might
seem confusing.
obj.method()
// is same as
<obj as baseValue>.method()
// so in obj.method and iiafe body
console.log(this.x)
// becomes
console.log(obj.x); // 1
For method()
call:
method()
// is same as
<undefined as baseValue>.method()
// in non-strict mode becomes
<window>.method();
// so in method and iiafe body
console.log(this.x)
// becomes
console.log(window.x); // undefined
So please take this distinction into account.
We will have another example over arrow function’s creation place importance later on when discussing classes.
And now let’s revisit strict mode and this
keyword resolution edge cases.
Strict mode
Earlier we touched upon the topic of strict mode.
But what is “strict” code exactly?
Based on ECMAScript specification text, code is strict when it is:
- a Global code starting with
"use strict"
directive - a module code
- class declaration or expression code
- a direct
eval
call argument that starts with"use strict"
directive - a direct
eval
call argument, giveneval
was itself called from strict code - an indirect
eval
call argument that starts with"use strict"
directive - function declaration, expression, etc… that starts with
"use strict"
directive or is already in one - a global
Function
constructor’s second argument, starting with"use strict"
Everything else is considered non-strict code, or code in non-strict mode.
As we already know, in non-strict mode there is an additional conversion step.
But there are still some deviations from that rule, which we check next for broader perspective.
Global code
Let’s start with global level this
keyword.
You might ask, why didn’t we start the article with outlining this one?
Seems pretty basic from the first site.
But if you evaluate this
keyword directly in global code, you will be surprised that even
after "use strict"
directive this
keyword will still resolve to global window
object.
// global code
"use strict";
console.log(this);
To understand the mechanics we need to go up one abstraction level, and look from the perspective of the running program itself.
So in pseudo-code the above example can be expressed as:
const window = {...};
// main browser program call
function GlobalScriptCall() {
// global code "use strict"; console.log(this);}
GlobalScriptCall.call(window);
So in other words we end up evaluating a global level call
with explicitly set <baseValue>
GlobalScriptCall.call(window);
// is same as
<window as baseValue>.GlobalScriptCall();
// hence in GlobalScriptCall() body
console.log(this)
// becomes
console.log(window)
Strict mode doesn’t have anything to affect,
<baseValue>
is already provided and it’s an object,
so there is nothing to convert or not convert to.
Eval
Now let’s look at a different, but not less interesting this
keyword resolution scenario.
this
resolution in eval code.
There are 3 forms of eval calls:
- direct
eval
call - indirect
eval
call (global) - builtin
Function
call (global)
Direct eval works without surprises and evaluates the string argument in the code level within which it was called, respecting inherited strict mode rules:
"use strict";
const obj = {
x: 1,
method() {
eval("console.log(this.x)");
},
};
obj.method(); // logs: 1
const { method } = obj;
method(); // logs: TypeError: Cannot read property 'x' of undefined
As expected,
obj.method()
// is the same as
<baseValue>.method()
// hence
console.log(this.x)
// becomes
console.log(obj.x)
and for method()
method()
// is the same as
<baseValue>.method()
// hence
console.log(this.x)
// in strict mode
console.log(undefined.x) // TypeError: Cannot read property 'x' of undefined
A bit different story with other eval forms, though.
I deliberately marked aforementioned indirect eval
and Function
eval calls
as “global”, because they evaluate the string argument as global level code.
What’s interesting about global eval invocation is that it’s unaffected by surrounding code mode.
To change its code mode one has to explicitly declare it inside the string argument for each global eval invocation.
For example, in the following setup
"use strict"; // (1)
const obj = {
x: 1,
method() {
// non-strict indirect eval
(1, eval)(`
// this block of code is unaffected by external "use strict" (1)
console.log(this); // window, because indirect eval is global code
(function() {
console.log(this) // window, because non-strict code
})();
`);
// non-strict Function eval
Function(
"",
`
// this block of code is unaffected by external "use strict" (1)
console.log(this) // window
(function() {
console.log(this) // window
})();
`
)();
},
};
obj.method();
const { method } = obj;
method();
Global eval code is not affected by surrounding "use strict"
, so it’s in non-strict mode,
unless explicitly stated inside the string argument like here:
"use strict";
const obj = {
x: 1,
method() {
(1, eval)(`
// this block of code is now a strict code
"use strict";
console.log(this); // window, because global level is always hardcoded
(function() {
console.log(this) // undefined, as expected in strict mode
})();
`);
Function(
"",
`
"use strict";
console.log(this); // window, because global level is always hardcoded
(function() {
console.log(this) // undefined, as expected in strict mode
})();
`
)();
},
};
obj.method();
const { method } = obj;
method();
One last thing that is not specific to eval
but applies generally and still can
be a little bit more confusing with eval + strict mode:
function logThis() {
console.log(this);
}
const obj = {
x: 1,
method() {
eval(`
"use strict";
logThis();
`);
},
};
obj.method(); // window
You might think that since "use strict"
is declared within string argument,
logThis
should abide by strict mode rules, but it’s not, because we evaluate by
the place of creation and not the place of invocation,
i.e. logThis
was created in non-strict mode, hence non-strict mode rules apply
even if called from strict mode, and vice versa:
function containedLogThis() {
"use strict";
return function logThis() {
console.log(this);
};
}
const obj = {
x: 1,
method() {
// logThis is created in strict mode even when called from non-strict
const logThis = containedLogThis();
eval(`
logThis();
`);
},
};
obj.method(); // undefined
That’s the gist of it for eval this
keyword resolution mechanics.
Now let’s shift our attention to classes and
their mechanics of this
keyword resolution.
Class
class is a syntactic sugar for pre-es6 class constructor function.
The main difference is that es6 class
is by definition a strict code.
So this
class Obj {
constructor() {
this.x = 1;
}
arrowProp = () => {
console.log(this.x);
};
method() {
console.log(this.x);
}
}
is basically same as this
function Obj() {
"use strict"; this.x = 1;
this.arrowProp = () => {
console.log(this.x);
};
}
Obj.prototype.method = function() {
"use strict"; console.log(this.x);
};
When we instantiate the class with new
operator,
<baseValue>
of constructor call is set to a new empty object {}
new Obj()
// is internally calling
<{} as baseValue>.Obj()
// hence inside constructor
this // equals {}
Later when we want to call the methods, that’s where wee see the differences.
Let’s unpack those one by one and start with an example for
pre-es6 class constructor function this
keyword resolution
in non-strict mode:
function Obj () {
this.x = 1;
this.arrowProp = () => {
console.log(this.x);
};
}
Obj.prototype.method() {
console.log(this.x);
}
const obj = new Obj()
obj.method(); // 1
obj.arrowProp(); // 1
const {method, arrowProp} = obj;
method(); // undefined
arrowProp(); // 1
let’s analyze obj.method()
:
- Is
obj.method()
call an arrow function call? No - Apply the “dot” rule to
obj.method()
call
obj.method()
// is the same as
<baseValue>.method()
// hence
console.log(this.x)
// becomes
console.log(obj.x) // 1
No surprises here.
Now it’s time to consider an example that I promised to look at in arrow function callback section relating to arrow function creation place.
So let’s analyze obj.arrowProp()
call:
- Is
obj.arrowProp()
an arrow function call? Yes - Where was
obj.arrowProp()
function created? Duringnew Obj()
call - Apply the “dot” rule to
new Obj()
new Obj()
// is same as
<{} as baseValue>.Obj()
// {} is the obj object, hence within constructor body
console.log(this.x)
// becomes
console.log(obj.x)
This might be confusing because if you look
at the chain of calls for obj.arrowProp()
call
GlobalScriptCall() -> obj.arrowProp()
you don’t see the new Obj()
call, because it happened in one of previous call chains,
during obj
instantiation.
But we still use its <baseValue>
,
because new Obj()
call is the place
where arrowProp
arrow function is created.
So again pay attention to where arrow function is created,
to correctly infer the <baseValue>
.
Now you have all the knowledge to correctly infer this
keyword
in remaining dot-free method()
and arrowProp
invocations.
For method()
:
- Is
method()
call an arrow function call? No - Apply the “dot” rule to
method
call
method()
// is same as
<undefined as baseValue>.method()
// in non-strict mode becomes
<window>.method()
// hence
console.log(this.x)
// becomes
console.log(window.x) // undefined
For arrowProp()
:
- Is
arrowProp()
an arrow function call? Yes - Where was
arrowProp()
function created? Duringnew Obj()
call - Apply the “dot” rule to
new Obj()
new Obj()
// is same as
<{} as baseValue>.Obj()
// {} is the obj object, hence within constructor body
console.log(this.x)
// becomes
console.log(obj.x) // 1
Now let’s look at a class example
class Obj {
constructor() {
this.x = 1;
}
arrowProp = () => {
console.log(this.x);
};
method() {
console.log(this.x);
}
}
const obj = new Obj();
obj.method(); // 1
obj.arrowProp(); // 1
const { method, arrowProp } = obj;
method(); // TypeError: Cannot read property 'x' of undefined
arrowProp(); // 1
Essentially all the steps and resolution logic is the
same as in previous pre-es6 class constructor function from above,
except method()
, and that’s because class
definition code is a strict mode code,
so no conversions happen from undefined
to global window
object.
- Is
method()
call an arrow function call? No - Apply the “dot” rule to
method()
call
method();
// is same as
<undefined as baseValue>.method();
// hence
console.log(this.x);
// becomes
console.log(undefined.x) // TypeError: Cannot read property 'x' of undefined
That’s it. Congrats on making it this far.
Now as promised, let’s put all the pieces together into one final example.
Putting it all together
Behold the ultimate boss.
const x = 1;
const obj1 = {
x: 2,
};
class Obj2 {
constructor() {
this.x = 3;
}
anotherMethod() {
const func = function () {
new Promise(
// iiafe2
(resolve, reject) => {
const testFunc = (() => {
console.log(this.x);
}).bind(obj2);
const innerObj = {
x: 2,
testFunc,
};
innerObj.testFunc();
}
);
};
func.call(obj1);
}
method() {
// iiafe1
(() => {
eval("this.anotherMethod()");
})();
}
}
const obj2 = new Obj2();
obj2.method(); //?
const { method } = obj2;
method(); //?
What are you going to do? You got 5… 4… 3… 2… 💣 kaboom!!!
Kidding :)
For obj2.method()
call:
As always we start with finding the call in the call chain that
contains this
expression directly inside.
Here we have two candidates
iiafe1()
innerObj.testFunc()
Let’s also visualize the chain of calls for convenience:
GlobalScriptCall() -> obj2.method() -> iiafe1() -> eval('this.anotherMethod()') -> func.call(obj1) -> iiafe2() -> testFunc()
Since we have 2 this
expressions to resolve, we can resolve them one by one,
in call order.
Let’s start with resolving the this
keyword in eval('this.anotherMethod()')
call within iiafe1()
.
Analyzing:
- Is
iiafe1
an arrow function ? yes. - Where was
iiafe1
defined? inobj2.method()
call. - Is
obj2.method
an arrow function ? no - Apply “dot” rule to
obj2.method()
call.
obj2.method();
// is the same as
<obj2 as baseValue>.method();
// hence
this.anotherMethod();
// becomes
obj2.anotherMethod();
Now onto the remaining this
expression:
- Is
innerObj.testFunc
an arrow function ? yes, ignore.bind(obj2)
call - Where was
innerObj.testFunc
defined? iniiafe2
. - Is
iiafe2
an arrow function ? yes - Where was
iiafe2
defined? Infunc.call(obj1)
call. - Is
func
an arrow function ? no - Apply the “dot” rule to
func.call(obj1)
call.
func.call(obj1);
// is same as
<obj1 as baseValue>.func();
// hence in nested code
console.log(this.x);
// becomes
console.log(obj1.x); // 2
Great!
And what about dot-free method()
invocation?
Well let’s see.
The chain is a little different
GlobalScriptCall() -> method() -> iiafe1() -> eval('this.anotherMethod()') -> func.call(obj1) -> iiafe2() -> testFunc()
We still have 2 expressions to tackle
iiafe1()
innerObj.testFunc()
Let’s start with iiafe1
again:
Analyzing:
- Is
iiafe1
an arrow function ? yes. - Where was
iiafe1
defined? inmethod()
call. - Is
method
an arrow function ? no - Apply “dot” rule to
method()
call.
method();
// is the same as
<undefined as baseValue>.method();
// hence
this.anotherMethod();
// becomes in strict mode
<undefined>.anotherMethod(); // TypeError: Cannot read property 'anotherMethod()' of undefined
And program halts, because we are in a class method, and class level code is always a strict code.
Summing up
So if you want to correctly infer this
keyword:
- Build the call chain all the way down to the call/calls that contain
this
expression directly inside. - If there are multiple calls with
this
keyword directly inside, evaluate them from left to right, i.e. in order of invocation. - When evaluating the call containing
this
keyword, check if it’s an arrow function. - If it is, apply the “dot” rule to the call where this arrow function was defined.
- Otherwise apply the “dot” rule to the call, directly containing
this
keyword. - Given a call like
foo.call(<baseValue>)
orfoo.apply(<baseValue>)
, apply “dot” rule tofoo
with explicitly provided<baseValue>
fromcall/apply
. - Unless it’s an arrow function call, in which case ignore
call/apply
altogether. - Given call that was previously bound with
.bind(<baseValue>)
, apply “dot” rule to that call with explicitly provided<baseValue>
frombind
. - Unless
.bind(<baseValue>)
was called on an arrow function, then ignore.bind(...)
altogether. - When in strict mode don’t convert primitive
<baseValue>
likeundefined
ornull
to object counterparts, likewindow
- Beware of edge cases with global evaluation, eval and indirection.
Bonus: NodeJS
In the bonus section I’d like to explore the resolution of this
keyword in NodeJS.
When executing global code like this in NodeJS:
console.log(this);
internally it gets wrapped into something like this
const module = { exports: {} };
(function (exports, require, module, __filename, __dirname) {
console.log(this); // {}
}.call(
module.exports,
module.exports,
require,
module,
__filename,
__dirname
));
And since it’s a .call()
that sets
<baseValue>
explicitly to module.exports similarly to how in GlobalScriptCall()
we set window
as global object, it’s unaffected by strict mode.
"use strict";
console.log(this); // {}, i.e. module.exports
! Important note:
Beware when trying above example in NodeJS CLI REPL
because REPL [operates](NodeJS CLI REPL)
with global
as the default global level object
useGlobal
If true, specifies that the default evaluation function will use the JavaScript global as the context as opposed to creating a new separate context for the REPL instance. The NodeJS CLI REPL sets this value to true. Default: false.
$ user
Welcome to Node.js v12.13.0.
Type ".help" for more information.
> console.log(this)
Object [global] {
global: [Circular],
clearInterval: [Function: clearInterval],
clearTimeout: [Function: clearTimeout],
setInterval: [Function: setInterval],
setTimeout: [Function: setTimeout] { [Symbol(util.promisify.custom)]: [Function] },
queueMicrotask: [Function: queueMicrotask],
clearImmediate: [Function: clearImmediate],
setImmediate: [Function: setImmediate] {
[Symbol(util.promisify.custom)]: [Function]
}
}
So that can be confusing but if you just
$ echo "console.log(this)" > index.js
$ node index.js
{}
$ echo "console.log(this === module.exports)" >> index.js
$ node index.js
true
You see that it correctly yields module.exports
object as it should.
And finally non-global non-strict code this
keyword gets
resolved to NodeJS global object
which is literally called global.
So to sum it up:
console.log(this); // {}, i.e. module.exports
(function () {
console.log(this); // Object [global] {
// global: [Circular],
// clearInterval: [Function: clearInterval],
// clearTimeout: [Function: clearTimeout],
// setInterval: [Function: setInterval],
// setTimeout: [Function: setTimeout] { [Symbol(util.promisify.custom)]: [Function] },
// queueMicrotask: [Function: queueMicrotask],
// clearImmediate: [Function: clearImmediate],
// setImmediate: [Function: setImmediate] {
// [Symbol(util.promisify.custom)]: [Function]
// }
// }
})(); // <baseValue> is undefined, gets converted to global object
(function () {
"use strict";
console.log(this); // undefined
})(); // <baseValue> is undefined, doesn't get converted
// to global object, because of strict mode
Good reads
Share if you liked it: