call、apply、bind方法的使用及js原生实现(含this、arguments、rest、扩展运算符与结构赋值简单使用)

2,135 阅读9分钟

讲解call、apply及bind对this修改指针指向方法的使用、区别及实现。

前言

本文主要内容是对call、apply及bind的功能及使用方法进行介绍,之后会通过js原生实现这三种方法,让我们更深入地了解其中的作用与原理。但由于多数介绍的实现方法的过程中对this、arguments、rest、解构赋值及扩展运算符有一定涉及,本文会在先导中先做一个简单介绍,可能会对之后正文对bind等方法内容的讲解有一定理解上的帮助。

先导

  • this

    很多对call、apply及bind的讲解中都会对this有较为详细的介绍,本文在前人的基础上简略做个介绍。需要重点突出重复 的就关于this的一个问题:this永远指向最后被调用的地方

    以下是一个简单的例子:

    var name = 'windowPart'
    const testObj = {
      name: 'objPart',
      func: function(){console.log(this.name)}
    }
    // testObj调用func
    testObj.func() // objPart   this -> obj
    
    // outer最终调用func
    const outer = testObj.func
    outer() // windowPart   this -> window
    

    简单就能看出在对象被调用后this会改变其指向,指到最后被调用的地方。因此在开发过程中对this指向多加留意是很必要的,一不留心可能就会出现bug,此时有对call、apply及bind方法能熟练掌握的话就能派上很大的用场。

  • arguments、rest、...扩展运算符及解构赋值

    1. ...扩展运算符与解构赋值

      • 对于...扩展运算符,该运算符主要用于函数调用,通常可用于对数组的解构,ES8将其引入对象,对于整形、布尔值、字符串等等,扩展运算符也可将其扩展赋值,其原理是将其他类型的变量转化为对象后,将其展开,类似于Object.assign()方法。

        以下是简单用法:

        let arr = [1, 2, 3]
        console.log(...arr) // 1 2 3 
        
      • 而对于解构赋值,其可以在数组、对象、字符串甚至数值和布尔值中运用。

        用法如下:

        let arr = [1, 2, 3]
        let [a, b, c] = arr
        console.log('a:', a) // a: 1
        console.log('b:', b) // b: 2
        console.log('c:', c) // c: 3</code></pre>
        

        基于这样“匹配赋值”的模式,我们可以完成更多复杂的解构赋值,例如嵌套解构等等。

      • 但对于解构在对象上的运用,需要注意的是,其是取出参数对象的所有可遍历属性,并且在完成类似深拷贝时,申明的变量名必须为扩展对象中存在的key值,此处相当于调用了一次get(keyname)方法,其简单运用如下:

        let obj = {
          me: '我',
          you: '你'
        }
        let {me, err} = {...obj}
        console.log(me) // 我
        console.log(err) // undefined
        
    2. arguments与rest

      • arguments是一种类数组对象,其只能够在函数内部调用,主要包含着该函数的参数,其中也有所指代的Argument对象的一些其他属性,例如简单的length属性,此处不做详解。

        所谓类数组对象,其在基本使用上与数组并无异同,但对于自身属性,arguments不能使用数组中push、pop等方法,基本使用如下:

        function argsFunc () {
            console.log('arguments:', arguments)
        }
        argsFunc(1, 2, 3) // arguments: [Arguments] { '0': 1, '1': 2, '2': 3 }
        

        值得注意的是类数组对象通过扩展运算符可以很方便转化为数组,通过以下例子可以理解:

        function argsFunc (...arguments) {
            console.log('展开后的arguments:', arguments)
        }
        argsFunc(1, 2, 3) // 展开后的arguments: [ 1, 2, 3 ]
        
      • ES6引入的rest参数,相当于数组扩展运算符的逆运算,在函数参数中,运用rest可以将arguments对象进行解构取值,在定义函数时若其中存在可以归为一类的参数,此时我们加以运用rest会显得很亮眼,对函数的书写有很大的精简作用,相较于类数组对象arguments,rest作为数组去包含参数会有更加优秀的使用效率。

        以下是一个简单的使用:

        function restFunc (str, ...rest) {
          console.log('str:', str)
          console.log('rest:', rest)
        }
        restFunc('Me', 1, 2) // str: 'Me'
                             // rest: [ 1, 2 ]
        

