Sunday, December 22

Taming <code>this</code> In JavaScript With Bind Operator

Do you want to discover the next exciting JavaScript features that you didn’t even know you needed? In this article, I will introduce one of these proposals that if accepted may change the way you write code the same way the spread operator did.

However, here’s a small disclaimer: This feature is under development and discussion. The goal here is to add some hype around it and create awareness of the hard work that TC39 is doing to find consensus, fix all the syntax and semantics issues and have it shipped with the next releases of ECMAScript. If you have any concerns, comments or desire to express your support, please go to the TC39 proposals repository, add a star to this feature to show your support, open an issue to voice your concerns and get involved.

But before, I want to ask a simple (but tricky) question:

What is this?

In ECMAScript, this has a different semantic than this in many other programming languages, where this often refers to the lexical scope. In general, this behaves differently in the global scope, within a function, in non-strict mode and strict mode. Let’s break this behavior down into small examples.

this In The Global Scope

What is the value of this in this example?

console.info(this);

At the global scope, this refers to the global object, like the window in the browser, self on web workers and the module.exports object in NodeJS.

this In The Function Scope

At the function scope, this behaves depending on how the function is called, and this aspect makes it tricky to predict its value. We can understand it better by checking the following examples:

What Is The Value Of this Here?

function foo() {
  return this;
}

console.info(this);

Inside a function, this starts to have an interesting behavior since its value depends on how the function is called. In the example above, this still refers to the global scope, with one difference. In NodeJs, this will point to the global object instead of module.exports.

Setting A Value Into this:

function foo() {
  this.bar = 'baz';
  return this;
}

console.info(foo());
console.info(new foo());

Setting a value into this sets the value into the current context. The example above logs the global scope with the property bar with the value baz in the first console.info, but it logs only { bar: ‘baz’ } in the second console.info. It happens because the new operator among other things bounds the value of this to the newly created object.

This Keyword In The Strict Mode

In strict mode, the this variable doesn’t carry the value of the context implicitly, this means if its context isn’t set, the value of this is default to undefined as shown in the following snippet.

function foo() {
  "use strict";
  return this;
}

console.info(foo()); //undefined

To set the context of this in strict mode you can set the function as member of an object, use new operator, Function.prototype.call(), Function.prototype.apply() or Function.prototype.bind() methods for example.

function foo() {
  "use strict";
  return this;
}

var a = { foo };

foo(); // undefined
a.foo(); // { foo: ƒunction }
new foo(); // Object foo {}
foo.call(this); // Window / Global Object
foo.apply(this); // Window / Global Object
foo.bind(this)(); // Window / Global Object

Making this Variable Predictable

At this point, you may realize that the value of this in ECMAScript is quite tricky to predict. To demonstrate the available techniques to make it predictable, I’d like to present the following example that mimics a common use case of this.

<button id="button">? ?</button>
<script>
  class MeowctComponent {
    constructor() {
      this.paw = document.getElementById('button');
    }

    meow() {
      console.info('? on this: ', this.paw);
    }
  }

  const cat = new MeowctComponent();
  cat.paw.addEventListener('click', cat.meow);
</script>

In the example above, I created a MeowctComponent, which has only one property paw that points to the button element and one method called meow that should print the paw instance property into the console.

The tricky part is that the meow method is executed only when the button is clicked, and because of that, this has the button tag as context, and since the button tag does not have any paw property, it logs the undefined value into the console. Tricky, isn’t it?

To fix this specific behavior we can leverage on the Function.prototype.bind() method to explicitly bind this to the cat instance, like in the following example:

<button id="button">Meow</button>
<script>
  class MeowctComponent {
    constructor() {
      this.paw = document.getElementById('button');
    }

    meow() {
      console.info('? on this: ', this.paw);
    }
  }

  const cat = new MeowctComponent();
  cat.paw.addEventListener('click', cat.meow.bind(cat));
</script>

The method .bind() returns a new permanently bound function to the first given parameter, which is the context. Now, because we bound the cat.meow method to the cat instance, this.paw inside the meow method correctly points to the button element.

As an alternative to the Function.prototype.bind() method, we can use the arrow function to achieve the same result. It keeps the value of the lexical this of the surrounding context and dispenses the need to bind the context explicitly, like in the next example:

<button id="button">? Meow</button>
<script>
  class MeowctComponent {
    constructor() {
      this.paw = document.getElementById('button');
    }

    meow() {
      console.info('? on this: ', this.paw);
    }
  }

  const cat = new MeowctComponent();
  cat.paw.addEventListener('click', () => cat.meow());
</script>

Although arrow functions solve the majority of use cases where we need to bind the lexical this explicitly, we still have two use cases for which the use of the explicit bind is needed.

Calling A Known Function Using this To Provide Context:

let hasOwnProp = Object.prototype.hasOwnProperty;
let obj = Object.create(null);

obj.hasOwnProperty('x') // Type Error...

hasOwnProp.call(obj, "x"); //false

obj.x = 100;

hasOwnProp.call(obj, "x"); // true

Let’s suppose for any reason we have this obj object that doesn’t extend Object.prototype but we need to check if obj has an x property by using the hasOwnProperty method from Object.prototype. To achieve that, we have to use the call method and explicitly pass obj as the first parameter to make it work as expected, which appears not to be so idiomatic.

Extracting A Method

The second case can be spotted when we need to extract a method from an object like in our MeowctComponent example:

<button id="button">? ?</button>
<script>
  class MeowctComponent {
    constructor() {
      this.paw = document.getElementById('button');
    }

    meow() {
      console.info('? on this: ', this.paw);
    }
  }

  const cat = new MeowctComponent();
  cat.paw.addEventListener('click', cat.meow.bind(cat));
