深入理解JavaScript之this全面解析

    在之前的章節裏我們知道,this  是在函數運行時綁定的,它只與函數在哪被調用有關係

  1.1  調用位置

   在理解  this  的綁定之前,我們先理解  this  的調用位置,

  調用位置就是函數在代碼中被調用的位置(而不是聲明位置)

   通常來說,尋找調用位置就是尋找“函數被調用的位置”,看起來很簡單,但是某些編程模式會隱藏真正的調用位置。

   尋找調用位置,實際上就是尋找分析調用棧(就是爲了到達當前執行的函數位置所調用的所有函數)

  我們所關心的調用位置就在當前正在執行的函數的前一個調用中

  

  下面看個例子


     function baz() {
         //當前函數位置是baz
         //調用棧:全局-->baz
         //調用位置是當前函數位置的上一個位置,全局作用域調用了baz(),所以調用位置是全局作用域
         console.log("baz");
         bar();       //bar函數被調用
     }
     function bar() {
         //當前函數位置是bar
         //調用棧:全局-->baz-->bar
        //調用位置是當前位置的上一個位置,baz()調用了bar(),所以調用位置是baz()
         console.log("bar");
         foo();      //foo函數被調用
     }
     function foo() {
         //當前函數位置是foo
         //調用棧:全局-->baz-->bar-->foo
         //調用位置是當前位置的上一個位置,bar()調用了foo(),所以調用位置是bar()
         console.log("foo");
     }
     baz();         //baz被調用  
    

  一般而言,都是從全局作用域中逐一推斷出調用棧的位置。

 

   可以將調用棧想象成一個函數調用鏈,就像我們在上面代碼裏面分析的那樣,但是這種方法非常麻煩並且容易出錯,,另一個查看調用棧的方法是通過瀏覽器的調試工具。當今絕大多數瀏覽器都內置了開發者工具,其中包括JavaScript調試器。

   就本例來說,你可以在foo(...)中設置一個斷點,或者直接在foo(...)中第一行代碼中插入一個debugger語句,在運行代碼時,調試器會在那個位置暫停,同時會顯示當前位置的函數調用列表,這就是你的調用棧,然後找到棧中第二個元素,這就是真正的調用位置。

 

  下面我們就chrome瀏覽器爲例,看看如何設置斷點,並查看調用棧。

​​​​​​​1、用chrome打開需要調試的JS頁面

2、按下F12,打開“開發者工具”

3、點擊開發者工具中的"sources"打開需要調試的JS頁面

4、鼠標單擊,代碼行號就能設置斷點了,如下圖我們在19行處的bar(...)函數中設置了斷點,程序運行到19行處便會停止。

5、點擊  "call stack"  查看當前設置斷點函數的調用棧(注意是當前函數的上一個函數纔是此函數的調用位置)如下圖,我們在bar(...)處設置了斷點,"call stack"中顯示了三個調用棧,從下到上依次調用,有一個藍色圖標的是當前設置了斷點的函數,上一個函數baz(...)纔是調用bar(...)的調用位置

 

1.2   綁定規則 

    我們接下來看看,在函數的執行過程中調用位置是如何決定  this  的綁定對象的。

   在JavaScript中有四個  this  的綁定規則,但是,應用哪一條規則要看  this函數  的調用位置。

1.2.1  默認綁定

   首先要介紹的是最常用的函數調用類型:獨立函數調用。可以把這條規則看作是當其他規則無法應用時纔會應用的一條規則。

看看下面的代碼

 function foo() {
            console.log(this.a);
        }
        var a=2;
        foo();      //2

  你應該注意到的一件事是

聲明在全局作用域中的變量(此例中是  var a=2)是全局對象的一個同名屬性

  接下來我們看到當調用了  foo(...)  函數時,this.a  被解釋成了全局變量a  。這是爲什麼?  因爲在本例中,函數調用時應用了  this  的默認綁定,因此  this  指向了全局對象。

  那麼我們如何知道此處是應用了  this  的默認綁定規則呢?

   我們首先來看看,foo函數的調用棧,通過分析我們可以得出,是全局對象調用了foo(...)函數,代碼中的foo(...)是直接使用不帶任何修飾的函數引用進行調用的。因此只能使用默認綁定,而不能使用其他規則

 

