手撕call、apply、bind,相关实现和原理

call,apply,bind是做什么的

call、apply、bind是函数的一种特殊形式,它们的作用是调用函数,并且把函数的参数传递给它。call、apply、bind的实现原理是一样的,都是通过构造函数来实现的。这三种形式的第一个参数,都是函数的调用者,即谁调用了这个函数,从而决定this指向谁。

下面通过一个例子来说明call、apply、bind的用法。

function foo(a, b, c) {
  console.log(this, a, b, c);
}
foo(1, 2, 3); // window 1 2 3
foo.call({ name: 'bar' });, 1, 2, 3); // { name: 'bar' }, 1, 2, 3;
foo.call(null, 1, 2, 3); // null, 1, 2, 3
foo.apply({ name: 'bar' }, [1, 2, 3]); // { name: 'bar' }, 1, 2, 3;
foo.apply(null, [1, 2, 3]); // null, 1, 2, 3
foo.bind({ name: 'bar' })(1, 2, 3); // { name: 'bar' }, 1, 2, 3;
foo.bind(null)(1, 2, 3); // null, 1, 2, 3

call、apply、bind的实现原理

通过上面的例子我们大概可以了解call、apply、bind的实现方法,即this指向第一个函数,剩余的参数绑定到函数的arguments上。知道了这些我们就可以很方便地手写call、apply、bind的实现了。
首先我们先来实现call的方法

Function.prototype.mycall = function(thisArg, ...args) {
  // 在这里可以去执行调用的那个函数(foo)
  // 问题: 得可以获取到是哪一个函数执行了mycall
  // 1.获取需要被执行的函数
  var fn = this

  // 2.对thisArg转成对象类型(防止它传入的是非对象类型)
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg): window

  // 3.调用需要被执行的函数
  thisArg.fn = fn
  var result = thisArg.fn(...args)
  delete thisArg.fn

  // 4.将最终的结果返回出去
  return result
}

简单的验证一下

function testmycall(a, b) {
    console.log(this, a + b);
}

testmycall(1, 2); // window 3
testmycall.mycall('mycall', 1 , 2); // mycall 3
testmycall.call('call', 1 , 2); // call 3

弄懂了call的实现方法,我们就可以很方便的手写apply、bind的实现了。其实主要的区别就是call的其他参数就是函数的参数,而apply的第二个参数是函数的参数转成数组,bind的回调函数就是函数的参数。

Function.prototype.myapply = function(thisArg, args) {
    var fn = this;

    thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) :window;

    thisArg.fn = fn;
    
    args = args || [];
    var res = thisArg.fn(...args);
    delete thisArg.fn;

    return res;
}

testmycall.myapply('myapply', [111, 222]); // myapply 333
testmycall.apply('apply', [111, 222]); // apply 333

Function.prototype.mybind = function(thisArg, ...args) {
    var fn = this;

    thisArg = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) :window;

    return function proxyFn(...newArgs) {
        thisArg.fn = fn;
        var res = thisArg.fn(...args, ...newArgs);
        delete thisArg.fn;
    }
}

var testbind = testmycall.bind('bind', 123, 456);
testbind(111, 222); // bind 333

var testmybind = testmycall.mybind('mybind', 111);
testmybind(222); // mybind 333