DEV Community

yw662
yw662

Posted on

What indeed is `this` in JavaScript

Functions and their invocations

Before we talk about this let's talk about function invocations first.

A function, in general is a (...args: TArgs) => TReturn. The code inside a function are given access to a list of arguments (or parameters if you like), but those arguments are undecided before the function is invoked, that is why they are called "formal". They are kind of "imaginary". The code "imagine" there will be a list of real values to use, but that happens only when the function is invoked, and only for and during this specific invocation.

Arguments are finally bond to a "actual" value upon each invocation, and are different among different invocations.

But this is not in the list. How can a function access an argument/a parameter, when it is not in the list ?

Is this a capture ?

High level programming languages, like JavaScript, has an ability to "capture" identifiers (or usually, variables, if you like, or you may just call it "an item") from the current "lexical scope" and make those captures usable inside the function. This is indeed not that trivial as it seems. A function can capture an identifier if and only if it is inside the lexical scope of that identifier.

But, Is there an identifier called this to capture? Usually no. For "arrow expressions", yes this, if there is one, is a capture. But for functions that are not arrow expressions, no.

Then what is this indeed ?

If it is not a capture, it is an argument. It "has to" be an argument or it will never be bond to an actual value.

But it is not in the list. Usually arguments are explicitly defined in the list, not for the two "implicit arguments" in JavaScript. One of the two is this, and the other one is arguments. Arrow expressions have no implicit arguments therefore no this and arguments.

So, implicit argument, is an argument but passed implicitly.

But how you pass an argument implicitly ?

Each argument must be decided upon "invocation". Therefore, the implicit argument this is passed upon invocation. But you don't pass it via the list, you pass it by the "member expressions" (in ecma262) or "property accessors" (in MDN).

Member expressions include, the dot in a.b, the bracket pair in a[b], the new in new b(), the .# in a.#b, and super. These are pretty much what we would care about, since you would never worry about this in new.target, import.meta and template literal calls.

That is to say, if you call a function like a.b() or a[b](), a will be passed as this. If you call it like new b(), the object created by new will be passed. That is the basic rule of this.

The implicit argument arguments is passed as the whole actual argument list, including those without a formal argument bond. It is less useful today thanks to the super cool rest syntax.

Let's make it short: you pass this by "member expression". That's it. Although ecma262 is not describing it this way, it pretty much follows this logic. It is pretty much a lexical thing.

corner cases ?

If a function foo is called without using member expression, it is implicitly a globalThis.foo (if it is not strict mode), or the this argument is simply not passed, therefore it is undefined (in strict mode).
Or let's make it clear: why would you ever want to access this if you are not planning to pass it ?

If you are not calling a function directly, it is the direct caller to decide how it is called. eg. function.prototype.apply, function.prototype.call and function.prototype.bind. As well as Array.prototype.map and so on: if you are not the direct caller, ask its direct caller to pass this.

These are not really corner cases. One is to say "if it is not passed it is not passed", and the other is to say "if you are not the caller you are not the caller". It is just that the runtime gives those internal things some cheating power, so you are allowed to b.bind(a) even if b is not a member of a.

So,
you pass this by "member expression". That's it.

But why it seems so dynamic if it is indeed lexical ?

In statically typed OOP languages like C++, this is exactly an implicit argument. The "left hand side value of the member operator" (. or ->) is passed as an argument, and it depends on the calling convention (or ABI) how it is practically passed.

For rust, and surprisingly, lua, this is an explicit formal argument but an implicit or explicit actual argument, so you won't need the keyword. If you are passing it implicitly, it will be "the left hand side value of the member operator", or it will be what you put in the list.

Lua uses a special member operator : for this particular case, which means, "call the function with the left hand side value of operator : as the first argument". So a:b(...) means exactly a.b(a, ...). A smart and thoughtful design.

You should see how "dynamic" the this argument could be in lua even though it is such beautifully designed. It is the same case in JavaScript.

In statically typed languages, if a function is a member function (or "method" but it really is just a function), it will "always" be called with a "correctly" typed this. It is totally predictable. However, in dynamically typed languages like lua and JavaScript (and Python), you may pass whatever as this. It is lexical, but it is dynamically AND weakly typed.

For example you can never do a f = a.b; f() in C++, but you do it everyday in JavaScript. It is impossible to f = a.b.bind(c) in C++, but this is what JavaScript may do. You need to make it a "delegate" or "callable object" (objects with operator() overloaded) in C++, CSharp and Java but a "closure" is simply as useful as a general "callable object".

C++ treats "member function pointer" differently from a normal function pointer, obviously because there is an implicit this argument to pass, if you don't know what to pass as this you cannot call it. But in JavaScript ? a.b is a function and lets forget about passing this.

TypeScript has a "this argument type restriction" to make it less weak. It works when it works but you have to explicitly apply this restriction, otherwise it would just assume you are being a nice kid and not making troubles around.

But let's say making troubles is the JavaScript way. We enjoy the dynamic world and hopefully it can be a bit lexical and strong. That's JavaScript.

Top comments (0)