CODEX

Binding in JS explained

Adam Reeves
CodeX
Published in
6 min readFeb 28, 2021

--

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.

I’m not even sorry

Here’s what’s actually happening:

  1. We declare the function sayHello which simply logs this.hello
  2. We assign the string ‘hello there’ to the hello variable in the global scope
  3. 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 the hello 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 the let does not create properties of the window 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.

--

--

Adam Reeves
CodeX

Professional Software Engineer, man of many hobbies