</script>

These use cases are the baseline problem that the bind operator tries to solve.

The Bind Operator ::

The Bind operator consists of an introduction of a new operator :: (double colon), which acts as syntax sugar for the previous two use cases. It comes in two formats: binary and unary.

In its binary form, the bind operator creates a function with its left side is bound to this of the right side, like in the following example:

let hasOwnProp = Object.prototype.hasOwnProperty;
let obj = Object.create(null);

obj.hasOwnProperty('x') // Type Error...

obj::hasOwnProp("x"); //false

obj.x = 100;

obj::hasOwnProp("x"); // true

That looks more natural, doesn’t it?

In its unary form, the operator creates a function bound to the base of the provided reference as a value for this variable, like in the following example:

...
cat.paw.addEventListener('click', ::cat.meow);
// which desugars to
cat.paw.addEventListener('click', cat.meow.bind(cat));
...

What’s so cool about the bind operator is the fact that it opens up new opportunities for creating virtual methods, as in this example of lib for iterable.

import { map, takeWhile, forEach } from "iterlib";

getPlayers()
  ::map(x => x.character())
  ::takeWhile(x => x.strength > 100)
  ::forEach(x => console.log(x));

It’s super useful because the developer doesn’t need to download the whole lib to do small stuff, which reduces the amount of imported JavaScript. Besides, it makes those kinds of libs easier to extend.

How To Develop Using Bind Operator

To keep the example simple, let’s suppose we need to create a math module which the developer can chain the operations to form an math expression that, given a number as an entry it could make all calculations into a pipeline. The code to achieve this is simple and could be written as the following.

function plus(x) {
  return this + x;
}

function minus(x) {
  return this - x;
}

function times(x) {
  return this * x;
}

function div(x) {
  return this / x;
}

As you can spot in the example above, we expect to have the value as a context and we use this to make the calculation, so then using the bind operator, we could make an expression like the following:

1::plus(2)::times(4)::div(3)::minus(1); // returns 3

Which is equivalent to:

minus.call(div.call(times.call(plus.call(1, 2), 4), 3), 1);

The first snippet looks more idiomatic, isn’t?

Going a little further, we can use it to convert a temperature from Celsius to Fahrenheit, this can be accomplished by the following function expression:

const toFahrenheit = x => x::times(9)::div(5)::plus(32);
console.info(toFahrenheit(20)); // 68

So far, we demonstrate how create functions to interact with the values, but what about extending the object with virtual methods? We can do new stream compositions mixing built-in methods with custom ones. To demonstrate it, we can compose string methods with custom ones. First, let’s check the module with the custom methods with its implementation.

function capitalize() {
  return this.replace(/(?:^|s)S/g, a => a.toUpperCase());
}

function doubleSay() {
  return `${this} ${this}`;
}

function exclamation() {
  return `${this}!`;
}

With this module in place we can do cool things like the following:

const { trim, padEnd } = String.prototype;

console.info(
  '   hello world   '
    ::trim()
    ::capitalize()
    ::doubleSay()
    ::exclamation()
    ::padEnd(30)
);

// "Hello World Hello World!      "

In the example above, you can spot that I extracted two methods from the String.prototype, trim() and padEnd(). Since these methods are extracted, I can use them to compose my stream of methods alongside with my virtual methods capitalize(), doubleSay() and exclamation(). This aspect is what makes bind operator so exciting and promising.

Advantages And Disadvantages Of Bind Operator

As you may realize at this point, there are some aspects that Bind Operator shines. Those are the following:

  • It covers the only two missing use cases that explicit bind is necessary;
  • It makes easy to make this variable to be predictable;
  • It adds a new way to extend functionality by using virtual methods;
  • It helps to extend built-in objects without extending the prototype chain. Do you remember Smoosh Gate?

In the other side, to compose functions with bind operator you need to rely on this to be bound, that can lead to some issues like in this example:

const plus = (x) => this + x;

console.info(1::plus(1));
// "[object Window]1"

As it becomes clear in the example above, it’s not possible to compose arrow function with bind operator, since it’s not possible to bind this to an arrow function. Sometimes users don’t want to rely on this to be bound to compose their behavior through a function chain, which could be a problem if you only use bind operator to achieve this.

Another issue that is often said is the possible syntax overload that the bind operator can bring which can be a problem to onboard newcomers to the language. Realizing that a specific operator works in binary and unary form is tricky as well. One possible solution for this is to introduce the binary form to the language separately of the unary form. So once the binary form is integrated to the language, the committee can reassess if the unary form is still necessary. Meanwhile, users can get used to the binary form, and the syntax overload could potentially be mitigated.

Conclusion

Predict the value of this in JavaScript is trick. The language has some rules to explain how the context is assigned to this, but in the daily basis we want to make this value predictable. The Function.prototype.bind() method and arrow functions help us to make the value of this predictable. The bind operator comes to play to cover the two use cases that we still need to explicitly bind this.

The advent of bind operator opens an opportunity to create a new set of function composition via virtual methods, but it can add a syntax overload making difficult to onboard newcomers to the language.

The author of the bind operator is Kevin Smith, and this proposal is in Stage 0. The TC39 is open to feedback. If you like this feature and think that it’s useful, please add a star in the repository, if you have an Idea to solve the issues presented here, if you have another way to shape the syntax or semantics of this features or if you spot another issue with it, please open an issue in the repo and share your thoughts/ideas with the committee.

Source: Smashingmagazine.com

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x