Function Borrowing methods in JavaScript and their Polyfills

Function Borrowing methods in JavaScript and their Polyfills

call(), apply() and bind() and creating our own implementations of these methods

Hello everyone, I'm back again with a new article.

In this article we will discuss and learn about the Function Borrowing methods - call(), apply() and bind(). These are called function borrowing methods because the this context for a function can be modified using these methods. Before we jump into this discussion about what these methods do, I think it is important to understand how this works in JavaScript.

this in JavaScript

The value of this in JavaScript is calculated at runtime i.e. when the code is getting executed. At a global level, this value equals the window object. Inside a normal function in an object, this refers to the enclosing object and inside an arrow function it references the value of this from the enclosing lexical context

placeValue of this
global levelequal window (global) object
normal functionsequal to global object
normal functions inside objectsthe current object
arrow functionsvalue of this in the enclosing lexical context

Let's understand all these with a simple example

console.log(this); 
/**
 * window object
 */

function normalFunction() {
  console.log(this);
}
normalFunction()
/**
 * window object
 */


const arrowFunction = () => {
  console.log(this); 
};
/**
 * Does not redefine this.
 * Equal to enclosing scope in this case the window object
 */


const objectWhichContainsNormalFunction = {
  name: "tahir",
  printName: function () {
    console.log(this);
  },
};
objectWhichContainsNormalFunction.printName();
/**
 * Points to the object containing the function
 * {
 *    name: "tahir",
 *    printName: f()
 * }
 */

const objectWhichContainsArrowFunction = {
  fname: "tahir",
  lname: "ahmed",
  printName: () => {
    console.log(this);
  },
  printFullName: function () {
    (() => {
      console.log(this);
    })();
  },
};
objectWhichContainsArrowFunction.printName();
/**
 * In this case the value of this will be equal to the window object.
 *  */

 objectWhichContainsArrowFunction.printFullName();
/**
 * `this` will be equal to the object `objectWhichContainsArrowFunction`
 * This is because the `printFullName()` is a normal function and it redefines 
 * `this` value to the object
 */

With this concept being clear, let's dive into the function borrowing methods.

call

call invokes the functions with a this context and arguments provided individually.

Let's understand it with an example. Consider an object person1 which is as below

const person1 = {
  fname: "john",
  lname: "doe",
  printFullName: function () {
    console.log(`${this.fname} ${this.lname}`);
  },
};
person1.printFullName()
// john doe

There is another object person2, which does not have the printFullName() implementation. We can call printFullName() and provide person2 as the context.

const person2= { 
  fname: "Alfred", 
  lname: "Marshall" 
}

/**
 * Invoking call method below
 */

person1.printFullName.call(person2);
// Alfred Marshall

It is a good practice to have these functions outside. We can pass additional arguments to the call function individually.

const person1 = {
  fname: "Justin",
  lname: "Case",
};

const person2 = {
  fname: "Parsley",
  lname: "Montanna",
};

function greetUser(salutation, message) {
  console.log(`${salutation} ${this.fname} ${this.lname}!!! ${message}`);
}

greetUser.call(person1, "Mr", "Good Morning!");
// Mr Justin Case!!! Good Morning!

greetUser.call(person2, "Ms", "Good Morning!");
// Ms Parsley Montanna!!! Good Morning!

In the above example snippet, greetUser() function defined outside. It expects two arguments salutation and message. In call we pass these arguments individually.

apply

apply() is similar to the call() method with the only difference being the way in which additional arguments are passed to the function while it is invoked.

In the case of apply() we pass all our arguments as an array

const person1 = {
  fname: "Justin",
  lname: "Case",
};

const person2 = {
  fname: "Parsley",
  lname: "Montanna",
};

function greetUser(salutation, message) {
  console.log(`${salutation} ${this.fname} ${this.lname}!!! ${message}`);
}

greetUser.apply(person1, ["Mr", "Good Morning!"]);
// Mr Justin Case!!! Good Morning!

greetUser.apply(person2, ["Ms", "Good Morning!"]);
// Ms Parsley Montanna!!! Good Morning!

bind

bind creates a new function and returns the function with the newly bound this context. It attaches the value of this passed as an argument to the function and returns the function.

Let us understand this with an example. We have an object named user and the function greetUser() within the object. Consider we have an admin role too and there is an object for that. Instead of defining a new function altogether, we can bind the greetUser() of the user object to the admin object and create a new function.

const user = {
  fName: "Kevin",
  sName: "Edwards",
}

const greetUser =  function() {
  console.log(`Welcome ${this.sName}, ${this.fName}`)
}

const admin = {
  fName: "John",
  sName: "Doe",
}

const greetAdmin = greetUser.bind(admin)

greetAdmin()
// Welcome Doe, John

Polyfills for call(), apply() and bind()

Polyfill, on its own, is a very vast concept. It would be too much information in a single article. In short, polyfills allow us to provide modern JavaScript functionalities in browsers that don't support them.

call(), apply() and bind() are available on every function that we create in JavaScript. To make our own functions available in the same way, we have to define the functions on Function Prototype. For more information on what prototype is read here.

Polyfill for call()

call() method calls a function with a new this context and other arguments that the function being called expects. The function would look something as below.

