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)
      複製代碼

相關文章

謝謝以上做者大大~

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

相關文章
相關標籤/搜索