如果使用嚴格模式"strict mode",那麼全局對象將無法應用默認綁定,  this會綁定到"undefined"

 function foo() {
          "use strict";
            console.log(this.a);
        }
        var a=2;
        foo();      //TypeError:this is undefined

默認綁定規則:

非嚴格模式下-----------------------------------------發生獨立函數調用(函數直接使用不帶任何修飾的函數引用進行調用)且調用位置是全局作用域時,this  綁定全局對象。

嚴格模式(strict more)下----------------------------this 與函數調用位置無關,不適用默認綁定規則

 function foo() {
            console.log(this.a);
        }
        var a=2;
        (function () {
            "use strict"
            foo();   //2
        })();

注意:通常來說JavaScript代碼中要麼非嚴格,要麼嚴格,不提倡有一些嚴格,有一些不嚴格,但是在引用類庫中,可能會遇到,非嚴格與嚴格並存的情況,這個時候要注意到這種兼容性的小細節

 

1.2.2  隱式綁定

  另一條需要考慮的是,調用位置是否存在上下文對象。或者說   this函數  是否被某個對象擁有或者包含。這個說法不夠嚴謹。

  我們舉個例子來說明

 function foo() {
           console.log(this.a);
       }
       var obj={
           a:2,
           foo:foo
       }
       obj.foo();

    首先要注意的是,foo(...)的聲明方式,及其是如何被添加爲obj的引用屬性的。但是無論是直接在obj中定義還是先定義再添加爲引用屬性,都改變不了foo(...)嚴格來說不屬於obj對象。

  然而調用位置會使用  obj  的上下文來引用函數,因此你可以說函數被調用時  obj  對象包含或者擁有它。

   無論你怎麼稱呼這個模式,當foo(...)函數被調用時,它的落腳點確實指向  obj  對象。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的  this  綁定到這個上下文對象。因爲調用foo(...)時  this  被綁定到obj,因此this.a和obj.a是一樣的。

 

  那假如它有多個上下文對象呢?比如這個函數處在一個對象屬性引用鏈中

對象屬性引用鏈只有最頂層或者說最後一層會調用位置

看下面的例子

 function foo() {
           console.log(this.a);
       }

       var obj2={
           a:3,
           foo:foo
       };

       var obj1={
           a:2,
           obj2:obj2
       };

       obj1.obj2.foo();   //結果爲3,對象屬性引用鏈

  foo被obj2調用,但是引用方式卻是通過對象屬性引用鏈來實現的,"obj1.obj2.foo"。按照我們的隱式綁定規則來看,this  只會綁定最後一層的對象,於是此例中  this 與obj2  進行了綁定,輸出的爲  obj2  中的a

 

存在的問題:隱式丟失---------被隱式綁定的函數會丟失綁定對象,它會應用默認綁定從而把this綁定到全局對象(非嚴格模式)或者undefined(嚴格模式)上

看下面的一段代碼


      function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo
      };
      var bar=obj.foo;   //函數別名,將foo()賦給了bar()
      var a=3;
    bar();        //3,此處bar()沒有添加別的修飾詞,是獨立函數調用,所以應用默認綁定。發生了隱式丟失
    

   我們剛開始時讓foo()函數隱式綁定obj,但是當"var bar=obj.foo"時,bar  就已經算obj.foo的另一種引用了,bar引用的是foo()函數的本身,那麼此時的"bar()"其實是一種不帶任何修飾的函數調用,因此發生了默認綁定。輸出全局對象a的值

 

  另一種更加隱蔽的隱式丟失發生在:傳遞參數期間也會發生隱式丟失----------即回調函數傳入參數時


      function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo
      };
      function doFoo(fn) {       //foo被當成參數傳入
          fn();                    //foo被調用
      }
      var a=3;
      doFoo(obj.foo);     //傳入的是obj.foo,參數傳遞中發生了隱式丟失
    

  這裏同樣發生了隱式丟失,且是在參數傳遞的過程中發生了隱式丟失,doFoo(obj.foo)傳入的同樣是foo()函數本身,而且在調用時,是不帶有任何裝飾(上下文對象)的調用,因此  this  應用默認綁定。

 

  如果把函傳入語言的內置函數而不是你定義的函數,比如傳入setTimeout(),會怎樣?

  結果也是一樣的。

  function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo
      };
      var a=3;
      setTimeout(obj.foo,0);    //setTimeout傳入的是foo函數本身,且是不帶任何裝飾的調用,因此是默認綁定

