学会手写这三个方法,就已经超过了88%的初级前端

手写题是前端面试中经常遇到的,可以考察面试者的基础能力。

callapply 和 bind是 Function.prototype 上的三个方法。这三个方法的基本用法介绍,我们在上一篇“聊聊 call、apply 和 bind”中已经进行了介绍。

如果在前端面试中,如果面试官要求手写这三个方法的模拟实现,小伙伴们是否能顺利写出来呢?今天我们就来看看这几个方法如何模拟实现。

call的模拟实现

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

在开始实现call函数之前,先要思考如何修改this的指向,其实很简单,直接把目标函数挂载到指定的上下文中。

  • ES3版本
Function.prototype.call = function (context{
    context = context ? Object(context) : window
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

有同学可能会对 'context.fn(' + args +')' 这段感到奇怪,其实里面有个js的隐式转换的知识点。

比如'(' + [1,2,3] + ')' 会输出什么?答案是 (1,2,3)

上述代码中,为了执行函数,使用了evaleval会将传入的字符串当做 JavaScript 代码进行执行。

console.log(eval('2 + 2'));
// expected output: 4

console.log(eval(new String('2 + 2')));
// expected output: 2 + 2

console.log(eval('2 + 2') === eval('4'));
// expected output: true

eval() 是一个危险的函数,不建议大家使用,这儿也是给大家举个例子。

当然es6的扩展运算符也可以实现相同的功能。

  • ES6版本
Function.prototype.call = function (context{
  context = context ? Object(context) : window
  context.fn = this;

  let args = [...arguments].slice(1);
  let result = context.fn(...args);

  delete context.fn
  return result;
}

还是 ES6 版本的模拟实现看上去更加清爽。

apply的模拟实现

apply() 方法调用一个具有给定 this 值的函数,以及作为一个数组(或类似数组对象)提供的参数。

applycall 的作用是一样的,区别是 apply 是将参数作为一个数组传入,而 call 是将参数一个一个的传入。

所以就直接上代码:

  • ES3版本
Function.prototype.apply = function (context, arr{
    context = context ? Object(context) : window
    context.fn = this;

    var result;
    // 判断是否存在第二个参数
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }

    delete context.fn
    return result;
}
  • ES6版本
Function.prototype.apply = function (context, arr{
    context = context ? Object(context) : window
    context.fn = this;
  
    let result;
    if (!arr) {
        result = context.fn();
    } else {
        result = context.fn(...arr);
    }
      
    delete context.fn
    return result;
}

bind的模拟实现

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

实现 bind 主要有三个要点:

  • 返回一个函数
  • 可以预设参数
  • 生成的绑定函数可以使用 new 操作符。

返回函数和预设参数我们可以用 apply 来实现,大致的效果如下。

Function.prototype._bind = function (thisArg{
    let self = this;
    let args = Array.prototype.slice.call(arguments1);
    let fBound = function ({
        let bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(thisArg, args.concat(bindArgs));
    }
    return fBound;
}

function sum(c, d{
    return this.a + this.b + c + d;
}

let obj = {a1b2}

let t = sum._bind(obj, 3);

console.log(t(4)) //10

下面就是要实现 new 调用。

当使用 new 调用绑定函数,this 将指向绑定函数的原型,我们要的效果是原型指向的是原函数的 prototype,那么最直接的想法就是将绑定函数的 prototype 指向原函数的 prototype 即可。

但是这样做有一个问题就是当我们后面改变绑定函数的 prototype,原函数的 prototype 也会被修改,他们指向的是同一个对象。基于这样的原因我们需要在中间加一层。最终的实现如下:

Function.prototype._bind = function (thisArg{
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    let self = this;
    let args = Array.prototype.slice.call(arguments1);

    let fNOP = function ({};
    let fBound = function ({
        let bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : thisArg, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();

    return fBound;
}

function sum(c, d{
    console.log(this.a, this.b) //undefined undefined
    this.a = c;
    this.b = d;
}

let obj = {a1b2}

let t = sum._bind(obj, 3);

let m = new t(45)

console.log(m) //{3, 4}

我们可以看到最后结果使用的参数是 bind 的时添加的一个参数和 new 添加的第一个参数,new 的多余参数被忽略。这也是 bind 的另一个功能,可以预设参数。

而我们也发现 bind 绑定的 obj 没有生效,这部分我们是用 instanceof 判断调用绑定函数时的 this 来判断的,如果是 new 调用,那么这个 thisfNOP 的实例(如果是直接调用,那么这个 this 会是全局对象,浏览器环境就是 window 对象)。

最后

已经到了6月中旬,大厂的秋招提前批,还有半个月就要开始了。祝应届的同学们都能在秋招中,收获让自己满意的offer。

这儿也打个广告,《前端面试题宝典》经过近一年的迭代,现已推出 小程序 和 电脑版刷题网站 (https://fe.ecool.fun/),欢迎大家使用~

同时,我们还推出了面试辅导的增值服务,可以为大家提供 “简历指导” 和 “模拟面试” 服务,感兴趣的同学可以联系小助手(微信号:interview-fe)进行报名。

最后,给大家留一个思考题:不使用 callapply,怎么模拟实现 bind