Implement custom call() in JavaScript

In my last article, I used Function.prototype.call() to invoke parent constructor in child constructor, in order to inherit parent properties. The call() function is a very interesting feature in JavaScript, and I was asked to implement my custom call() function during a interview, unfortunately I didn’t make it through. Now with the help from mqyqingfeng’s blog, I finally made it.

According to MDN,

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

Take an example:

var person = {
    name: "Bob",
    age: 28
}

var dog = {
    name: "woof",
    age: "woof-woof"
}

function identify() {
    console.log(this.name + ", " + this.age);
}

identify.call(person); // Bob, 28
identify.call(dog); // woof, woof-woof

we can see there are two steps in call(): change this in the function, then execute the function.

Change caller function’s this

So in my custom implementation, we can set the function as a method of the callee object, it will change this in the function. Then we invoke this method, and delete it when we are done.

var person = {
    name: "Bob",
    age: 28
}

var dog = {
    name: "woof",
    age: "woof-woof"
}

function identify() {
    console.log(this.name + ", " + this.age);
}

// identify.call(person); // Bob, 28
// identify.call(dog); // woof, woof-woof

Function.prototype.myCall = function (context) {
    context.fn = this;
    context.fn();
    delete context.fn;
}

identify.myCall(person); // Bob, 28
identify.myCall(dog); // woof, woof-woof

It works. However it has not been done yet, because the real call() can accept arguments.

Pass arguments

var person = {
    name: "Bob",
    age: 28
}

var dog = {
    name: "woof",
    age: "woof-woof"
}

function identify(isHuman) {
    console.log("is " + this.name + " human? " + isHuman);
}

identify.call(person, true); // is Bob human? true
identify.call(dog, false); // is woof human? false

In call() function, the first argument is the new this, and the rest are those you want to pass to the function. Luckily, JavaScript functions have a built-in object called the arguments object, contains an array of the arguments used when the function was called (invoked).

So in function call(this, args1, args2, ...argsN), argument[0] is the new this, and arguments[1] to arguments[arguments.length-1] are the real args1 to argsN. Then we can use eval() to assemble the function with arguments.

Function.prototype.myCall = function (context) {
    // get arguments
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i + "]");
    }
    context.fn = this;
    // args[] will call toString() in eval()
    eval("context.fn(" + args + ")");
    delete context.fn;
}

Return value

Further, functions can have a return value, for example:

var person = {
    name: "Bob",
    age: 28
}

var dog = {
    name: "woof",
    age: "woof-woof"
}
        
function identify(isHuman) {
    console.log("is " + this.name + " human? " + isHuman);
    return {
        name: this.name,
        age: this.age,
        isHuman: isHuman,
    }
}

var info1 = identify.call(person, true); 
// is Bob human? true

console.log(info1); 
// {name: "Bob", age: 28, isHuman: true}

var info2 = identify.call(dog, false); 
// is woof human? false

console.log(info2); 
// {name: "woof", age: "woof-woof", isHuman: false}

That is not that difficult, we just need assign the assembled function to a variable and return it.

Function.prototype.myCall = function (context) {
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i + "]");
    }  
    context.fn = this;
    var result = eval("context.fn(" + args + ")");
    delete context.fn;
    
    return result;
}

When this == null

One more corner case, in the real call() function, when the first argument is null, this will pointer to window object.

So add one more condition for context == null, then the final implementation looks like below:

var person = {
    name: "Bob",
    age: 28
}

var dog = {
    name: "woof",
    age: "woof-woof"
}

var name = "it";
        
function identify(isHuman) {
    console.log("is " + this.name + " human? " + isHuman);
    return {
        name: this.name,
        age: this.age,
        isHuman: isHuman,
    }
}
        
identify.call(null, "maybe"); // is it human? maybe

var info1 = identify.call(person, true); 
// is Bob human? true

console.log(info1); 
// {name: "Bob", age: 28, isHuman: true}

var info2 = identify.call(dog, false); 
// is woof human? false

console.log(info2); 
// {name: "woof", age: "woof-woof", isHuman: false}
        
Function.prototype.myCall = function (context) {
    var context = context || window;
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
        args.push("arguments[" + i + "]");
    }  
    context.fn = this;
    var result = eval("context.fn(" + args + ")");
    delete context.fn;
    
    return result;
}


identify.myCall(null, "maybe"); // is it human? maybe

var info3 = identify.myCall(person, true); 
// is Bob human? true

console.log(info3); 
// {name: "Bob", age: 28, isHuman: true}
        
var info4 = identify.myCall(dog, false); 
// is woof human? false

console.log(info4); 
// {name: "woof", age: "woof-woof", isHuman: false}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s