1. 程式人生 > >深入理解JavaScript之this的四種繫結

深入理解JavaScript之this的四種繫結

  之前對this的四種繫結不太理解,好在瀏覽了https://www.cnblogs.com/xiaohuochai/p/5735901.html這篇博文,才得以清晰思路,接下來我再次總結this的四種繫結機制。

1  this的四種繫結機制

  在JavaScript中this共有四種繫結機制:分別是

  • new繫結
  • 顯式繫結(包括硬繫結)
  • 隱式繫結
  • 預設繫結

  繫結優先順序從上到下

2  預設繫結

  •  在非嚴格模式下預設繫結指的是,函式獨立呼叫,不新增修飾的函式呼叫,它繫結的是全域性物件
  • 嚴格模式下,this繫結"undefined"

接下來我們將預設繫結分為幾種情況,在這幾種情況下發生的一般都是預設繫結

2.1全域性作用域中使用this

  我們不在任何函式中使用this,直接在全域性作用域中使用this,那麼這個this指向的是全域性物件

 var a=2;
      console.log(this.a);    //繫結全域性物件屬性a=2

2.2  函式獨立呼叫時

  我們在全域性作用域中單獨呼叫某一個函式,這個函式的this繫結的是全域性物件

   function foo() {
         var a=3;
         console.log(this.a);
     }
     var a=2;
     foo();   //值為2,而不是3,代表繫結的是全域性物件

2.3  被巢狀的獨立函式呼叫時

  被巢狀的函式獨立呼叫時,this預設繫結的是window物件

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

  雖然bar(...)是巢狀在obj的foo(...)函式中,由於bar(...)是獨立呼叫,而不是方法呼叫,因此this被繫結到全域性物件window

2.4 立即執行函式表示式

  在立即執行函式表示式中的this也會預設繫結全域性物件。

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

  foo(...)是obj的屬性函式,應該是屬於上下文呼叫,this的繫結是隱式繫結,但是在foo(...)函式中我們執行的程式是IIFE,那麼this就會預設繫結全域性物件。

2.5  閉包

  我們先回憶一下閉包,閉包是外部函式內部定義了內部函式,在全域性作用域(或者是其他的作用域,只要不是外部函式本身的作用域)中呼叫內部函式,使得可以訪問外部函式作用域的功能。

 var a=2;
   function foo() {     //外部函式
      function bar() {      //內部函式
          console.log(this.a)
      }
      return bar();         //返回內部函式
   }
   var obj={
       a:3,
       foo:foo()
   };
   obj.foo;        //2  

  那麼在閉包中,我們需要訪問到外部函式的作用域,這時我們該怎麼做呢?答案是使用  var self=this,在內部函式中使用self代替this。

 var a=2;
        function foo() {     //外部函式
            var self=this;        //self代替this
            function bar() {      //內部函式
                console.log(self.a)       //self代替this
            }
            return bar;         //返回內部函式
        }
        var obj={
            a:3,
            foo:foo
        };
        obj.foo()();        //3

 

2.6  隱式丟失

  在this的隱式繫結過程中,由於失去了繫結物件,從而應用預設繫結規則。

2.6.1 函式別名的隱式丟失

  在this的隱式繫結過程中,給帶有上下文的this函式值起一個別名(建立一個新的物件)

 var a=2;
   function foo() {
    console.log(this.a);
   }
   var obj={
       a:3,
       foo:foo
   };
obj.foo;    //3
   var bar=obj.foo;     //函式別名
   bar();        //2  隱式繫結的過程中發生了丟失

  原本在obj中的foo屬性:foo(...)中的this繫結上下文obj物件,但是我們建立一個bar()作為obj.foo的引用,實際上bar()引用的是foo(...)函式的本身,bar()是一個不帶任何裝飾的函式呼叫,因此預設函式呼叫

2.6.2  傳入回撥函式時,引數的隱式賦值

  在傳入回撥函式時,引數之間的隱式賦值也會導致this物件丟失從而應用預設繫結

 var a=2;
   function foo() {
    console.log(this.a);
   }
   var obj={
       a:3,
       foo:foo
   };
     function doo(fn) {   //傳入obj.foo,傳入的實際上只有foo(...),這算是一個獨立的函式呼叫
         fn();
     }
     doo(obj.foo);        //2    obj.foo傳值給foo,就跟之前的var fn=obj.foo;