正文

  • call、apply及bind的使用

    当被定义的函数在外部调用时,通过call、apply或bind方法将指向window的this指定回该函数中this应该指向的对象,这是保证this指向的情况之一。

    简单使用如下:

    var name = 'windowPart'
    const testObj = {
      name: 'objPart',
      func: function (...rest) {
        console.log('this.name:', this.name)
        console.log('args:', rest)
      }
    }
    let arr = [1, 2]
    // testObj调用func
    testObj.func(...arr) // objPart   this -> obj
    
    // 修改this指向后outer调用func
    const outer = testObj.func
    outer.call(testObj, ...arr)
    outer.apply(testObj, arr)
    outer.bind(testObj, ...arr)()
    
    //以上输出均为:
    //    this.name: objPart
    //    args: [ 1, 2 ]
    

    在coding时,对于用到this的地方,一定要多加注意this指向丢失问题,不论后期在何处调用一定先将this绑定好,上例为更好理解是在调用处指回函数内部this本应指回的对象。在开发过程中,我们不仅可以在赋值给全局变量后调用时通过call、apply及bind方法将丢失指向了window的this绑定回,也可如以下方法不通过赋值给全局变量后调用并在用到this时就提前绑定好避免this丢失的情况发生:

    // 方法一:通过bind(this)绑定好上一级this
    var name = 'windowPart'
    const testObj = {
      name: 'objPart',
      func: function () {
        setTimeout(function(){
          console.log('绑定后 this.name:', this.name) // this -> testObj
        }.bind(this), 1000)
      }
    }
    testObj.func() // 绑定后 this.name: objPart
    
    //方法二:通过_this保留上级this
    var name = 'windowPart'
    const testObj = {
      name: 'objPart',
      func: function () {
        let _this = this
        setTimeout(function(){
          console.log('绑定后_this.name:', _this.name) // _this -> testObj
          console.log('未绑定 this.name:', this.name)  //  this -> window
        }, 1000)
      }
    }
    testObj.func() // 绑定后_this.name: objPart
                   // 未绑定 this.name: windowPart
                   
    //方法三:通过箭头函数this指向上一级对象
    var name = 'windowPart'
    const testObj = {
      name: 'objPart',
      func: function () {
        setTimeout(() => {
          console.log('箭头函数内 this.name:', this.name) // this -> testObj
        }, 1000)
      }
    }
    testObj.func() // 箭头函数内 this.name: objPart
    
  • call、apply及bind的异同

    通过上一示例,可以看出对于call、apply及bind的区别有以下:

    • call与apply之间: 第一个参数均为this应指向的对象,而对于其余参数,call需要展开传递,apply则需要将其以数组形式传递;
    • call与bind之间: 第一个参数均为this应指向的对象这一相同之处不变,call及bind其与参数传递形式也相同(展开传递),但这两种方法在返回值上有一个细节的不同,call方法直接将需要修改this指向指定回的函数直接执行,返回该函数内部的返回值;而bind方法,则返回一个function对象 即 需要修改内部this指向指定回的函数,最后再被调用才会最后执行该函数。

    以上如果不好理解,通过下一节自己js手写这三个方法会有很清晰的认识。ヾ(◍°∇°◍)ノ゙加油

  • js手写call、apply及bind

    • call方法实现

      const arr = [1, 2]
      function testFunc (num1, num2) {
        console.log('this.name:', this.name)
        console.log('num1:', num1)
        console.log('num2:', num2)
      }
      const testObj = {
        name: 'objName'
      }
      
      Function.prototype.myCall = function (testObj, ...rest) {
        if(arguments.length == 2 && Array.isArray(arguments[1])) {
          try{
            throw new Error('myCall参数错误!')
          } catch (e) {
            console.log(e.name + ': ' + e.message)
          }
          return
        }
        console.log(this) // 此处输出便于理解输出一下this内容 
                          // this -> 最后调用myCall方法的[Function: testFunc]
        testObj._fn = this
        var ret = testObj._fn(...rest)
        delete testObj._fn
        return ret
      }
      testFunc.myCall(testObj, ...arr) // this.name: objName
                                       // num1: 1
                                       // num2: 2
      

      if()部分就是稍微写的细节一点的一个对参数的判断问题。其中最需要解释的一点,在testObj中申明一个_fn,将testFunc赋给_fn,然后通过ret调用testObj._fn(...rest)可以简便的将testFunc作为testObj一个内部属性,从而达到修改testFunc内部this指向testObj的效果。需要注意的是testObj内部利用完的_fn要在最后进行回收处理。

      简化原理如下:

      const func = function () {
        console.log(this.name)
      }
      const testObj = {
        name: 'testObj',
        _fn: func
      }
      testObj._fn()
      
    • apply方法实现

      对于apply的js原生实现,仅仅与call在参数传递上有细微的不同,读懂call的原生实现,可以尝试自己完成apply的过程。

      const arr = [1, 2]
      function testFunc (argsArr) {
        console.log('this.name:', this.name)
        console.log('argsArr:', argsArr)
      }
      const testObj = {
        name: 'objName'
      }
      Function.prototype.myApply = function (testObj, rest) {
        if(!Array.isArray(rest)) {
          try{
            throw new Error('myApply参数错误!')
          } catch (e) {
            console.log(e.name + ': ' + e.message)
          }
          return
        }
        testObj._fn = this
        var ret = testObj._fn(rest)
        delete testObj._fn
        return ret
      }
      testFunc.myApply(testObj, arr) // this.name: objName
                                     // argsArr: [ 1, 2 ]
      
    • bind方法实现

      对于bind方法,在使用时就曾提及过一个不同:其返回的是一个函数,所以与call及apply相比在使用上会多出一个bind方法返回的函数可以作为构造函数的情况,以下对bind方法的实现我将由浅入深,逐渐完善,更方便理解。

      1. Version 1: 仅考虑 将bind返回函数作为普通函数使用 的情况。
      const arr = [1, 2]
      const testObj = {
        name: 'objName'
      }
      function testFunc(...argsArr) {
        console.log('this.name:', this.name)
        console.log('argsArr:', argsArr);
      }
      Function.prototype.myBind = function (testObj, ...rest) {
        if(arguments.length == 2 && Array.isArray(arguments[1])) {
          try{
            throw new Error('myCall参数错误!')
          } catch (e) {
            console.log(e.name + ': ' + e.message)
          }
          return function () {}
        }
        
        const _this = this
        let resFn = function () {
          _this.apply(testObj, rest)
        }
        return resFn
      }
      testFunc.myBind(testObj, ...arr)() // this.name: objName
                                         // argsArr: [1, 2]
      
      2. Version 2: 添加考虑 将bind返回函数作为构造函数使用 的情况。

      当bind返回函数作为构造函数使用时 即 new Func() ,此时我们需要注意两个地方:

      (1). 因为new不仅自身优先级大且new对this指向改变优先级大于bind方法的问题,会将内部this的指向实例,此处我们需要在做一个判断,对内部调用apply方法需绑定的地方做一个选择。其实此时bind指定的this值会失效,但传入值依然有效。

      (2). 对于prototype,在这个情况下,函数被作为构造函数返回就需要将实例需继承该原型中的值。

      const arr = [1, 2]
      const testObj = {
        name: 'objName'
      }
      function testFunc(...argsArr) {
        console.log('this.name:', this.name)
        console.log('argsArr:', argsArr)
      }
      Function.prototype.myBind = function (testObj, ...rest) {
        if(arguments.length == 2 && Array.isArray(arguments[1])) {
          try{
            throw new Error('myCall参数错误!')
          } catch (e) {
            console.log(e.name + ': ' + e.message)
          }
          return function () {}
        }
        const _this = this
        let resFn = function () {
          _this.apply(this instanceof resFn ? this : testObj, rest)
        }
        resFn.prototype = this.prototype
        return resFn
      }
      testFunc.myBind(testObj, ...arr)() // this.name: objName
                                         // argsArr: [1, 2]
                                         
      new (testFunc.myBind(testObj, ...arr)) // this.name: undefined
                                             // argsArr: [1, 2]
      
      3. Version 3: 优化代码。

      对于实例需继承该原型中的值,原型链上的操作,若如上resFn.prototype = this.prototype定义,会产生引用赋值共用一个内存地址的情况,发生以下问题:

      Function.prototype.testBind = function () {
        let retFunc = function () { }
        retFunc.prototype = this.prototype
        return retFunc
      }
      
      function Test1 () {}
      let Test2 = Test1.testBind()
      Test2.prototype.a = function () {}
      const test = new Test2
      console.log(Test2.prototype) // {a: ƒ, constructor: ƒ}
      console.log(Test1.prototype) // {a: ƒ, constructor: ƒ}
      

      因此这个时候我们需要一个空函数中转一下或者使用Object.create(),防止对父级原型链的污染。

      const arr = [1, 2]
      const testObj = {
        name: 'objName'
      }
      function testFunc(...argsArr) {
        console.log('this.name:', this.name)
        console.log('argsArr:', argsArr)
      }
      Function.prototype.myBind = function (testObj, ...rest) {
        if(arguments.length == 2 && Array.isArray(arguments[1])) {
          try{
            throw new Error('myCall参数错误!')
          } catch (e) {
            console.log(e.name + ': ' + e.message)
          }
          return function () {}
        }
        const _this = this
        let resFn = function () {
          _this.apply(this instanceof resFn ? this : testObj, rest)
        }
        resFn.prototype = Object.create(this.prototype)
        // const TempFunc = function () {}
        // TempFunc.prototype = this.prototype
        // resFn.prototype = new TempFunc()
        return resFn
      }
      
      const BindFunc = testFunc.myBind(testObj, ...arr)
      BindFunc.prototype.a = function () {}
      var test = new BindFunc
      console.log(BindFunc.prototype)
      console.log(testFunc.prototype)
      

相关文章

谢谢以上作者大大~

第一篇文章~完结撒花~*★,°*:.☆( ̄▽ ̄)/$:*.°★*