手写call、apply、bind思路及实现

294 阅读4分钟

引言:一般在面试或工作中对call、apply、bind会用就可以了,但有的时候面试还是会问到“能说说它们的实现吗”,所以对原理还是要有一定了解。

回顾:call、apply、bind 的用法介绍

Call的实现

一、回顾用法

call(修改后的this指向, 参数1, 参数2, 参数3...)

二、思路

先想一下call做了哪些事情

  1. 修改this指向
  2. 执行这个函数
  3. 返回函数执行的结果
  4. 可以传入多个参数
  5. 异常处理

三、实现

第一步:修改指向,执行函数,返回结果

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

先看效果

var obj = {
    a: 1
}
function test() {
    console.log(this.a);
}
test(); // 输出 undefined
test.myCall(obj); // 输出 1

实现解释

  1. context.fn = thiscontext作为第一个参数,表示修改后的指向,所以context对应的就是obj,这里的this指向调用者,对应的就是test。那这里的意思就是在obj上创建一个fn来接收test
  2. obj上接收test后,执行这个fn,再将执行结果保存到result上。
  3. 删除临时创建的fn
  4. 返回执行结果result

第二步:支持传入多个参数

Function.prototype.myCall = function (context, ...args) {
    context.fn = this;
    const result = context.fn(...args);
    delete context.fn;
    return result;
}

先看效果

var obj = {
    a: 1
}
function test(agr1, arg2, arg3) {
    console.log(this.a, agr1, arg2, arg3);
}
test(); // 输出:undefined undefined undefined undefined
test.myCall(obj, "第一个", "第二个", "第三个"); // 输出:1 第一个 第二个 第三个

实现解释

支持多个参数的话,那就用...args来接收参数,然后在执行函数的时候传入进去就可以了

第三步:异常处理,this参数可以为null

当this参数为null时,this指向的是window

所以最后还要加一个判断

最终版

Function.prototype.myCall = function (context, ...args) {
    if (typeof context === "undefined" || context === null) {
        context = window
    }
    let symbolFn = Symbol()
    context[symbolFn] = this
    const result = context[symbolFn](...args)
    delete context[symbolFn]
    return result
}

效果

var obj = {
    a: 1
}
function test(agr1, arg2, arg3) {
    console.log(this.a, agr1, arg2, arg3);
}
test(); // 输出:undefined undefined undefined undefined
test.myCall(obj, "第一个", "第二个", "第三个"); // 输出:1 第一个 第二个 第三个
test.myCall(null, "第一个", "第二个", "第三个"); // 输出:undefined 第一个 第二个 第三个

实现解释

  1. 在修改this指向时判断,如果contextnullundefinedthis就指向window
  2. 在创建fn属性时可能在context上本身就有一个fn,所以使用Symbol()来保证用的是唯一值

Apply的实现

一、回顾用法

apply(修改后的this指向, [参数1, 参数2, 参数3...])

二、思路

和call类似,唯一不同的就是只有两个参数,第二个参数是数组

三、实现

Function.prototype.myApply = function (context, args) {
    if (typeof context === "undefined" || context === null) {
        context = window
    }
    let symbolFn = Symbol()
    context[symbolFn] = this
    const result = context[symbolFn](...args)
    delete context[symbolFn]
    return result
}

效果

var obj = {
    a: 1
}
function test(agr1, arg2, arg3) {
    console.log(this.a, agr1, arg2, arg3);
}
test(); // 输出:undefined undefined undefined undefined
test.myApply(obj, "第一个", "第二个", "第三个"); // 输出:1 第一个 第二个 第三个
test.myApply(null, "第一个", "第二个", "第三个"); // 输出:undefined 第一个 第二个 第三个

实现解释

call类似,因为只有两个参数,所以把接收参数的方式修改了

call接收方式是function (context, ...args)apply接收方式是function (context, args)

Bind的实现

这里介绍的是es5通过apply的实现方式

一、回顾用法

bind(修改后的this指向, 参数1, 参数2, 参数3...)

二、思路

  1. call类似,接收多个参数
  2. 返回新函数
  3. 新函数执行后是修改this后的执行结果
  4. 新函数可传参
  5. 异常处理

三、实现

第一步:接收多个参数,返回新函数

Function.prototype.myBind = function (context, ...args) {
    return function() {
    }
}

实现解释

通过...args接收this参数外的其他参数,然后再返回一个funtion

第二步:接收多个参数,返回新函数

Function.prototype.myBind = function (context, ...args) {
    let self = this
    return function() {
        return self.apply(context, args)
    }
}

实现解释

  1. let self = this返回结果不是立即执行的,需要外部调用,所以这里需要把当前调用者(this指向调用者)保存起来,避免在返回的function中找不到原来指向的函数
  2. return self.apply(context, args),由于返回的function体内是会返回一个修改this指向后的执行结果,所以用apply返回一个修改指向后的结果

第三步:新函数可传参

Function.prototype.myBind = function (context, ...args) {
    let self = this
    return function(...params) {
        return self.apply(context, args.concat(params))
    }
}

实现解释

args.concat(params),因为返回的函数还可以传参,所以用...params接收执行时的参数,并与调用myBind时的参数连接起来

第四步:异常处理,this参数可以为null

当this参数为null时,this指向的是window

所以最后还要加一个判断

最终版

Function.prototype.myBind = function (context, ...args) {
    if (typeof context === "undefined" || context === null) {
        context = window
    }
    // 这里的 this 就是 test
    let self = this
    return function(...params) {
        // 这里的 self 就是 test
        return self.apply(context, args.concat(params))
    }
}

效果

var obj = {
    a: 1
}
function test(agr1, arg2, arg3) {
    console.log(this.a, agr1, arg2, arg3);
}
test.myBind(obj, "第一个", "第二个")("第三个"); // 输出:1 第一个 第二个 第三个

self.apply(context, args.concat(params))这里用call也行,把后面args.concat(params)展开即可