2.6.3  內建函式

  當我們使用Javascript時內建函式時,也會發生this物件的改變,從而應用預設繫結

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

  我們應用了"setTimeout(...)"內建函式,原本foo(...)中的this應該繫結到obj上,但是我們引用了javascript內建函式,“obj.foo”作為引數,導致this應用了預設繫結。

 

3  隱式繫結

  當呼叫有上下文物件的函式時,便是隱式繫結。通俗點來說,就是被直接物件所包含的函式呼叫時,也稱為該直接物件的方法呼叫。

 function foo() {
          console.log(this.a);
      }
      var obj={
          a:2,
          foo:foo     //foo成為obj中的方法
      }
      obj.foo();    //obj物件的foo方法----方法呼叫,

  函式foo(...)被儲存在obj物件中,成為obj物件的一個屬性,這時,我們稱這個函式(foo)為這個物件(obj)的方法,在最後一行,我們對這個函式進行呼叫(物件.屬性或者說是物件.方法),這就是方法的呼叫。而這個物件我們稱之為上下文物件。

 

3.1  多個巢狀的上下文物件呼叫

  什麼是多個巢狀的上下文物件呼叫呢,就是在一個物件中引用另一個物件的方法,此時的this會繫結第一的物件還是最後的物件呢?

function foo() {
          console.log(this.a);
      }
      var obj2={
          a:2,
          foo:foo
      };
      var obj1={
          a:3,
          obj2:obj2,
          foo:foo
      };
       obj1.foo();            //3
       obj2.foo();         //2
       obj1.obj2.foo();     //2    巢狀的上下文呼叫

  從結果可以看出,當發生巢狀的上下文呼叫時,首先this傳入的值是obj2的值,那麼就是說發生巢狀的上下文呼叫時,this的繫結物件是離this最近的那一個。

  注意!!!在這裡,var  obj2的定義一定要比  var  obj1的定義位置靠前,因為之前講了函式的提升,變數(物件)的提升優先順序是一樣的,當先宣告obj1後宣告obj2時,會發生錯誤。

 

3.2  間接引用

  在一般的程式設計中,函式的間接引用是最容易導致隱式丟失的

ar a=2;
  function foo() {
      console.log(this.a);
  }
  var obj1={
      a:3,
      foo:foo
  };
  var obj2={
      a:4,
  };
  obj1.foo();   //3
  obj2.foo=obj1.foo;   //foo不存在obj2中,this繫結的物件應該是obj1
  obj2.foo();          //4
   var a=2;
  function foo() {
      console.log(this.a);
  }
  var obj1={
      a:3,
      foo:foo
  };
  var obj2={
      a:4,
  };
  obj1.foo();             //3
    (obj2.foo=obj1.foo)();     //2 IIFE中this預設繫結

3.3  其他情況

  在JavaScript內部,obj物件單獨有一個記憶體地址M1,obj的屬性foo函式單獨有一個記憶體地址M2,當"obj.foo"時,引擎首先找到M1地址,再從M1地址處呼叫M2地址,這樣foo的this就會繫結到obj上。

  如果單獨引用M2地址,就會發生隱式丟失

 

4  顯示繫結

  顯示繫結是利用JavaScript中的方法".call(...)、.apply(...)"強制this繫結到指定物件上。對於被呼叫的函式來說這是間接引用

4.1  call()方法

  call方法是JavaScript中,強制使物件繫結到指定物件上。

var a=3;
 function foo(b) {
     console.log(this.a);
 }
 var obj={
     a:2
 };
 foo(1);    //4
 foo.call(obj,1);         //3  強制foo繫結到obj上

4.2  apply()方法

    apply()方法與call()方法作用相同,不同的是兩者除了第一個引數是物件其餘的引數不同。具體可看這一篇文章

https://www.cnblogs.com/lengyuehuahun/p/5643625.html

 var a=3;
 function foo(b,c) {
     console.log(this.a+b+c);
 }
 var obj={
     a:2
 };
 foo(1,2);    //3
