A polyfill is a piece of code that implements a functionality that is not natively supported by a browser. Old browsers might not have a native functionality provided by newer browser versions. To make it work on older browsers we generally write or use polyfills.

Such Javascript interview questions come in one of the below variations:

  • Implement a polyfill for bind
  • Suppose the browser does not provide a bind functionality, how would you implement it?
  • Implement bind using apply or call

For such questions, you are expected to implement the functionality from scratch.

How to approach 'polyfill type' problems in Javascript Interview?

1) Write down the usage scenarios for the functionality. How is it used? What syntax it follows? If you don't remember the exact syntax, it's ok to ask the interviewer in most of the cases. They should be happy to help.

2) Create test cases. It will make it easier to come up with an implementation that handles edge cases.

3) Implement - Write the code.

4) Do a dry run with the test cases created in the step 2.

Solution

We'll use the approach discussed above to implement a polyfill for .bind() function in javascript.

You can skip this section and jump directly to test cases or implementation if you know the in and out of .bind()

What does .bind() do?

.bind() is a method available on the Function object. So any function that we create should have access to this method.

We use .bind() when we want to execute a function in a particular this context. .bind() returns a new copy of the bound function.

How is .bind() used?

Example below:

const city = {
  name: 'London',
  printCity: function() {console.log(`The city is ${this.name}`)}
};

setTimeout(city.printCity, 100);

The example above logs The city is undefined. This is because setTimeout executes within the global context. So city.printCity will execute with this set to the global context. A name property does not exist on the global object, so it prints undefined.

To fix it, we need to bind city.printCity to the city context.

setTimeout(city.printCity.bind(city), 100);

Sometimes, we need to pass arguments to the bound function. This is how we do it.

const city = {
  name: 'London',
  printCityLove: function(personName) {console.log(`${personName} loves ${this.name} city.`)}
};

setTimeout(city.printCityLove.bind(city, 'George'), 100);

It prints George loves London city.

There's another way you can pass arguments. See below:

const city = {
  name: 'London',
  printCityLove: function(personName) {console.log(`${personName} loves ${this.name} city.`)}
};

const boundFunc = city.printCityLove.bind(city);

setTimeout(boundFunc('George'), 100);

From the section above, these things should be clear:

  • bind is a method available to function objects
  • bind returns a new function for which this is bound to the passed context
  • bind can take argument in the two ways specified above

Based on what we learnt, let's move on the next section, which is formulating test cases.

Formulating test cases

1) No arguments

const boundfn = fn.bind(ctx);

// boundfn: The new function created with bound context
// fn: The function we want to bind
// ctx: The ctx we want fn to be bound to

// Test Case:

const city = {
  name: 'London',
  printCityLove: function() {console.log(`I love ${this.name} city.`)}
};

setTimeout(city.printCityLove.bind(city), 100);

2) Passing Arguments - Case I

const boundfn = fn.bind(ctx, arg1, arg2, arg3) //..

// arg1 - argi: Arguments we want to pass to the function

// Test Case:

const city = {
  name: 'London',
  printCityLove: function(personName) {console.log(`${personName} loves ${this.name} city.`)}
};

setTimeout(city.printCityLove.bind(city, 'Dave'), 100);

3) Passing Arguments - Case II

const boundfn = fn.bind(ctx);
boundfn(arg1, arg2, arg3);

// arg1 - argi: Arguments we want to pass to the function

// Test Case:
const city = {
  name: 'London',
  printCityLove: function(personName) {console.log(`${personName} loves ${this.name} city.`)}
};

const boundFn = city.printCityLove.bind(city);

setTimeout(boundFn('George'), 100);

4) Passing Arguments - Case III

const boundfn = fn.bind(ctx, arg1, arg2);
boundfn(arg3, arg4, arg5);

// arg1 - argi: Arguments we want to pass to the function