setTimeout()函數和以下的僞代碼類似

   function  setTimeout(fn,delay){
       .....//delay延時
           fn();   //調用fn
}

  就如同這個函數被定義在之前的代碼裏執行的一樣。

 

實際上使用回調函數時,丟失this的綁定是非常常見的,你無法控制回調函數的執行方式,因此就沒有辦法控制會影響綁定的調用位置,但是可以通過固定this解決這個問題。

 

1.2.3   顯示綁定

  在之前我們講到了隱式綁定,在分析隱式綁定時,我們必須在一個對象內部包含一個指向函數的屬性,並通過這個屬性間接引用函數,從而把這個  this  間接(隱式)綁定到這個對象上

  既然有了隱式綁定,那麼就有顯示綁定,就像當我們不想在對象內部包含函數引用(在對象內部,創建此函數屬性),想要在某個對象上強制調用該函數,這就是隱式綁定。

  JavaScript中的“所有”函數都有一些有用的特性(原型),可以用來解決這個問題,具體點來說是使用我們之前接觸過的"call(...)"---強制綁定this對象,以及"apply(...)"方法。

  嚴格來說,JavaScript的宿主環境有時會提供一些非常特別的函數,它們並沒有這兩個方法(就是某一些函數用不了這兩個方法),但是這種函數非常罕見,JavaScript大多數函數都可以使用"call(...)"和"apply(...)"方法

  

  接下來我們看看這兩個函數是如何工作的

  函數.方法名(對象)

  它們第一個參數是對象,它們會把這個對象綁定到this,接着在調用函數時指定這個this。如此便將此函數的this和對象綁定到了一起,這就是顯示綁定的由來

  call與apply的作用相同,區別在於,兩者傳入的參數不同

 

  看下面的代碼

function foo() {
          console.log(this.a);
      }
      var obj={
          a:2
      };
      var   a=3;
      foo.call(obj);       //2,call將foo函數中的this顯示綁定到obj對象

 通過foo.call(...)我們在調用foo的時候強制把它的this綁定到obj上。

  如果你正在call(...)中傳入的不是一個對象,而是一個原始值(字符串類型、布爾類型或者數字類型)來當做this的綁定對象,這個對象會轉化成它的對象形式(也就是new String(...)、new Boolean(...)或者new Number(...))-------------------原始值對象轉化爲它的對象形式稱之爲“裝箱”

  儘管顯式綁定很牛逼!!!但是在我們不清楚綁定對象時,仍然無法解決我們的隱式綁定丟失的問題

但是顯式綁定的一個變種可以解決這個問題

1  硬綁定

  思考以下代碼

 function foo() {
          console.log(this.a);
      }
      var obj={
          a:2
      };
      var bar=function () {
          foo.call(obj);     //在bar()中使用call方法將obj對象練到了一起
      }
      bar();  //2
        setTimeout(bar(),10) //2
          bar.call(window);  //2

  在bar(...)函數中將foo函數顯式綁定在obj中,無論bar(...)在哪被調用,它的內部都在使foo(...)顯式綁定  obj  對象。因此無論如何調用bar(...),它綁定的對象都在obj中。這就是硬綁定

硬綁定:隱式丟失的主要原因是在隱式綁定的過程中,我們this的對象發生了改變,那麼我們只需要將this在調用之前提前綁定到合適的對象就行。

硬綁定典型的應用場景

  • 創建包裹函數:傳入所有的參數並返回接收到的所有值
function foo(something) {
         console.log(this.a,something);         //輸出,輸入的參數
         return this.a+something;           //返回接收到的參數,值爲a+參數
     }
     var obj={
         a:2
     };
     var bar=function () {       //創建包裹函數
        return foo.apply(obj,arguments);
     };
     var b=bar(3);   //2  3
     console.log(b);  //5(2+3)

  所謂包裹函數其實就是封裝函數,把該函數重要的內容封裝起來,只需要知道怎麼用即可,用戶接觸不到核心的函數(foo(...)以及"obj")。

  如上圖中包裹函數爲bar(...),用戶真正接觸到的只有bar(...)而沒有foo(...)以及"obj",用戶只需要知道bar(...)的用法,傳入的參數即可。

 

 

  • 創建i可以重複使用的輔助函數:

     function foo(something) {
         console.log(this.a,something);         //輸出,輸入的參數
         return this.a+something;           //返回接收到的參數,值爲a+參數
     }
    function bind(fn,obj) {      //創建了捆綁函數,使傳入的函數fn與對象obj實現硬綁定
        return function () {
            return fn.apply(obj,arguments);          //實現了硬綁定
        }
    }
    var obj={
         a:2
    };
     var bar=bind(foo,obj);
     var b=bar(3);     //2  3
     console.log(b);    //5
    

 

 