Function.prototype.myCall = function(currentContext = {}, ...args) {
  currentContext.fn = this;
  currentContext.fn(...args);
};

Let us go through each line and understand what is happening.

  • We are creating a polyfill called myCall and adding it to the Function.prototype so that it is available on all the functions.
    • currentContext is the object we want to call our function with
    • ...args - taking the rest of the parameters
  • We then create a property fn on the currentContext object.
  • this points to the function that is invoking or calling the myCall() function
  • We now call the function with its additional parameters by spreading them

Consider the below example where we are using the above polyfill

Function.prototype.myCall = function(currentContext = {}, ...args) {
  currentContext.fn = this;
  currentContext.fn(...args);
};

const person1 = {
  fname: "Justin",
  lname: "Case",
};

function greetUser(salutation, message) {
  console.log(`${salutation} ${this.fname} ${this.lname}!!! ${message}`);
}

greetUser.myCall(person1, "Mr", "Good Morning!");
// Mr Justin Case!!! Good Morning!

We are assigning the greetUser() function to person1 object and redefining the this context to be the invoking object and calling it with the arguments.

Polyfill for apply()

We have seen that call() and apply() are similar except that apply() accepts all additional arguments to be in an array. So their polyfills are also similar

Function.prototype.myApply = function(currentContext = {}, arg = []) {
  currentContext.fn = this;
  currentContext.fn(...arg);
};

Instead of accepting all other arguments using the rest operator, we use a second parameter that accepts an array.

Function.prototype.myApply = function(currentContext = {}, arg = []) {
  currentContext.fn = this;
  currentContext.fn(...arg);
};

const person2 = {
  fname: "Parsley",
  lname: "Montanna",
};

function greetUser(salutation, message) {
  console.log(`${salutation} ${this.fname} ${this.lname}!!! ${message}`);
}

greetUser.myApply(person2, ["Ms", "Good Morning!"]);

Polyfill for bind()

bind returns a new function with a new this context. Before we delve into writing polyfill for bind, let's explore a bit more about how bind works

const admin = {
  fname: "John",
  lname: "Doe",
};

const printDetails = function (city) {
  console.log(`${this.fname} ${this.lname} resides in ${city}`);
};

const printDetailsOfAdmin = printDetails.bind(admin, "Doha");
printDetailsOfAdmin()

// John Doe resides in Doha

In the above codeblock we have an object admin and a function printDetails. When we write the polyfill we need to keep in mind that we have to return a function that can be called later. This function can take additional arguments which need to be passed on when we are returning the function. With these things in mind, let's get started. We will iteratively improve our bind() polyfill so that things are not confusing.

Basic version of bind() polyfill

Function.prototype.myBind = function (...args) {
  const obj = this;
  return function () {
    obj.call(args[0]);
  };
}

This is a very basic implementation of bind() where we are returning a function that calls the method on which are binding the new object i.e., the new this context.

const printDetails = function () {
  console.log(`${this.fname} ${this.lname}`);
};

const admin = {
  fname: "John",
  lname: "Doe",
};

const printAdminDetails = printDetails.myBind(admin)

Consider the above function and the subsequent call to myBind, then this in line 2 of the polyfill will be equal to the printDetails() function. We need to call the function with the object admin which can be done by accepting arguments using the rest operator and the first argument is the object which we need to pass accessible using args[0]. This version works fine as long as we don`t have any extra arguments.

Avanced version of bind() polyfill

One of the very first changes that we need to do to pass other arguments too, is to replace call with apply the reason being we cannot keep track of how many extra arguments need to be passed. Now we need to pass the rest of the arguments apart from the object i.e., args[0] to the function that is getting returned

Function.prototype.myBind = function (...args) {
  const obj = this;
  const params = args.slice(1);
  return function () {
    obj.apply(args[0], [...params]);
  };
};

Now let's try to see how it behaves when we use it on one of the objects

const admin = {
  fname: "John",
  lname: "Doe",
};

const printDetails = function (city, country) {
  console.log(`${this.fname} ${this.lname} resides in ${city}, ${country}`);
};

const printDetailsOfAdmin = printDetails.myBind(admin, "Doha", "Qatar");
printDetailsOfAdmin()

// John Doe resides in Doha, Qatar

This totally works fine but consider the below scenario where we are passing the arguments when we are calling the printDetailsOfAdmin() function.

const printDetailsOfAdmin = printDetails.myBind(admin, "Doha");
printDetailsOfAdmin("Qatar");

// John Doe resides in Doha, undefined

This is happening because we are not passing the arguments that we are receiving when the returned function gets called inside our polyfill.

Function.prototype.myBind = function (...args) {
  const obj = this;
  const params = args.slice(1);
  return function (...callerFunctionArgs) {
    obj.apply(args[0], [...params, ...callerFunctionArgs]);
  };
};

This version of polyfill will work for all cases.

const printDetailsOfAdmin = printDetails.myBind(admin, "Doha");
printDetailsOfAdmin("Qatar");

// John Doe resides in Doha, Qatar

That's all for this article. Thanks for reading.

You can follow me on my Twitter to be notified whenever I'm writing an article and keep updated with my journey in the world of web dev. Thanks for reading.