
What is this here? Inner Operation of JavaScript Objects
- Transfer
Photo: Curious Liliana Saeb (CC BY 2.0)
JavaScript is a multi-paradigm language that supports object-oriented programming and dynamic linking. Dynamic linking is a powerful concept that allows you to change the structure of JavaScript code at runtime, but these additional power and flexibility are achieved at the cost of some confusion, most of which is related this
to JavaScript behavior .
Dynamic linking
With dynamic binding, the definition of the method to call occurs at run time, and not at compile time. JavaScript does this with a this
prototype chain as well. In particular, inside the method it this
is determined during the call, and the value this
will be different depending on how the method was defined.
Let's play a game. I call her "What is here this
?"
const a = {
a: 'a'
};
const obj = {
getThis: () => this,
getThis2 () {
return this;
}
};
obj.getThis3 = obj.getThis.bind(obj);
obj.getThis4 = obj.getThis2.bind(obj);
const answers = [
obj.getThis(),
obj.getThis.call(a),
obj.getThis2(),
obj.getThis2.call(a),
obj.getThis3(),
obj.getThis3.call(a),
obj.getThis4(),
obj.getThis4.call(a)
];
Think about what the values in the array will be answers
and check your answers with console.log()
. Guessed?
Let's start with the first case and continue in order. obj.getThis()
returns undefined
, but why? Arrow functions never have their own this
. Instead, they always take this
from the lexical scope ( approx. Lexical this ). For the root of the ES6 module, the lexical region will have an undefined ( undefined
) value this
. obj.getThis.call(a)
also not defined for the same reason. For arrow functions this
it cannot be redefined, even with .call()
or .bind()
. this
will always be taken from the lexical field.
obj.getThis2()
Gets a binding during a method call. If before this there was no binding this
for this function, then it can be bound this
(since this is not an arrow function). In this case, it this
is an object obj
that is bound at the time the method is called using .
or [squareBracket]
property access syntax. ( note implicit binding )
obj.getThis2.call(a)
a little harder. The method call()
calls a function with the given this value and optional arguments. In other words, the method gets the binding this
from the parameter .call()
, so it obj.getThis2.call(a)
returns an object a
. ( note explicit binding )
In the case obj.getThis3 = obj.getThis.bind(obj);
we are trying to get an arrow function with an attached one this
, which, as we have already figured out, will not work, so we get undefined
for obj.getThis3()
and, obj.getThis3.call(a)
respectively.
For regular methods, you can bind, so it obj.getThis4()
returns obj
as expected. He already got his binding here obj.getThis4 = obj.getThis2.bind(obj);
, and obj.getThis4.call(a)
takes into account the first binding and returns obj
instead a
.
Twisted ball
We’ll solve the same problem, but this time we use class
public fields to describe the object ( Stage 3 innovations at the time of this writing are available in Chrome by default and with @babel/plugin-offer-class-properties
):
class Obj {
getThis = () => this
getThis2 () {
return this;
}
}
const obj2 = new Obj();
obj2.getThis3 = obj2.getThis.bind(obj2);
obj2.getThis4 = obj2.getThis2.bind(obj2);
const answers2 = [
obj2.getThis(),
obj2.getThis.call(a),
obj2.getThis2(),
obj2.getThis2.call(a),
obj2.getThis3(),
obj2.getThis3.call(a),
obj2.getThis4(),
obj2.getThis4.call(a)
];
Think about the answers before continuing.
Ready?
All calls except obj2.getThis2.call(a)
return an instance of the object. obj2.getThis2.call(a)
returns a
. Arrow functions are still derived this
from the lexical environment. There is a difference in how it this
is defined from the lexical environment for class properties. Inside, the initialization of class properties looks something like this:
class Obj {
constructor() {
this.getThis = () => this;
}
...
In other words, the arrow function is defined within the constructor context. Since this is a class, the only way to create an instance is to use the keyword new
(omission new
will result in an error). One of the most important things a keyword does new
is create a new instance of the object and bind this
to it in the constructor. This behavior, combined with other behaviors that we mentioned above, should explain the rest.
Conclusion
Did you succeed? A good understanding of how this
JavaScript behaves will save you a lot of time debugging complex problems. If you made a mistake in the answers, this means that you need to practice a little. Practice with the examples, then go back and check yourself again until you can run the test and explain to someone else why the methods return what they return.
If it was harder than you expected, then you are not alone. I asked a lot of developers on this topic and I think that so far only one of them has coped with this task.
What started as a search for dynamic methods that you could redirect using .call()
, .bind()
or .apply()
, has become much more difficult with the addition of class methods and arrow functions. Perhaps you should once again focus on this. Remember that arrow functions are always taken this
from the lexical scope, and class
this
are actually lexically limited by the class constructor under the hood. If you ever doubt it this
, then remember that you can use the debugger to check its value.
Remember that you can do without solving many JavaScript tasks this
. In my experience, almost everything can be redefined in terms of pure functions that take all the arguments used as explicit parameters ( this
you can imagine it as an implicit variable). The logic described through pure functions is deterministic, which makes it more testable. Also, with this approach there are no side effects, therefore, unlike the moments of manipulation with this
, you are unlikely to break something. Each time it is this
installed, something depending on its meaning may break.
However, sometimes this
useful. For example, to exchange methods between a large number of objects. Even in functional programming, this
you can use it to access other methods of the object in order to implement the algebraic transformations necessary to build new algebras on top of existing ones. So, universal .flatMap()
can be obtained using this.map()
and this.constructor.of()
.
Thanks for the help with translating wksmirnowa and VIBaH_dev