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

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

前言

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

先导

  • this

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

    如下是一个简单的例子:app

    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()方法。post

        如下是简单用法:优化

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

        用法以下:this

        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>
        复制代码

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

      • 但对于解构在对象上的运用,须要注意的是,其是取出参数对象的全部可遍历属性,而且在完成相似深拷贝时,申明的变量名必须为扩展对象中存在的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)
      复制代码

相关文章

谢谢以上做者大大~

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

相关文章
相关标签/搜索