this,apply和call

jsvascript 中的 this/apply/call

this

  • 定義:javascript 中 this 總是指向一個對象,指向哪個對象,則根據函數執行環境動態綁定,不是函數聲明。

  • this 的指向

    1.對象方法調用

     var obj = {
            a:0,
            getA:function(){
                console.log(this === obj); // true
                console.log(this.a) // 0
            }
        }
       obj.getA()
    

    分析: obj.getA()這個 getA 方法屬於 obj 對象,而obj.getA(),這 obj 屬於宿主對象直接調用方法,該函數裏 this 指向宿主對象,無論是在嚴格模式下還是非嚴格模式下,this 依然指向宿主對象

  1. 普通函數調用
    注: 有一點得注意,就是對象方法賦值變量問題,將這種情況拆開分析

    • 函數全局調用
    window.name = 'ldy'
    function whoIsName (){
        return this.name
    }
    console.log(whoIsName()) // ldy
    

    分析: 方法whoIsName被默認掛載到全局對象 window 上,是可以通過window.whoIsName獲取該方法。而調用whoIsName()方法時,而該函數裏 this 特性總是指向一對象,所以該函數裏 this 指向 window。其實在嚴格模式下,該函數裏的 this 是 undefined,那是因爲該函數被認爲沒有宿主對象,所以爲 undefined;其實你可以將window.whoIsName = function(){return this.name},這時 this 纔會指向 window,我說的這是在嚴格模式。

    • 對象賦值調用函數

      ```
      window.name = 'ldy'
      var myObj = {
          name:'12',
          getName:function(){
              return this.name
          },
      
      }
      var getName = myObj.getName;
      console.log(getName()); // ldy
      ```
      

      分析: 將 myObj 對象的 getName 方法賦值 getName 變量,這時這個方法被間接掛在了 window 上,所以 this 就指向了 window,在嚴格模式下也是無法找對 this 的宿主對象,也是 undefined;

  2. 構造器調用 this

    var Myself = function(){
    
            this.name = 's';
            // return 'l'
            return {
                l:'l'
            }
        }
    var obj = new Myself();
    console.log(obj, obj.name)
    

    分析: 進行對象覆蓋問題,return 的對象字面量對類實例的覆蓋;而字符串是無法進行覆蓋,由於值內存和地址內存導致的主要原因。地址內存:返回的引用類型,存放於地址內存,返回的基本類型存放於值內存(棧內存)。

  3. Function.prototype.call 或 Function.prototype.apply 調用

    • 可以動態改變函數內部的 this 指向
        var myObj = {
            name:'ldy',
            getName:function(){
                return this.name
            }
        }
    
        var on = {
            name:"sly"
        }
    
        console.log(myObj.getName());// ldy
        console.log(myObj.getName.call(on))// sly
    

    分析: 將函數的方法的宿主對象改變,將掛載在 myObj 的 getName 函數掛載到了 on 對象上;導致我們看到 this 指向的改變。

