[手写call/apply/bind] 麻烦你一定要学会!

72 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

前言

网上有各种优秀的手写call/apply/bind的文章,但是为什么我还要自己写一篇呢?

原因是我自己在学习这三个方法的时候,确实也看了非常多的文章,可能是自己基础也不太好,都是一知半解,当自己理解了之后,也想用自己的理解去教会大家(已经会的同学可以忽略)

前置知识:

  • this指向
  • 原型和原型链
  • 数组方法 slice(), concat()

原型链相关知识可以看我之前写的

如果理解这些前置知识,会有助于你的理解和学习

面向需求编程

在我们开始手写之前,我们先梳理一下关于这三个函数的相关知识,以及同异之处。

call

语法function.call(thisArg, arg1, arg2, ...)

参数

thisArg: function函数运行时使用的this值,也就是把function函数的this指向改变为thisArg

arg1,arg2,...: 参数列表

返回值: 在调用的时候会立即执行,并且使用提供的this,和参数去执行,如果没有返回值就返回undefined

apply

语法function.apply(thisArg, [ arg1, arg2, ...])

参数

thisArg: function函数运行时使用的this值,也就是把function函数的this指向改变为thisArg

[arg1,arg2,...]: 参数列表,需要是一个数组

返回值: 在调用的时候会立即执行,并且使用提供的this,和参数去执行,如果没有返回值就返回undefined

虽然call()的语法与 apply() 几乎相同,但根本区别在于,call() 接受一个参数列表,而 apply() 接受一个参数的单数组

bind

语法function.bind(thisArg, arg1, arg2, ...)

参数

thisArg: function函数运行时使用的this值,也就是把function函数的this指向改变为thisArg

arg1,arg2,...: 参数列表

返回值: 在调用的时候不会立即执行,而是返回一个函数,这个函数的this是thisArg,参数是arg1, arg2, ...

在了解了这三个函数的基本语法和用法之后,我们可以开始着手去实现

call()

先写出大概框架

在原型链上挂上我们自己实现的call()函数 myCall()

Function.prototype.myCall(){


}

获取需要改变执行上下文

我们可以通过获取传入的第一个参数来获取,也就是context,这里简写为ctx

但是,会有几种情况:

  1. 是否传入了需要改变的执行上下文ctx

如果没有传入需要改变的执行上下文,那我们就将它指向window

  1. 传入的不是一个Object

如果传入的不是一个Object形式,我们就用Object()方法转成Object

开始使用代码实现:

Function.prototype.myCall( ctx ){
  ctx = ctx ? Object(ctx) : window;

}

获取参数

获取除了第一个参数之后的参数,我们使用slice()和方法来实现

了解一下slice():slice()方法主要是用来对数组进行截取,或者删除特定项,不会改变原数组,而是返回一个截取之后的新数组

可以参考MDN的介绍:slice()

Function.prototype.myCall( ctx ){
  ctx = ctx ? Object(ctx) : window;
  let args = [...arguments].slice(1)
}

将调用的函数设置为对象的方法

谁调用了myCall,这里的this就指向谁

Function.prototype.myCall( ctx ){
  ctx = ctx ? Object(ctx) : window;
  let args = [...arguments].slice(1);
  ctx.myFn = this;
}

传入参数,调用函数

因为call和apply都是调用就立马执行的,所以,在内部我们需要执行,并且把返回值使用一个变量接住,这里我们用一个res接住

     Function.prototype.myCall( ctx ){
      ctx = ctx ? Object(ctx) : window;
      let args = [...arguments].slice(1);
      ctx.myFn = this;
      let res = ctx.myFn(...args);
    }

收尾阶段

在我们需求都满足的情况下,就顺利的进入到了收尾阶段了

因为我们在开始的时候,在ctx身上新增了一个对象myFn,在调用完之后,需要删除掉

并且要把执行的返回结果 return 出去

   Function.prototype.myCall( ctx ){
    ctx = ctx ? Object(ctx) : window;
    let args = [...arguments].slice(1);
    ctx.myFn = this;
    let res = ctx.myFn(...args);
    delete ctx.myFn;
    return res;
  }

到这里,call()已经实现了,我们来试试看

call().png

一定一定要理解了call()的实现之后再进行接下来apply()和bind()的学习!

apply()

上面有介绍到,call()和apply()几乎是一模一样,但是参数的形式不一样,apply()中的参数需要是数组的形式。

因此大家务必理解了上面call()的实现之后再接下去学习

废话不多说

因为call()和apply()的相似,所以我们在call()的基础上进行修改即可

首先把一样的步骤写下来,在参数截取接收的方法上做出改变

