Photography: "Curious" Liliana Saeb (CC BY 2.0)
JavaScript is a multi-paradigm language that supports object-oriented programming and dynamic linking. Dynamic binding is a powerful concept that allows you to change the structure of JavaScript code at runtime, but this extra power and flexibility is achieved at the cost of some confusion, much of which is related to this
behavior in JavaScript.
With dynamic binding, the definition of the method to call occurs at run time, not at compile time. JavaScript does this with this
and a prototype chain. In particular, inside the this
method is determined during the call, and the value of this
will be different depending on how the method was defined.
Let's play the game. I call her "What is this
here?"
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 the values in the answers
array and check your answers with console.log()
. Have you guessed it?
Let's start with the first case and continue in order. obj.getThis()
returns undefined
, but why? The switch functions never have this
. Instead, they always take this
from the lexical scope ( note lexical this ). For the root of the ES6 module, the lexical area will have an undefined
value of this
. obj.getThis.call(a)
also not defined for the same reason. For switch functions, this
cannot be overridden, even with .call()
or .bind()
. this
will always be taken from the lexical area.
obj.getThis2()
gets a binding during a method call. If before this binding for this function was not, then it can bind this
(since this is not an arrow function). In this case, this
is an obj
object that is bound at the time the method is called using .
or [squareBracket]
property access syntax. ( note implicit binding )
obj.getThis2.call(a)
bit more complicated. The call()
method calls a function with the given value this and optional arguments. In other words, the method gets the binding this
from the .call()
parameter, so obj.getThis2.call(a)
returns the object a
. ( note explicit binding )
In the case of obj.getThis3 = obj.getThis.bind(obj);
we are trying to get an arrow function with this
bound that, as we have already figured out, will not work, so we get undefined
for obj.getThis3()
and obj.getThis3.call(a)
respectively.
For ordinary methods, you can bind, so obj.getThis4()
returns obj
, as expected. He already got his binding here obj.getThis4 = obj.getThis2.bind(obj);
, and obj.getThis4.call(a)
considers the first binding and returns obj
instead of a
.
Let's solve the same problem, but this time we use a class
with public fields to describe an 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 over 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 still get this
from the lexical environment. There is a difference in how this
from the lexical environment is defined for class properties. Inside the initialization of class properties looks like this:
class Obj { constructor() { this.getThis = () => this; } ...
In other words, the switch function is defined within the context of the constructor. Since this is a class, the only way to create an instance is to use the keyword new
(omitting new
will result in an error). One of the most important things that the new
keyword does is to create a new instance of an object and link this
to it in the constructor. This behavior in combination with other behaviors that we have already mentioned above, should explain the rest.
Did you succeed? A good understanding of how this
behaves in JavaScript will save you a lot of time debugging complex problems. If you have the wrong answers, it means that you need to practice a little. Practice with the examples, then go back and check yourself again until you can complete the test and explain to someone else why the methods return what is returned.
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 began as a search for dynamic methods that you could redirect using .call()
, .bind()
or .apply()
became much more complicated with the addition of class methods and pointer functions. Perhaps you should once again focus on this. Remember that switch functions always take this
from the lexical scope, and class
this
in fact lexically limited to the class constructor under the hood. If you ever doubt this
, then remember that you can use a debugger to check its value.
Remember that you can do without this
in solving many JavaScript tasks. In my experience, almost everything can be redefined in terms of pure functions that take all the arguments used as explicit parameters ( this
can be thought of 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, so, unlike the moments of manipulation with this
, you are unlikely to break something. Each time this
set, something depending on its value may break.
However, sometimes this
is useful. For example, to exchange methods between a large number of objects. Even in functional programming, this
can be used to access other object methods in order to implement the algebraic transformations necessary to construct new algebras over existing ones. So, the universal .flatMap()
can be obtained using this.map()
and this.constructor.of()
.
Thanks for the help with the translation of wksmirnowa and VIBaH_dev
Source: https://habr.com/ru/post/452192/