foo.apply(obj,[1,2]);   //apply()傳入的引數必須要放到一個數組中

   儘管顯式繫結很厲害,但是還是解決不了隱式丟失的問題。

4.3  硬繫結

  儘管顯式繫結無法解決隱式丟失的問題,但是顯式繫結的變種-----硬繫結,可以。

  硬繫結是,在一個函式中通過call(...)或者是apply(...)方法將帶有this的函式繫結到指定的物件中。

  簡而言之就是,將call()或者是apply()方法強制使this指向一個物件這個操作封裝起來,呼叫這個封裝函式就行了。

 var a=3;
 function foo() {
     console.log(this.a);
 }
 var obj={
     a:2
 };
   function bind() {     //在bind中this與obj物件繫結到了一起,所以只要呼叫bing函式,this肯定指向obj
       foo.call(obj);
   };
   bind();      //2
  setTimeout(bind,100);    //2
   bind.call(wind);      //2

  硬繫結是如何解決隱式丟失的呢?答案是通過封裝call()、apply()方法將this繫結到指定的物件上,等到呼叫時,用這個封裝函式就行了。如此一來,無論何時,在封裝函式中,this永遠指向物件。

  在JavaScript中由於對硬繫結的需求是非常大,所以JavaScript中定義了一個內建函式"bind(...)"來代表硬繫結

 function foo(something) {
        console.log(this.a,something);
        return this.a+something;
    }
    var obj={
        a:2
    };
    var bar=foo.bind(obj);
    var b=bar(3);
    console.log(b);               //5

4.4  API呼叫

  JavaScript中不止有"bind(...)"這一個函式,還有許多內建函式具有顯式繫結功能,如陣列迭代的五個方法:map(...)、forEach(...)、filter(...)、some(...)、every(...)

 var id='window';
   function foo(el) {
       console.log(el,this.id);
   }
   var obj={
       id:'fn'
   };
   [1,2,3].forEach(foo);  //1|2|3  window
   [1,2,3].forEach(foo,obj);  //1|2|3  fn

  

5  new繫結

  new繫結有太多細節要講,具體的可以看我之前的文章,現在是簡單介紹下new繫結的概念。

  new繫結:函式或者方法呼叫之前帶有關鍵字"new",它就是建構函式呼叫,這也就是this繫結

5.1  不帶return的new繫結

  當不帶return的new建構函式呼叫時,它會初始化新物件,當建構函式的函式體執行完畢以後,它會顯式返回。在這種情況下 ,建構函式的計算值便是"new"建立的新物件的值

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

5.2  帶有return 但是卻不返回值的new繫結

  當帶有return的函式,發生,建構函式呼叫(new繫結)時,它同上面型別的new繫結是一致的,初始化新物件,執行建構函式體,顯示返回,建構函式的計算值是"new"建立的新物件的值。

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

5.3  帶有return返回一個物件

  帶有return的函式,發生,建構函式呼叫時,並且顯式返回一個物件時,新建立的物件的值等於這個物件。

 var obj1={
        a:3
    }
   function foo() {
       this.a=2;
       return obj1;
   };
   var obj=new foo();
   console.log(obj);     //a:3

5.5  new繫結發生的那些事

  new繫結到底發生了什麼導致this物件的改變呢?

  1. 建立一個全新的物件
  2. 這個物件被執行原型連結
  3. 這個物件會被繫結到指定的this
  4. 如果函式中沒有返回物件,那麼new表示式中的函式會自動返回這個物件

5.6  new繫結的用法

  使用new繫結非常之簡單,只需要建立指定的物件,new一個this函式。

  var a=3;
    function foo(a) {
       this.a=a;
    }
         foo(a);
    var bar=new foo(2);    //建立的bar繫結到foo中的this上
    console.log(bar.a);    //2

  注意!!!儘管建構函式看起來很像一個方法呼叫,它依然會使用這個新物件作為this。也就是說,在表示式new o.m()  中,this不是o而是m()

 

6  優先順序

  在優先順序上:new>顯式繫結>隱式繫結>預設繫結

 

7  總結

  new------建構函式呼叫

  顯式繫結-----間接呼叫

  隱式繫結-----方法呼叫

  預設繫結-----獨立呼叫