由於在硬綁定是非常常用的模式,因此在ES5中一共了內置的方法:Function.prototype.bind,它的用法和用法跟我們上個代碼中創建的捆綁函數"bind(...)"類似


     function foo(something) {
         console.log(this.a,something);         //輸出,輸入的參數
         return this.a+something;           //返回接收到的參數,值爲a+參數
     }
    var obj={
         a:2
    };
     var bar=foo.bind(obj);     //函數.bind(this的綁定對象)
     var b=bar(3);     //2  3
     console.log(b);    //5
    

bind(...)會返回一個硬編碼的新函數,它會把參數(obj)設置爲this的上下文對象並調用原始函數(foo)。

  bind(...)還有一個非常重要的作用便是實現柯里化:bind(...)能把除了第一個參數(用於綁定this)之外的參數傳遞給下層的函數

 

2   API調用的上下文

  第三方類庫的許多函數,以及JavaScript語言和宿主環境中有許多新的內置函數,都提供了一個可選的參數,通常稱之爲“上下文”,其作用和bind(...)一樣,確保你的回調函數使用指定的this。

  舉例來說:

   function foo(el,id) {
            console.log(el,this.id);
        };
        var obj={
            id:"awesome"
        };
        [1,2,3].forEach(foo,obj);   //傳入的參數.forEach(函數,對象);
          //1  awesome
          //2  awesome
          //3  awesome

 

1.2.4  new綁定

  這是最後一條規則“new綁定規則”,在講解這條規則之前,首先讓我們澄清一個非常常見的關於javascript中函數和對象的誤解。

  在傳統的面向對象的語言中,“構造函數”是類中的一些特殊方法,使用  new  初始化類時會調用類中的構造函數。通常的形式是這樣的:

                                                    something = new  Myclass(...);

  在就JavaScript中也有一個  new  操作符,使用方法看起來也跟面向對象的語言一樣,學過面嚮對象語言的開發者會認爲這個機制跟面向對象中的機制一樣,然而兩者天差地別。

  爲什麼呢???

首先我們先理解一下JavaScript中的構造函數

  • 在JavaScript中構造函數只是一個普通的函數
  • 它們不屬於某個類,也不會實例某個類
  • 在使用  new  操作會調用的普通函數

舉例說明:

ES5.1中是這麼描述Number(...)作爲構造函數的

15.7.2  Numbar構造函數

   當  Numbar  對象在  new  表達式中被調用時,它是一個構造函數:它會初始化新創建的對象

  所以包括內置對象函數(比如Numbar(...))在內的所有函數都可以用new來調用,這種函數稱爲構造函數調用

  也就是說:在JavaScript中沒有所謂的“構造函數”,只有對於函數的“構造調用”

 

我們來分析一下使用  new  調用函數----發生函數“構造調用”的過程

  1. 創建(構造)一個全新的對象
  2. 這個對象會被執行“原型”連接
  3. 這個對象會被綁定到函數調用的this
  4. 如果函數中沒有返回其他對象,那麼  new  表達式中的函數調用會自動返回這個新對象。

 舉例說明使用  new  調用函數的過程。

  function foo(a) {
         this.a=a;
     }
     var bar=new foo(2);
    console.log(bar.a);

  使用  new  來調用foo(...)函數時,我們會構造一個新對象並把它綁定到foo(...)中調用的  this  上去---------------這就是new綁定

  若還有不明白的  new  可在https://www.cnblogs.com/faith3/p/6209741.html中參考

 

