likes
comments
collection
share

都2023年了,我不允许你还不会手写call、apply、bind!

作者站长头像
站长
· 阅读数 35

手写call、apply、bind不是为了应付面试,是为了锻炼自己的编程思维,理解这些API的核心。在工作中能更好的去理解使用。从底层思维出发去解决一系列问题,而不是遇到问题靠百度,靠猜,靠蒙。从而也是对自己职业生涯的一种交代。如果一味的觉得没什么意义,那这篇文章就不用往下看了!

首先咱先分析一波这些API的特点:

  1. call()、apply()和bind()方法都属于函数原型方法

  2. 它们3个的第一个参数都是传 this 的指向对象

  3. 执行函数自身

  4. 都是用来改变this指向

接下来咱就按照它们的特点,一步一个脚印的实现它们的功能吧!

第一个特点:挂载原型

为了不覆盖JS原生的API,咱把它们的命名为首字母大写...

   //挂载原型方法

    Function.prototype.Call = function () {
      console.log('Call')
    }
    Function.prototype.Apply = function () {
      console.log('Apply')
    }
    Function.prototype.Bind = function () {
      console.log('Bind')
    }

    //定义一个空函数

    function fn(){}

    //依次调用一下原型方法

    fn.Call()
    fn.Apply()
    fn.Bind()
    

都2023年了,我不允许你还不会手写call、apply、bind!

浏览器控制台都能正常打印。

第二个特点:首参传 this 的指向对象

    //挂载原型方法

    Function.prototype.Call = function (self) {

    }
    Function.prototype.Apply = function (self) {

    }
    Function.prototype.Bind = function (self) {

    }


    //定义一个obj对象
    let obj = {
      a: '1',
      b: '2'
    }
    //定义一个空函数
    function fn() {
      console.log('打印一下吧')
    }

    //依次调用一下原型方法

    fn.Call(obj)
    fn.Apply(obj)
    fn.Bind(obj)

现在首参也进行了传值,但是函数的console没有进行打印。因为到目前为止,这个函数自身还没有执行!

第三个特点:执行函数自身

这里需要注意一下:bind与call、apply的区别就是,调用bind是返回一个新的函数,不会马上执行需重新调用,call、apply则是立即执行的。这里面试也是经常考到

   //挂载原型方法

    Function.prototype.Call = function (self) {
      let fn = this //自身调用Call的函数
      fn()
    }
    Function.prototype.Apply = function (self) {
      let fn = this //自身调用Apply的函数
      fn()
    }
    Function.prototype.Bind = function (self) {
      let fn = this //自身调用Bind的函数
      return () => {
        fn()
      }
    }


    //定义一个obj对象
    let obj = {
      a: '1',
      b: '2'
    }
    //定义一个空函数
    function fn() {
      console.log('打印一下')
    }

    //依次调用一下原型方法

    fn.Call(obj)
    fn.Apply(obj)
    fn.Bind(obj)()

都2023年了,我不允许你还不会手写call、apply、bind!

这里就可以看到3个函数都已经打印执行了。但是到了这一步还没修改this指向哦,接着往下面看吧!

第四个特点:改变this指向

//挂载原型方法

    Function.prototype.Call = function (self) {
      let fn = this //自身调用Call的函数
      self = self || window //拿到传进来的参数,简单的判断一下如果没有值就是window
      self._ob_ = fn  //在传入进来的对象上面自定义一个属性,把函数自身赋值给这个属性
      self._ob_()
    }
    Function.prototype.Apply = function (self) {
      let fn = this //自身调用Apply的函数
      self = self || window //拿到传进来的参数,简单的判断一下如果没有值就是window
      self._ob_ = fn  //在传入进来的对象上面自定义一个属性,把函数自身赋值给这个属性
      self._ob_()
    }
    Function.prototype.Bind = function (self) {
      let fn = this //自身调用Bind的函数
      return () => {
        self = self || window //拿到传进来的参数,简单的判断一下如果没有值就是window
        self._ob_ = fn  //在传入进来的对象上面自定义一个属性,把函数自身赋值给这个属性
        self._ob_()
      }
    }

    //定义一个空函数
    function fn() {
      console.log(this)
    }
    //依次调用一下原型方法
    fn.Call({ a: 1 })
    fn.Apply({ a: 2 })
    fn.Bind({ a: 3 })()

都2023年了,我不允许你还不会手写call、apply、bind!

此时在浏览器控制台就可以看到效果了。是不是函数里面打印的this, 都变成了函数首参

以上手写call、apply、bind就按照之前分析的特点一步一步实现了,怎么样?伙伴们是不是挺简单的!

但是接下来我们还需要做一下细节上面的处理,比如有细心的小伙伴就发现,为什么打印的结果里面多了一个__ob__?

那是因为我们必须借助这个__ob__存储当前函数自身,然后用传入进来的对象再次调用!咱学过this的都知道,函数的this指向是根据谁在函数前面调用,就指向谁的。这里如果有不清楚的小伙伴可跳转我之前讲this指向的那章

同时这个__ob__被调用完之后,是不是就应该消失呀? 这个也非常简单,我们只需要delete一下就好了。为了大家能看清楚,下面拿Call来演示一下就行了! 因为其余的也是这个逻辑

//挂载原型方法

    Function.prototype.Call = function (self) {
      let fn = this //自身调用Call的函数
      self = self || window //拿到传进来的参数,简单的判断一下如果没有值就是window
      self._ob_ = fn  //在传入进来的对象上面自定义一个属性,把函数自身赋值给这个属性
      self._ob_() //调用,
      delete self._ob_  //调用完,删除即可
    }
   
    //定义一个空函数
    function fn() {
      console.log(this)
    }
    //依次调用一下原型方法
    fn.Call({ a: 1 })

那还有的小伙伴就问了。那我们函数的其他传参怎么处理呀? 这里也只需要加上就OK了呀

//挂载原型方法

    Function.prototype.Call = function (self,...org) {  //接收剩余参数
      let fn = this //自身调用Call的函数
      self = self || window //拿到传进来的参数,简单的判断一下如果没有值就是window
      self._ob_ = fn  //在传入进来的对象上面自定义一个属性,把函数自身赋值给这个属性
      self._ob_(...org) //调用的同时,传入剩余参数,
      delete self._ob_  //调用完,删除即可
    }
   
    //定义一个空函数
    function fn(a,b) {
      console.log(this,a,b)
    }
    //依次调用一下原型方法
    fn.Call({ a: 1 },'1','2')

都2023年了,我不允许你还不会手写call、apply、bind!

这样是不是就打印出来了呀。

但是这里还剩一个小问题了,就是函数的返回值还没有。这里也需要简单处理一下

//挂载原型方法

    Function.prototype.Call = function (self,...org) {  //接收剩余参数
      let result = null  //定义一个接收返回值的变量
      let fn = this //自身调用Call的函数
      self = self || window //拿到传进来的参数,简单的判断一下如果没有值就是window
      self._ob_ = fn  //在传入进来的对象上面自定义一个属性,把函数自身赋值给这个属性
      result = self._ob_(...org) //调用的同时,传入剩余参数,
      delete self._ob_  //调用完,删除即可
      return result //返回函数执行完的结果
    }
   
    //定义一个空函数
    function fn(a,b) {
      console.log(this)
      return a + b
    }
    //依次调用一下原型方法
    let res = fn.Call({ a: 1 },1,2)
    
    console.log(res) //3

写到这里,call整个底层逻辑就全部实现完成了。还剩下bind,apply小伙们可以自行发挥完善一下!可评论区留言讨论哦

转载自:https://juejin.cn/post/7242195696412639291
评论
请登录