// Test Case:
const city = {
  name: 'London',
  printCityLove: function(firstGuy, secondGuy) {console.log(`${firstGuy} and ${secondGuy} love ${this.name} city.`)}
};

const boundFn = city.printCityLove.bind(city, 'Dave');

setTimeout(boundFn('George'), 100);

5) Calling bind on non-functions

const boundfn = notAFn.bind(ctx);

// notAFn: Can be anything but function. e.g. below
const a = {};
const boundfn = [1, 2, 3].bind(a); // Should throw Type Error

Now we have the basic test cases ready, let's move on to the implementation. It's always good to have the test cases ready before jumping onto the implementation. It would help you to come up with an implementation that will handle majority of scenarios if not all.

Implementation

I advice you to try implementing the bind polyfill on your own, before you see the solution below.

Let's see a simple implementation for a bind polyfill below.

if (!Function.prototype.bind) {
  Function.prototype.bind = function() {
    const thisCtx = this;
    const boundCtx = arguments[0];
    const boundArgs = Array.from(arguments).slice(1);

    if (typeof thisCtx !== 'function') {
      throw new TypeError('Not a valid function');
    }

    return function() {
      const allArguments = boundArgs.concat(Array.from(arguments));
      thisCtx.apply(boundCtx, allArguments);
    };
  };
}

Let's go over it line by line.

if (!Function.prototype.bind) {}

This line checks if bind method already exists in the Function prototype chain. If not, we want to create a new method on the Function object. This check makes sure we don't override any existing method.

Function.prototype.bind = function() {}

Let's create our own bind method if it does not already exist on the function object. We need to add it to the prototype so that this method is available for any function we create.

Now let's get the basic structure ready. Bind returns a new function. Remember? So we need to return a function.

if (!Function.prototype.bind) {
  Function.prototype.bind = function() {
    return function() {};
  };
}

Till now we are done with the basic setup for our implementation. Now comes the core functionality.

const thisCtx = this;

The above line refers to the original function context. We need to save it somewhere, so that we don't loose a reference to it later. So, we use thisCtx

// const boundFn = fn.bind(boundCtx, boundsArgs...)

const boundCtx = arguments[0];
const boundArgs = Array.from(arguments).slice(1);

The first line above gets the first argument passed to the bind function. The first argument is the context we want the function to be bound to.

The second line gets us rest of the arguments that we want to pass down to the function.

See the usage of Array.from above. arguments is an array like object, but not an array. So slice method does not exist on arguments. So we need to convert it to an array first.

Now, we have the structure ready, we have the current context, bound context and the bound arguments.

Next we need to check if the .bind() was called with a function or not. See test case 5 above.

if (typeof thisCtx !== 'function') {
  throw new TypeError('Not a valid function');
}

The final thing:

return function() {
  const allArguments = boundArgs.concat(Array.from(arguments));
  thisCtx.apply(boundCtx, allArguments);
};

Let's revisit test case #4 again:

const boundfn = fn.bind(ctx, arg1, arg2);
boundfn(arg3, arg4, arg5);

// arg1 - argi: Arguments we want to pass to the function

That's what we are doing with the first line above. You see there are two places arguments are passed in here. We need to concat all those arguments before we pass them into our core function.

Lastly, we make use of .apply() to call the original function with two parameters - the bound context and the arguments arrays we just concatenated.

The most important part of this entire exercise is the use of .apply() method. This can be achieved using .call() as well. There are a multiple other concepts involved here, namely:

  • Closure
  • Use of this and scopes
  • Array like objects (arguments)
  • Use of call and apply
  • How you check for edge cases

Dry Run

Once your implementation is ready, go back to your test cases and do a dry run over your code. A dry run just makes sure you are handling as many cases as possible. And you always get extra points when you follow a test driven approach to development.

Found it useful? Don't forget to subscribe to the mailing list below.

Found any bugs or issues with the article, please get in touch at https://www.facebook.com/jsInterview