1.3  優先級

  我們已經介紹完了四條this綁定的規則:

  1. 默認綁定----------不帶有任何修飾的獨立函數調用
  2. 隱式綁定----------帶有上下文對象的調用(方法調用)
  3. 顯式綁定----------call、apply以及API顯式調用(間接調用)
  4. new綁定-----------new構造調用

  那麼如果符合多條應用規則,我們該用哪條規則呢?

  首先我們不用考慮“默認綁定”,這肯定是在後的了,只有在不符合其餘三條規則下,纔會應用“默認規則”

  接下來我們比較下“隱式綁定”與“顯式綁定”的優先級,誰更高呢?

  看代碼:


   function foo() {
       console.log(this.a);
   }
   var obj1={
       a:2,
       foo:foo
   };
   var obj2={
       a:3,
       foo:foo
   };
   //隱式綁定開始
     obj1.foo();   //2
     obj2.foo();   //3
   //應用隱式綁定,又用顯示綁定
    obj1.foo.call(obj2);   //3
    obj2.foo.call(obj1);    //2
    

  我們創建了foo(...)函數,其中有  this  ,“obj1”和“obj2”對象,他們中的"a"值分別爲2以及3。我們首先應用了隱式綁定,可以看出輸出結果正確,接下來我們用上隱式綁定"帶有上下文的引用"再用顯式綁定"call方法調用",很明顯“obj1”輸出的是“obj2”的值,這正是顯式調用比隱式調用優先級高的最有利證據。

  顯式綁定比隱式綁定優先級高

 

接下來我們需要搞清楚,new綁定和隱式綁定,哪個優先級更高。

看接下來的代碼


  function foo(something) {
      this.a=something;        //將穿傳入的參數變成a屬性
  }
  var obj1={
      foo:foo
  };
  var obj2={};

     obj1.foo(2);
     console.log(obj1.a);         //2

     obj1.foo.call(obj2,3);         //顯式綁定
        console.log(obj2.a);       //3

        var bar=new obj1.foo(4);
        console.log(obj1.a);       //2    隱式綁定
        console.log(bar.a);           //4        new綁定將隱式綁定的a改變了


    

     從中可以看出

           new綁定比隱式綁定優先級更高。

 

  那麼new綁定與顯式綁定,誰的優先級更高呢?

new與call/apply無法同時使用,因此無法通過new  foo.call(obj1)來直接進行測試,但是我們可以使用硬綁定來測試它們的優先級

  在看接下來的代碼時,讓我們來回憶,硬綁定是如何工作的?Function.prototype.bind(...)會創建一個新的包裝函數,這個函數會忽略它當前的  this  綁定(無論綁定的對象是什麼),並把我們提供的對象綁定到  this  上。

  接下來我們通過代碼來看看,new和硬綁定誰的優先級更高。

function foo(something) {
      this.a=something;        //將穿傳入的參數變成a屬性
  }
  var obj1={};
        var bar=foo.bind(obj1);        //在bar中我們將obj1與foo對象強制綁定到了一起
        bar(2);
        console.log(obj1.a);    //2

//按道理來講,使用bar作爲構建調用時,obj1也應該和foo綁定到一起
        var baz=new bar(3);        
        console.log(obj1.a);     //2  obj1.a的值沒有改變,證明硬綁定被解開了
        console.log(baz.a);    //3   證明產生的新對象將硬綁定的this解開指向了新對象,否則obj1.a=3

    出乎意料!bar被硬綁定到obj1上,但是new bar(3)並沒有想我們預計(new綁定的優先級比硬綁定的優先級低)的那樣改變obj1.a的值爲3。相反new綁定修改了硬綁定(到obj1的)調用bar(...)中的this,因爲使用了new綁定,我們得到一個名字爲baz的新對象,修改了baz.a=3。這也就意味着

                         new綁定比硬綁定優先級別更高

 

  new看起來無法修改硬綁定的this,然而事實卻不一樣,在JavaScript中,會首先判斷硬綁定函數是否被new調用,如果是的話就會使用新創建的this替換硬綁定的this。

  那麼爲什麼要在new中使用硬綁定呢?這是因爲在new中使用bind(...)函數可以將傳入的參數,除了對象之外,傳遞給下一層的函數。

舉例:

function foo(p1,p2) {
     this.val=p1+p2;
 }
 //之所以用null,是因爲此處我們不用管this硬綁定的對象
 //反正不管是什麼,this的值都會被修改
 var bar=foo.bind(null,"p1");
 var baz=new bar("p2");
   baz.val;   //p1p2

 

1.4   this的引用順序

現在我們可以根據之前的結論來判斷this的綁定順序了

①函數是否在new中調用(new綁定),是的話this綁定的是新創建的對象。

    var baz=new foo()

②函數是否通過call、apply或者硬綁定(在一個函數A中,對象obj使用call或者是apply永遠與this函數綁定到了一起,無論何時在哪調用函數A,this指向的都是對象obj)調用?如果是的話,this綁定的是指定的對象。

  var bar=foo.call(obj1);