call 和 apply

  • 區別:作用一致,參數形式不同;

    var func = function( a, b, c ){
      console.log ( [ a, b, c ] ); // 輸出 [ 1, 2, 3 ]
     };
     // func.apply( null, [ 1, 2, 3 ] );
     func.call(null, 1,2,3)
    

    分析: 上面的代碼只是用於傳參,用於 this 指向的改變,可以參考 this 指向的第四點,此地,不再贅述。

  • bind 函數實現:其實在大部分瀏覽器都基本內置了 bind 方法,來改變 this 的指向,但是是如何實現 bind 方法的,請參考下面的代碼:

      Function.prototype.bind = function(context){
              var that = this;
              var context = [].shift.call(arguments);// 借用[].shift方法,給掛載到了arguments類數組上並執行shift方法
              var args = [].slice.call(arguments);
              return function(){
                  // args 傳進去是可行;[].concat.call( args, [].slice.call( arguments ) ),這個傳進去也是可行的;
                  // 具體會有差別,目前還沒有認證其差別
                   that.apply((!this|| this===window)?context:that,   [].concat.call( args, [].slice.call( arguments ) ))
              }
      }
    

    分析: bind 函數實現,也是借用了 apply 方法的實現,利用 call 實現,可以藉助 es6 語法去實現,理解其原理即可,that.call(context, ...args),其他實現方法,還未有找到資料去證明其實現方案。

  • 通過 call 或者 apply 綁定實現繼承

            var A = function(){
                this.ca = function(){}
                this.l = 'l'
            }
            A.app = function(){}
            var B = function(){
                A.call(this, arguments)
            }
            var b = new B();
            console.log(b)
    
    1. 其實通過 call 或 apply 實現繼承,有個問題,無法繼承原型方法
    2. 接下來實現原型繼承:
       var A = function(){
              this.ca = function(){}
              this.l = 'l'
              this.a=['q']
          }
          A.prototype.app = function(){}
          A.a = function(){}
          var a = new A();
          var B = function(){
    
          }
          B.prototype = a;
          var b = new B();
          var b1 = new B()
          b1.a.push(23)
    
          console.log(b, b1)
    

    分析: 子類繼承父類所有原型和類方法,但是不同實例中改變父類的屬性,會影響其他子類實例的獲取父類屬性的值。

    1. 組合模式繼承
    var A = function(){
            this.ca = function(){}
            this.l = 'l'
            this.a=['q']
        }
        A.prototype.app = function(){}
        A.a = function(){}
        var a = new A();
        var B = function(){
           A.call(this, arguments)
        }
        B.prototype = a;
        var b = new B();
        var b1 = new B()
        b1.a.push(23)
    
        console.log(b, b1)
    

    分析: 組合繼承,不同實例間獲取父類的值,改變父類的值,互不干擾。

    總結: 本篇先簡單的介紹這三種,其實還有原型式,寄生式等,下篇將會介紹 js 面向對象多態與繼承等問題。

  • 閉包中 this 指向:指向全局對象,當然也是可以通過 call 或者 apply 去修改

    var name = 'oi'
          var c = {
              name:'l',
              a:function(){
                  return function(){
                      return this.name
                  }
              }
          }
          console.log(c.a()()) // oi
    

    分析: 調用 c.a()時,返回一個 function 函數,這個函數默認掛載到全局,作用域爲全局,其實可以做個實驗,將 c.a()給掛到另一個對象中,那麼 this 的指向,就指向被掛載到的那個對象。

  • 硬綁定:修改一次,無法再次修改 this 的指向,

    var f = function(){
                console.log(this.n)
            }
    
            var o = {
                n:"876"
            }
    
            var ba = function(){
                f.call(o)
            }
            ba()
            ba.call(window)
    
  • 軟綁定:爲了保持和硬綁定同樣的效果,並且保留隱式綁定或者顯示綁定修改 this 的能力

      Function.prototype.softBind = function(){
              var that = this;
              var context = [].shift.call(arguments);
              var args = [].slice.call(arguments);
              var bound =  function(){
                  return that.apply((!this|| this===window)?context:this,   [].concat.call( args, [].slice.call( arguments ) ))
              }
              // bound.prototype = Object.create(that.prototype) // 繼承function原型特性
              return bound
          }
    
    

    實例:

       var foo = function(){
              // console.log(this)
              console.log(this.sco)
      }
      var liu1 = {
          sco:'sco1'
      }
      var liu2 = {
          sco:"sco2"
      }
      var liu3 = {
          sco:'sco3'
      }
      var fool = foo.softBind(liu1);
      fool(); // sco1
      liu2.foo = foo.softBind(liu1);// 隱式綁定
      liu2.foo(); // sco2
    
      fool.call(liu3); // sco3 顯示綁定
    
  • 箭頭函數 this 指向問題:箭頭函數並不是使用 function 關鍵字定義使用操作符來定義的=>,他的指向根據最外層作用域來決定的 this。

  • 實例 1:

    
        var o2 = {
            l:'09',
        }
        var o1 = {
            l:1
        }
        var j = function(){
            return ()=>{
                console.log(this.l)
            }
        }
        var b1 = j.call(o1)
        b1.call(o2) // 1
    
    
  • 實例 2:

       var o1 = {
            l:1
        }
        var fn = function(){
            setTimeout(() => {
                console.log(this.l)
            }, 100);
        }
        fn.call(o1)
    

    小結: 上面兩則實例是對箭頭函數的 this 取決於最外層函數的作用域。

總結: 對於上面介紹的 this 的指向綁定,在這裏分爲幾類:隱式綁定;顯示綁定;new 綁定三類;在顯示綁定存在軟綁定和硬綁定;那什麼是隱式綁定呢,其實隱式綁定其實就是直接寫進對象裏的函數或者通過 function 定義的全局的函數;顯示綁定,就是通過 call 或者 apply 改變函數的作用域;new 綁定通過 new 一個實例來改變函數或者類的作用域。軟綁定呢,就是實例中對 softBind 函數的實現方案,就是軟綁定。默認綁定到全局對象的函數,在嚴格模式下 this 是 undefined。

參考文檔

  • javascript 設計模式與開發實踐
  • 你不知道的 javascript(上卷)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章