CODEX
Binding in JS explained
In my last post about the prototype chain, I touched very briefly on the this
keyword and binding — and now the time has come to give it our full attention.
It’s one of the more confusing areas of JavaScript, so make sure your thinking hat is nearby, have a seat and let’s get into it.
To really understand the behaviour of this
, there are 4 binding rules we need to pay attention to.
As a quick overview, here they are in ascending order of precedence (lowest to highest):
- Default binding
- Implicit binding
- Explicit binding
- New binding
Now, for the main course 🥗…
Default binding
The default binding is basically the catch-all when none of the other binding rules apply — this’ll be easier to identify when we’ve looked at the other rules.
With the default binding, this
is bound to the global object. Take the following example:
You might be surprised to see that 'hello there'
is printed when we call sayHello
.
Here’s what’s actually happening:
- We declare the function
sayHello
which simply logsthis.hello
- We assign the string ‘hello there’ to the
hello
variable in the global scope - When we call
sayHello()
,this
is bound to the global object as no other binding rules apply due to it being a plain old function call in the global scope. As we’ve defined thehello
variable on the global object,this.hello
resolves to it.
Now, you might be thinking, if we remove this
and simply logged the hello
variable, due to the rules of lexical scope you’re probably already familiar with, you’d expect ‘hello there!’ to be printed.
And this leads me on to a very important point:
Lexical Scope and Binding Contexts are completely unrelated!
To demonstrate, let’s modify the first example a little:
If the rules of lexical scope applied to this
you might expect this.response
to equal 'general kenobi!’
. However, as they’re completely disparate concepts this.response
is actually undefined.
This is a pretty basic example, and most IDE’s and text editors are smart enough to highlight that the response
variable is not being used but you get the idea.
This is also the exact reason, replicating this behaviour is slightly different in Node.js!
Say this code was saved in index.js
and you run it using node index.js
— due to the internals of node, the top level scope of the file is not global as you might expect. index.js
is wrapped by Node which moves the global scope to a higher level, outside the file — so this
is still bound to the global object, but hello
is not set on the global object. That’s why we need to set hello
explicitly on the global
object in the example.
For simplicity, subsequent examples in this article will assume they are being run in the browser.
`let` it be
Now that I’ve got the Beatles stuck in your head, this brings me on to a side note:
If we tried the same example in the browser and replaced var
with the let
keyword in ES6, we’d get undefined
. This excerpt from MDN explains why:
Just like
const
thelet
does not create properties of thewindow
object when declared globally (in the top-most scope).
strict mode
JavaScript code can be run in strict mode by including 'use strict'
as the first statement in your code.
Let’s modify the previous example to demonstrate the difference using strict mode:
Paste this into dev tools in your browser and you’ll get the following error:
VM149:4 Uncaught TypeError: Cannot read property 'hello' of undefined
at demoDefaultScope (<anonymous>:4:22)
at <anonymous>:9:1
In strict mode, this
isn’t forced into being an object, therefore when we try to log this.hello
this time, this
is undefined
resulting in the above error.
Implicit Binding
When a function is called from within the context of an object, this
is set to the object.
In this example, we declare obj
using object literal notation, which has the properties, sayHello: sayHello()
and hello: ‘hello there'
.
When we call obj.sayHello()
, this
is bound to obj
, therefore sayHello()
is able to resolve to the hello
property defined in obj
.
Be careful with implicit bindings as, if the binding is lost, this
will default to the default binding:
When we assign obj.sayHello
to the variable yoink
, yoink
is simply referencing the sayHello
function, obj
isn’t involved at all. Therefore this
will default to the global scope as per the default binding rule.
Explicit Binding
The call
, apply
, and bind
functions exist on the Function prototype which means every function you declare will have access to these functions by default.
Call
and apply
are quite similar and allow you to call a function with this
explicitly stated. The only difference between call
and apply
is that call
requires arguments to be passed individually and apply
takes them as an array.
In this example, we modify sayHello
to print the firstName
and lastName
passed in along with the value of this.hello
. If the string within the console.log
looks a little unfamiliar it’s because it’s using template literals which were introduced in ES6.
In both sayHello.call
and sayHello.apply
we call sayHello with this
set to obj
, we can see from the output that this.hello
successfully resolves to obj.hello
.
bind()
The bind
function is used to return a new function with this
bound to the provided value.
This behaviour is known as hard binding as every time we call the function returned from bind
it will always be called with the this
we passed as an argument to bind
.
In this example, sayHello.bind(obj)
returns a new function with the same implementation as sayHello
with this
bound to obj
. So when we call boundFunction(firstName, lastName)
we get the expected output as this
is set to obj
.
currying with bind()
Another interesting side note about bind
is that it supports currying.
Currying is the transformation of a function f(a, b)
to f(a)(b)
. It’s particularly useful when you want to avoid repeatedly calling a function with the same initial arguments.
Let’s modify our example to use bind
with currying:
The sayHello()
function is refactored slightly, so that lastName
is passed in first.
sayHello.bind(obj, lastName)
returns a new function boundFunction
with this
bound to object and it also partially applies the argument lastName
with the value 'Skywalker'
.
This is handy as we can now keep calling bound function without having to repeat the first argument Skywalker
.
New binding
Last and certainly not least is the new binding rule which is the highest order of precedence out of the bindings.
In JavaScript, a function that is called with the new
keyword is known as a constructor function, it’s still a normal function —but it’s behaviour is modified by the new
keyword.
When we call a function with the new
keyword and assign it to a new object obj
, the function being called uses the new object obj
as the this
binding.
For example:
When we call let jedi = new Jedi('purple', 'master')
, the new object jedi
is first created and used as the this
binding by the Jedi
function. So this.lightsaber
and this.rank
within the Jedi
function refer to the jedi
object.
We can see this is the case when we log jedi
:
You might’ve noticed that Jedi
also shows up in the log. That’s because the constructor property on the jedi
prototype points to the function it was constructed by, in this case, the Jedi
function.
Arrow functions don’t play by the rules
What would software engineering be without a proverbial spanner to throw in the works 🔧
As of ES6, arrow functions became a thing and when it comes to the binding of this
, arrow functions use the same this
value as their enclosing scope.
To demonstrate this point, I’ve modified the example in the implicit binding section to the following:
Within sayHello()
we define an arrow function assigned to sayHelloAgain
which, surprise surprise, logs this.hello
. I also declared the hello
variable just to show it isn’t used.
When sayHelloAgain
is called, the output is exactly the same as this
is the value of the arrow functions enclosing scope sayHello
.
If you’re still reading, congrats on hanging in there — this is by far one of the more confusing aspects of JavaScript!
Hopefully you’ve found this article helpful and I haven’t ruined Star Wars for you anymore than Disney did for me 😬
Leave a comment if you questions or if I’ve missed something you think is important, — I’d be happy to hear from you.