③函數是否在某個上下文對象中被調用(隱式綁定)?是的話,this指向這個上下文對象

  var bar=obj1.foo();

④如果都不是的話,就是用默認綁定,在嚴格模式下,綁定"undefined",否者就綁定全局對象。

  var bar=foo();

 

1.5  例外的綁定

  規則總有例外,在這裏也一樣,在某些場景下的this綁定可能不按照我們之前的優先級來,而是直接綁定了默認規則。

1.5.1  被忽略的this

  在你把"undefined"或者"null"作爲this的綁定對象傳入"call(...)"、"apply(...)"以及"bind(...)"時,這些值會被忽略,而this會應用默認綁定。

var a=2;
  function foo() {
      console.log(this.a);
  }
  foo.call(null);       //2
  foo.call(undefined);  //2

  foo.apply(null);        //2
  foo.apply(undefined);    //2

  那麼什麼情況下,你需要往顯式綁定方法中傳入"null"呢?

  一種常見的做法是使用apply(...)來“展開”一個數組,並當作參數來傳入下一個函數,類似的,bind(...)可以對參數進行柯里化(預先放置一個參數)。

柯里化:把接受多個參數的函數變爲只接受開始的第一個參數的函數,並且返回接受餘下的參數且返回結果的新函數。通俗點解釋是:接受一個單一可以預期的參數,返回一個正確結果,http://www.cnblogs.com/pigtail/p/3447660.html參考這篇文章

  下面我們來看看,如何用apply(....)“展開”一個數組

 function foo(a,b) {
      console.log("a:"+a+"b:"+b);
  }
  foo.apply(null,[2,3]);   //a:2 b:3    把數組展開成參數

如何用bind(...)實現柯里化

 function foo(a,b) {
      console.log("a:"+a+"b:"+b);
  }
  var bar=foo.bind(null,2);    //a=2;
    bar(3);      //a:2  b:3

  如上我們可以看到,原本傳入的foo的參數爲"a""b"但是在使用bind(...),以及傳入的綁定對象是"null"之後我們在foo.bind()中就不需要將"a""b"的值完全輸入。

  這也就相當於變相的縮小了函數的使用範圍,例:原本foo函數可以用於"a""b"值爲任意值,但是在"var bar=foo.bind(null,2)"之後,我們使用"bar"只能用在'a=2"的場合----------------這就是柯里化

 

  這兩種方法都需要傳入一個參數作爲綁定對象,當我們不關心函數綁定的this=時,你仍然需要傳入一個參數才能使用這個函數,那麼“null”是一個正確的選擇

  注意!!!儘管這種做法有時候很實用,但是在this已經實現綁定時,忽略此this會導致,this綁定

結果出錯(綁定成全局對象)

   

更安全的this

   那麼有沒有別的更安全的做法忽略掉這次的this呢?

  其實有種做法比傳入"null"還要安全,這就是"DMZ"

 這種做法是傳入一個特殊的對象,把this綁定到這個對象不會對你的程序起到任何的副作用。這個對象是"DMZ"-------一個非空的委託對象。

如果我們將"DMZ"作爲對象傳入this以達到忽略this的目的,那麼,任何對this的使用都會被限制在這個空對象裏,對外界沒有任何影響

  在JavaScript中創建這個空對象最簡單的方法是:object.create(null)。這個與{}很像,單不同的是它不會創建object.prototype這個委託,它比{}更空。

function foo(a,b) {
     console.log("a:"+a+"b:"+b);
 }
 var ø=Object.create(null);   //創建DMZ空對象
  foo.apply(ø,[1,2]);         //利用DMZ空對象傳入參數到this函數中,對參數進行展開
       //利用bind進行柯里化
    var bar=foo.bind(ø,2);
    bar(3);

  儘管我們可以將"DMZ"對象名更改爲我們喜歡的名字,但是仍然是建議大家使用ø

 

1.5.2  間接引用

  另外一個經常容易犯的錯誤便是,你可能有意無意的創建一個函數的“間接引用”,在這種情況下this會默認綁定。

 IIFE中賦值

