call、apply和bind模拟实现

266 阅读3分钟

在实际工作中,这三个函数在我们代码中是很活跃的,比如经常使用Object.prototype.toString.call()来精确判断数据类型;今天我们就尝试去探究如果让我们自己动手去实现。

call的模拟实现

在模拟实现之前我们首先需要知道call做了什么?

  1. 改变了函数的this指向
  2. 第一个参数是指定的this,后面的参数是函数运行时的参数
  3. 执行函数

那么接下来我们一步步的进行实现

第一步改变函数的this

看下面这个例子:

function boo() {
    console.log(this.name)
}
var foo = {
    name: 'foo',
    boo: boo
}
foo.boo() // 输出结果为foo

从这个例子中我们得出改变this的思路:将函数设置成第一个参数的属性;然后在call内部执行。话不多说按照这个思路进行实现:

function call1(context) {
    context.fn = this; // 在这里this就是call前面的函数
    let result = context.fn(); // 通过context执行函数
    delete context.fn; //  执行完成之后为了不改变原有变量的属性,将添加的函数删除
    return result;
}
// 将call1挂载到Function的原型上
Function.prototype.call1 = call1

第二步可以接收参数

call函数从第二个参数开始都是函数运行时的实参,那接下来在第一步的基础上进行改进

function call2(context) {
    context = context || window; // 第一个参数有可能为null
    context.fn = this;
    var arg = []; // 用来保存参数
    var result = ''; // 用来保存函数运行的返回值
    // ES6的写法
    for (var i = 1; i < arguments; i++) {
        arg.push(arguments[i]);
    }
    result = context.fn(...arg)
    // ES6之前的写法
    for (var i = 1; i < arguments; i++) {
        arg.push('arguments[' + i + ']');
    }
    result = eval('context.fn(' + arg + ')');
    delete context.fn;
    return result;
}
Function.prototype.call2 = call2;

至此call的模拟已经完成

apply模拟实现

apply和call的作用是一样的,它们之间的区别是传参数方式不一样,apply只有两个参数,第一个参数是指定的this,第二个参数是数组,包含了函数执行所需的参数。下面我们来进行模拟:

function apply1(context, arr) {
    context = context || window;
    var result = '';
    context.fn = this;
    if (arr) {
        result = context.fn(...arr);
    } else {
        result = context.fn();
    }
    delete context.fn;
    return result;
}

bind模拟实现

按照顺序首先介绍一下bind做了哪些事:

  • 返回一个函数
  • 该函数运行时的this就是bind方法的第一个参数
  • 可以给函数传参

第一步:返回函数和this的绑定

Function.prototype.bind1 = function(context) {
    var _self = this;
    return function () {
        return _self.apply(context)
    }
}

第二步:实现参数的传递

Function.prototype.bind2 = function(context) {
    var _self = this;
    var arg = Array.prototype.slice.call(arguments, 1); // 获取bind函数的后续参数
    return function () {
       return  _self.apply(context, arg);
    }
}

上面代码我们已经实现了bind绑定时的参数传递,但是bind方法返回的函数也可以传参数,下面我们就对这个进行一下优化:

Function.prototype.bind2 = function(context) {
    var _self = this;
    var arg = Array.prototype.slice.call(arguments, 1);// 获取bind函数的后续参数
    return function () {
        arg = arg.concat([...arguments]);
        return _self.apply(context, arg);
    }
}

到此基本功能就完成,但是你以为就此结束了吗?那只能说对不起了,bind函数返回的方法还可以作为构造函数,当作为构造函数通过new生成实例时,这时的this就不是bind函数的第一个参数了,那么下面我们开始进行优化:

Function.prototype.bind3 = function (context) {
    var _self = this;
    var arg = Array.prototype.slice.call(arguments, 1);
    function fn () {
        arg = arg.concat([...arguments]);
        return _self.apply(this instanceof fn ? this : context, arg);
    }
    fn.prototype = _self.prototype;
    return fn;
}

上面代码实现作为构造函数时的情况,但是这种实现有个缺陷,当我们改变bind方法返回函数的原型时会同时把原来函数的原型给改了,下面我们进行优化一下:

Function.prototype.bind3 = function (context) {
    var _self = this;
    var arg = Array.prototype.slice.call(arguments, 1);
    function fn() {
        arg = arg.concat([...arguments]);
        return _self.apply(this instanceof fn ? this : context, arg);
    }
    function Trans(){};
    Trans.prototype = _self.prototype;
    fn.prototype = new Trans();
    fn.prototype.constructor = fn;
    return fn;
}

现在bind的模拟实现已经完成了。

参考:冴羽的博客