相同的部分如下,有关参数的部分先不写

    Function.prototype.myApply (ctx) {
        ctx = ctx ? Object(ctx) : window;
        ctx.myFn = this
        let res = null;
        delete ctx.myFn;
        return res
    }

该如何接收参数?

首先我们来思考该怎么接收参数,并且传入我们要执行的myFn中

参数传入的格式是 [ arg1, arg2, ...],是个数组

传入的arguments是这样的 [{a:1,b:2},["张三", "李四"]]

{a:1,b:2}是我们要改变的this指向,["张三", "李四"]是我们传入的参数,因此我们可以使用arguments[1]取出参数["张三", "李四"]

先不急着敲代码

思考一下,如果没有传入参数,那么arguments[1]就是undefined,所以我们需要先进行一个判断

看代码

    Function.prototype.myApply (ctx) {
        ctx = ctx ? Object(ctx) : window;
        ctx.myFn = this
        let res = null;
        
        if (arguments[1]) {
            res = ctx.myFn(...arguments[1])
        }else {
            res = ctx.myFn()
        }
        
        delete ctx.myFn;
        return res
    }
    

还是一样,我们执行一下

apply().png

如果能理解call(),那么apply()理解起来也不会难

bind()

bind()相对于上面两个函数,bind的不同在于,它不是立即执行的,而是返回一个函数

因为bind()的这种特性,bind()能够作为构造函数去new一个新的对象,这也是我们需要考虑的

还是一样,我们一步一步来

大概框架

写出大概框架,判断是否传入ctx,就是要改变的this的指向,没有的话直接指向window(反复强调,反复理解)

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window;
        
    }

接收参数

和call()一样,没什么好说的

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window;
        let args = [...arguments].slice(1)
        
    }

设置一个返回函数

因为bind()不是立即执行的,所以需要返回一个函数

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window;
        let args = [...arguments].slice(1)
        let Fn = function () {
            
        }
        
        return Fn
    }

返回的函数内容

  1. 返回出去的函数,this的指向是被改变了的,指向的是myBind中的ctx,我们这里使用apply()来改变指向

  2. 并且在返回出去的函数中需要执行函数,这个执行的函数是第一次调用myBind()的函数,为了方便调用,我们也使用一个对象进行接收

  3. 要执行的函数中参数有两部分,一部分是在一开始myBind()函数的时候传进去的,另一部分是在,函数执行的时候传进去的,所以我们需要合并在一起

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window; 
        let args = [...arguments].slice(1);
        let fn = this;
        let myFn = function () {
            return fn.apply(ctx, args.concat(...arguments))
        }
        return Fn
    }

值得注意的是,当它作为构造函数去使用的时候,this的指向也需要改变

那么怎么判断是不是它的构造函数构造出来的呢?

着重理解

我们可以使用instanceof去原型链上查找,看是不是myFn,如果是,那就是构造函数构造出来的,就让this指向它的构造函数myFn,在函数中就是this。不是的话,那就不是构造函数,就指向ctx就可以了。

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window; 
        let args = [...arguments].slice(1);
        let fn = this;
        let myFn = function () {
            return fn.apply(this instanceof myFn ? this : ctx, args.concat(...arguments))
        }
        return myFn
    }

连接原型链

构造函数构造出来的实例对象,之间是有原型链关系的

但是在我们上面的操作完成后,myFn和我们调用的函数之间没有联系,因此我需要把他们两个连接起来

可以这样

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window; 
        let args = [...arguments].slice(1);
        let fn = this;
        let myFn = function () {
            return fn.apply(this instanceof myFn ? this : ctx, args.concat(...arguments))
        }
        
        myFn.prototype = this.prototype; // *****看这******
        
        return myFn
    }

但是,非常不建议这样修改,如果两个函数对象共用了一个原型的话,其中一个函数进行原型的修改,将会影响到大家,所以非常不建议这样做

所以,我们可以通过一个中间函数进行连接

    Function.prototype.myBind(ctx){
        ctx = ctx ? Object(ctx) : window; 
        let args = [...arguments].slice(1);
        let fn = this;
        let _tempFn = function(){}; // ******看这******
        let myFn = function () {
            return fn.apply(this instanceof myFn ? this : ctx, args.concat(...arguments))
        }
        
         _tempFn.prototype = this.prototype; // *****看这******
        myFn.prototype = new _tempFn(); // *****看这******
        
        return myFn
    }

到这里,就完成了bind函数的实现

运行一下看看

bind().png

最后

希望你看完之后也能够学会手写这三个函数

最后的最后,希望大家不要害怕手撕代码,可能第一时间看到手写代码这几个字会害怕,想逃避。

但是希望大家记住一句话,共勉之

消除恐惧最好的办法,就是面对恐惧。