function foo() {
     console.log(this.a);
 }
 var a=4;
   var obj={
     a:3,
       foo:foo
   };
 var baz={
     a:2
 };
 (baz.foo=obj.foo)();         //4

 賦值表達式p.foo=o.foo的返回值是目標函數的引用,因此調用位置是foo()而不是o.foo()或者p.foo()。因此這裏使用默認綁定

函數賦值

function foo() {
     console.log(this.a);
 }
 var a=4;
   var obj={
     a:3,
       foo:foo
   };
 var bar=obj.foo;
 bar();       //4

  注意這裏!!!默認綁定輸出的是不是"undefined"並不取決於調用this的位置,而取決於this函數體(這裏是foo函數)是否爲嚴格模式。

 

1.5.3  軟綁定

  之前我們有提到硬綁定,強制使this指向一個我們希望指向的對象,隱式綁定、顯式綁定都無法改變它(除了new綁定以外)。這樣儘管很好,但是卻犧牲了函數的靈活性,使用硬綁定就無法使用顯式綁定或者隱式綁定修改this指向的對象,只能通過改變硬綁定函數內的函數體。

  如果給默認綁定對象指定一個全局對象和"undefined"以外的值,就可以實現和硬綁定同樣的效果,同時隱式綁定和顯式綁定還可以修改this。

  這種用默認綁定實現硬綁定功能的做法,我們稱之爲-----------軟綁定

if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲所有 curried 參數
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}

  除了軟綁定之外,softBind()的其他原理與ES5中的bind(...)類似。

  1. 對指定函數進行封裝
  2. 檢查使用的this是否綁定到全局對象或者是"undefined"
  3. 是的話就把默認對象obj綁定到this上
  4. 不是的話,就不會修改this
  5. 此函數支持柯里化

softBind()例子

function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
//隱式綁定開始
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看!!!
//顯式綁定開始
fooOBJ.call( obj3 ); // name: obj3 <---- 看!
setTimeout( obj2.foo, 10 );
// name: obj <---- 應用了軟綁定

  可以看到軟綁定的foo()可以手動將this綁定到obj2或者obj3的身上,但是如果應用默認綁定,則會將this綁定到obj身上。

 

1.6  this詞法

  之前我們介紹了this綁定的四種規則,然而在JavaScript中並不是所有的函數體都遵循着這四個規則,其中箭頭函數便是如此。

  箭頭函數並不是用function關鍵字定義的,而是使用被稱爲“胖箭頭”的操作符=>定義的函數(使用胖箭頭代替function聲明),胖箭頭不適用this的四條規則,胖箭頭函數的this是根據外層作用域來決定的

function foo() {
      return (a)=>{
          console.log(this.a);
      }
  }
 var obj1={
      a:2
 };
  var obj2={
      a:3
  };
  var bar=foo.call(obj1);   
  bar.call(obj2);    //2不是3

  foo(...)內部創建的胖箭頭會捕獲調用時foo()的this。由於foo()的this綁定到obj1,bar的this也綁定到obj1,胖箭頭的this無法修改,即使是new綁定也不行。

胖箭頭一般用於回調函數中,例如事件處理器或者定時器

 function foo() {
      setTimeout(()=>{
          console.log(this.a);
      },100);
  }
  var obj={
      a:2
  };
  foo.call(obj);   //2

  利用箭頭函數可以像硬綁定bind(...)那樣,確保this綁定到指定對象,不同的是它是利用作用域。而我們在寫代碼時,儘量使用一種設計模式-----要麼用四條規則綁定,要麼使用胖箭頭作用域綁定。

 

總結:

  this在JavaScript的綁定對象,根據以下步驟查找

①new調用?綁定創建的新對象---------------構造函數調用

②由call()、apply()、bind()調用?綁定到指定對象------------間接調用

③由上下文對象調用?綁定到上下文對象-------------------------方法調用

④函數直接使用不帶任何修飾的引用?綁定到全局對象--------獨立調用

 

能解決隱式丟失的只有:硬綁定(bind(...))

當使用間接引用時,很容易發生隱式丟失,要仔細看,一般發生了隱式丟失,都會應用默認綁定。

  當想要安全的忽略掉this綁定時,那麼久傳入一個"DMZ"對象,創建方法:

var ø=Object.create(null)。

  ES6中的胖箭頭函數不在四條this綁定規則之內,胖箭頭函數一旦綁定之後new綁定都無法改變綁定對象,它的this綁定只於上一層的作用域有關,這是用作用域影響this綁定。這與ES6之前的"self=this"一樣。

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章