DEV Community

Cover image for Understanding by making your own: JavaScript call, apply & bind
Kamaal Aboothalib
Kamaal Aboothalib

Posted on

Understanding by making your own: JavaScript call, apply & bind

It ultimately makes sense not to reinvent the wheel, but it's also of a way to Improve yourself by (re)creating things that were already existing. I'm writing this series to mainly improving my understanding of how things work in JavaScript's Standard built-in objects like call, apply, bind.

Function.prototype.call()

The call() method calls a function with a given this value and arguments provided individually. -- MDN

Mozilla

Initially, the method call invokes the function and allows you to pass comma-separated arguments.

Example from MDN


function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

const food = new Food('cheese', 5)
console.log(food.name) //  cheese
console.log(food) //  {name: 'chees', price: 5, category: 'food'}

const fun = new Toy('robot', 40);
console.log(fun.name) //  robot

Custom example


const add = (a, b) => a + b
console.log(add.call(null, 3, 8, 10)) // 11

Above examples, we can understand the basic functionality of the call method.

  • Call changes the this context of the caller, In the above example Product.call replaces the this from its original function body with the first argument of call, That is Food. > Using the call to chain constructors for an object -- MDN

call context

  • If call called with more than one arguments then in left to right order, starting with the second argument, pass each argument to the original function.

    • in our case name and price.
  • The call should not make any side effect on the this object.

The thisArg value is passed without modification as this value. This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value. Even though the thisArg is passed without modification, non-strict functions still perform these transformations upon entry to the function. -- Ecma

Ecma-spec

Lets re-implement the call method.


if(!Function.prototype.fauxCall){
    Function.prototype.fauxCall = function(context){
       context.fn = this;
       return context.fn();
    }
}


const food = new Food('cheese', 5)
console.log(food) //  expected {name: 'chees', price: 5, category: 'food'}

If we run the above code, we'll get

Call result

instead of


{name: 'chees', price: 5, category: 'food'}

Ok, we need to pass original arguments when we call fn(). Seems easy, but 🤔 how do we know how many arguments are coming from the original call?

Here we can use arguments it is Array-like object accessible inside the function, but still, we have a problem; remember arguments is not an array its an object that's why Array-like

We can convert this object to array with Array.from(more ways), then ignore the first argument by Array.slice from the second element.

if(!Function.prototype.fauxCall){
    Function.prototype.fauxCall = function(context){
       const args = Array.from(arguments).slice(1);
       context.fn = this;
       return context.fn(...args);
    }
}

const food = new Food('cheese', 5)
console.log(food) //  expected {name: 'chees', price: 5, category: 'food'}

If we run the above code, we'll get

Call result

Ok looks good, but still, we can see the side effect. Get rid of the side effect we can use delete operator, however, even if we can delete this side effect fn property that we created we have one more problem; if context already has a property with the same name fn. In this case, should form the random key then assign it to context then we have to delete it.

if(!Function.prototype.fauxCall){
    Function.prototype.fauxCall = function(context){
       const fnName =
    [...Array(10)].map(_ => ((Math.random() * 36) | 0).toString(36)).join`` ||
    {};
       const args = Array.from(arguments).slice(1);
       context[fnName]= this;
       const result = obj[fnName](...args); 
       delete obj[fnName];
       return result;
    }
}

const food = new Food('cheese', 5)
console.log(food) //  expected {name: 'chees', price: 5, category: 'food'}

If we run the above code, we'll get

Call success

Almost success, but if we call with null instead of the object we'll get an error.

Remember our add function? if we want to fauxCall add function without this argument we'll get error

const add = (a, b) => a + b;
console.log(add.fauxCall(null, 5, 6, 7)); // 11 :: 7 will ignore by add method

Call error

It's because o we're trying to set a property to a null object, and we can fix it by guard function.

Also, add two more methods to check the existing property and assign new property instead of static fnName variable.

  1. getRandomKey : this function generates and returns a random string each time.
  2. checkRandomKey : this function takes two arguments; key and context (object) and checks this object already has the same key as property if-then recurse it with the new key, until finding a unique new property for the property name.

Complete implementation

const isOBject = obj => {
    const type = typeof obj;
    return type === "function" || (type === "object" && !!obj);
};

const getRandomKey = () => {
    return (
    [...Array(10)].map(_ => ((Math.random() * 36) | 0).toString(36)).join`` ||
    {}
  );
};

const checkRandomKey = (key, obj) => (obj[key] === undefined) ? key : checkRandomKey(getRandomKey(), obj);

if(!Function.prototype.fauxCall){
    Function.prototype.fauxCall = function(_context) {
       const context = isOBject(_context) ? _context : {};
       const fnName = checkRandomKey(getRandomKey(), context);
       const args = Array.from(arguments).slice(1);
       context[fnName] = this;
       const result = context[fnName](...args);
       delete context[fnName];
       return result;
    };
}

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.fauxCall(this, name, price);
  this.category = "food";
}
const add = (a, b) => a + b;

console.log(new Food("cheese", 5)); // {name: 'chees', price: 5, category: 'food'}
console.log(add.fauxCall(null, 5, 6, 7)); // 11 :: 7 will ignore by add method

Function.prototype.apply()

The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object). -- MDN

Mozilla

Initially, the method apply invokes the function and allows you to pass an array or array-like arguments. Sound familiar? yes because call and apply almost doing the same thing only different is call accept comma-separated arguments while apply accepts array or array-like object as the argument.

In this case, everything that we did for the call is valid for apply except args part, now we know exactly which argument should go with the function call.

//... all call helper codes
if(!Function.prototype.fauxApply){
    Function.prototype.fauxApply = function(_context, _args) {
        const context = isOBject(_context) ? _context : {};
        const fnName = checkRandomKey(getRandomKey(), context);
        const args = _args.length ? _args : []
        context[fnName] = this;
        const result = context[fnName](...args);
        delete context[fnName];
        return result;
    };
}
const numbers = [5, 6, 7];

console.log(new Food("cheese", 5)); // {name: 'chees', price: 5, category: 'food'}
console.log(add.fauxApply(null, 5, 6, 7)); // 11 :: 7 will ignore by add method


Function.prototype.bind()

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

Only different between call and bind is call invoke the function and returns the value but bind returns a new function with updated context.

So we can simply return a new function that calls call with arguments and context.

//... all call helper codes
Function.prototype.fauxBind = function(_contetxt){
  const args = Array.from(arguments).slice(1);
  const self = this;
  return function(){
      //return self.fauxApply(_contetxt, args)
      return self.fauxCall(_contetxt, ...args) // either call or apply
  }
}
console.log(add.fauxBind(null, 4,7)());

CodeSandbox

Edit js-call-apply-bind

♥

This implementation here is one of many ways. The purpose of this simulation is only to get how call works under the hood. If you find any issue or typo please let me know.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.