整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          「干貨分享」教你徹底搞懂JavaScript中的th

          「干貨分享」教你徹底搞懂JavaScript中的this指向問題

          JavaScript中的this是讓很多開發者頭疼的地方,而this關鍵字又是一個非常重要的語法點。毫不夸張地說,不理解它的含義,大部分開發任務都無法完成。

          想要理解this,你可以先記住以下兩點:

          1:this永遠指向一個對象;

          2:this的指向完全取決于函數調用的位置;

          針對以上的第一點特別好理解,不管在什么地方使用this,它必然會指向某個對象;確定了第一點后,也引出了一個問題,就是this使用的地方到底在哪里,而第二點就解釋了這個問題,但關鍵是在JavaScript語言之中,一切皆對象,運行環境也是對象,所以函數都是在某個對象下運行,而this就是函數運行時所在的對象(環境)。這本來并不會讓我們糊涂,但是JavaScript支持運行環境動態切換,也就是說,this的指向是動態的,很難事先確定到底指向哪個對象,這才是最讓我們感到困惑的地方。

          先看原理

          function fun(){

          console.log(this.s);

          }?

          var obj={

          s:'1',

          f:fun

          }

          ?var s='2';

          ?obj.f(); //1

          fun(); //2

          上述代碼中,fun函數被調用了兩次,顯而易見的是兩次的結果不一樣;

          很多人都會這樣解釋,obj.f()的調用中,因為運行環境在obj對象內,因此函數中的this指向對象obj;

          而在全局作用域下調用 fun() ,函數中的 this 就會指向全局作用域對象window;

          但是大部分人不會告訴你,this的指向為什么會發生改變,this指向的改變到底是什么時候發生的;

          而搞懂了這些,this的使用才不會出現意外;

          首先我們應該知道,在JS中,數組、函數、對象都是引用類型,在參數傳遞時也就是引用傳遞;

          上面的代碼中,obj 對象有兩個屬性,但是屬性的值類型是不同的,在內存中的表現形式也是不同的;

          調用時就成了這個樣子:

          因為函數在js中既可以當做值傳遞和返回,也可當做對象和構造函數,所有函數在運行時需要確定其當前的運行環境,this就出生了,所以,this會根據運行環境的改變而改變,同時,函數中的this也只能在運行時才能最終確定運行環境;

          再來看下面的代碼,你可能會更加理解this對于運行環境的動態切換規則:

          var A={

          name: '張三',

          f: function () {

          console.log('姓名:' + this.name);

          }

          };?

          var B={

          name: '李四'

          };?

          B.f=A.f;

          B.f() // 姓名:李四

          A.f() // 姓名:張三

          上面代碼中,A.f屬性被賦給B.f,也就是A對象將匿名函數的 地址 賦值給B對象;

          那么在調用時,函數分別根據運行環境的不同,指向對象A和B

          function foo() {

          console.log(this.a);

          }

          var obj2={

          a: 2,

          fn: foo

          };

          var obj1={

          a: 1,

          o1: obj2

          };

          obj1.o1.fn(); // 2

          obj1對象的o1屬性值是obj2對象的地址,而obj2對象的fn屬性的值是函數foo的地址;

          函數foo的調用環境是在obj2中的,因此this指向對象obj2;

          那么接下來,我們對this使用最頻繁的幾種情況做一個總結,最常見的基本就是以下5種:

          對象中的方法,事件綁定 ,構造函數 ,定時器,函數對象的call()、apply() 方法;

          上面在講解this原理是,我們使用對象的方法中的this來說明的,在此就不重復講解了,不懂的同學們,請返回去重新閱讀;

          事件綁定中的this

          事件綁定共有三種方式:行內綁定、動態綁定、事件監聽;

          行內綁定的兩種情況:

          ?<input type="button" value="按鈕" onclick="clickFun()">

          <script>

          function clickFun(){

          this // 此函數的運行環境在全局window對象下,因此this指向window;

          }

          </script>

          ?

          <input type="button" value="按鈕" onclick="this">

          <!-- 運行環境在節點對象中,因此this指向本節點對象 -->

          行內綁定事件的語法是在html節點內,以節點屬性的方式綁定,屬性名是事件名稱前面加'on',屬性的值則是一段可執行的 JS 代碼段;而屬性值最常見的就是一個函數調用;

          當事件觸發時,屬性值就會作為JS代碼被執行,當前運行環境下沒有clickFun函數,因此瀏覽器就需要跳出當前運行環境,在整個環境中尋找一個叫clickFun的函數并執行這個函數,所以函數內部的this就指向了全局對象window;如果不是一個函數調用,直接在當前節點對象環境下使用this,那么顯然this就會指向當前節點對象;

          動態綁定與事件監聽:

          <input type="button" value="按鈕" id="btn">

          <script>

          var btn=document.getElementById('btn');

          btn.onclick=function(){

          this ; // this指向本節點對象

          }

          </script>

          因為動態綁定的事件本就是為節點對象的屬性(事件名稱前面加'on')重新賦值為一個匿名函數,因此函數在執行時就是在節點對象的環境下,this自然就指向了本節點對象;

          事件監聽中this指向的原理與動態綁定基本一致,所以不再闡述;

          構造函數中的this

          function Pro(){

          this.x='1';

          this.y=function(){};

          }

          var p=new Pro();

          ?

          對于接觸過 JS 面向對象編程的同學來說,上面的代碼和圖示基本都能看懂,new 一個構造函數并執行函數內部代碼的過程就是這個五個步驟,當 JS 引擎指向到第3步的時候,會強制的將this指向新創建出來的這個對象;基本不需要理解,因為這本就是 JS 中的語法規則,記住就可以了;

          window定時器中的this

          var obj={

          fun:function(){

          this ;

          }

          }

          ?

          setInterval(obj.fun,1000); // this指向window對象

          setInterval('obj.fun()',1000); // this指向obj對象

          setInterval() 是window對象下內置的一個方法,接受兩個參數,第一個參數允許是一個函數或者是一段可執行的 JS 代碼,第二個參數則是執行前面函數或者代碼的時間間隔;

          在上面的代碼中,setInterval(obj.fun,1000) 的第一個參數是obj對象的fun ,因為 JS 中函數可以被當做值來做引用傳遞,實際就是將這個函數的地址當做參數傳遞給了 setInterval 方法,換句話說就是 setInterval 的第一參數接受了一個函數,那么此時1000毫秒后,函數的運行就已經是在window對象下了,也就是函數的調用者已經變成了window對象,所以其中的this則指向的全局window對象;

          而在 setInterval('obj.fun()',1000) 中的第一個參數,實際則是傳入的一段可執行的 JS 代碼;1000毫秒后當 JS 引擎來執行這段代碼時,則是通過 obj 對象來找到 fun 函數并調用執行,那么函數的運行環境依然在 對象 obj 內,所以函數內部的this也就指向了 obj 對象;

          函數對象的call()、apply() 方法

          函數作為對象提供了call(),apply() 方法,他們也可以用來調用函數,這兩個方法都接受一個對象作為參數,用來指定本次調用時函數中this的指向;

          call()方法

          call方法使用的語法規則

          函數名稱.call(obj,arg1,arg2...argN);

          參數說明:

          obj:函數內this要指向的對象,

          arg1,arg2...argN :參數列表,參數與參數之間使用一個逗號隔開

          var lisi={names:'lisi'};

          var zs={names:'zhangsan'};

          function f(age){

          console.log(this.names);

          console.log(age);

          }

          f(23);//undefined

          ?

          //將f函數中的this指向固定到對象zs上;

          f.call(zs,32);//zhangsan

          apply()方法

          函數名稱.apply(obj,[arg1,arg2...,argN])

          參數說明:

          obj :this要指向的對象

          [arg1,arg2...argN] : 參數列表,要求格式為數組

          var lisi={name:'lisi'}; var zs={name:'zhangsan'}; function f(age,sex){ console.log(this.name+age+sex); }//將f函數中的this指向固定到對象zs上;f.apply(zs,[23,'nan']);

          注意:call和apply的作用一致,區別僅僅在函數實參參數傳遞的方式上;這個兩個方法的最大作用基本就是用來強制指定函數調用時this的指向。

          下來,筆者將按照以下目錄對this進行闡述:

          • this是什么?
          • this指向this在全局范圍內this在對象的構造函數內this在對象的方法內this在簡單函數內this在箭頭函數內this在一個事件偵聽器內
          • this綁定規則默認綁定隱式綁定顯示綁定(this修改)優先級
          • 箭頭函數

          1. this是什么?

          this是JavaScript的一個關鍵字,但它時常蒙著面紗讓人無法捉摸,許多對this不明就里的同學,常常會有這樣的錯誤認知:

          • this在函數內指向函數自身 function foo(num){ console.log("foo: " + num); //記錄foo被調用次數 this.count++; } foo.count=0; for(let i=0; i<10; i++){ if(i > 5){ foo(i); } } console.log(foo.count); // 0, this并沒有指向foo函數,foo.count沒有進行任何操作
          • this在函數內指向函數的作用域 function foo(){ var a=2; this.bar(); } function bar(){ console.log(this.a); } foo();// undefined, window對象沒有bar這一屬性

          2. this指向

          this的指向取決于他所處的環境. 大致上,可以分為下面的6種情況:

          • this在全局范圍內
          • this在對象的構造函數內
          • this在對象的方法內
          • this在一個簡單的函數內
          • this在箭頭函數內
          • this在一個事件偵聽器內

          2.1 this在全局范圍內

          this在全局范圍內綁定什么呢?這個相信只要學過JS,應該都知道答案。如果不知道,同學真的應該反思自己的學習態度和方法是否存在問題了。話不多說,直接上代碼,一探究竟,揭開this在全局范圍下的真面目:

          console.log(this); // Window
          

          不出意外,this在全局范圍內指向window對象()。通常, 在全局環境中, 我們很少使用this關鍵字, 因此對它也沒那么在意. 讓我們繼續看下一個環境.

          2.2 this在對象的構造函數內

          當我們使用new創建構造函數的實例時會發生什么呢?以這種方式調用構造函數會經歷以下四個步驟:

          • 創建一個空對象;
          • 將構造函數的作用域賦給新對象(this指向了這個新對象),繼承函數的原型;
          • 執行構造函數中的代碼;
          • 返回新對象。

          看完上面的內容,大家想必也知道this在對象的構造函數內的指向了吧!當你使用new關鍵字創建一個對象的新的實例時, this關鍵字指向這個實例 .

          舉個栗子:

          function Human (age) {
              this.age=age;
          }
          let greg=new Human(22);
          let thomas=new Human(24);
          
          console.log(greg); // this.age=22
          console.log(thomas); // this.age=24
          
          // answer
          Person { age:22}
          Person { age:24}
          

          2.3 this在對象方法內

          方法是與對象關聯的函數的通俗叫法, 如下所示:

          let o={
              sayThis(){
                  console.log(this);
              }
          }
          

          如上所示,在對象的任何方法內的this都是指向對象本身 .

          好了,繼續下一個環境!

          2.4 this在簡單函數內

          可能看到這里,許多同學心里會有疑問,什么是簡單函數?

          其實簡單函數大家都很熟悉,就像下面一樣,以相同形式編寫的匿名函數也被認為是簡單函數(非箭頭函數)。

          function hello(){
              console.log("hello"+this);
          }
          

          這里需要注意,在瀏覽器中,不管函數聲明在哪里,匿名或者不匿名,只要不是直接作為對象的方法,this指向始終是window對象(除非使用call,apply,bind修改this指向)。

          舉個栗子說明一下:

          // 顯示函數,直接定義在sayThis方法內,this指向依舊不變
          function simpleFunction() {
              console.log(this);
          }
          
          var o={
              sayThis() {
                  simpleFunction();
              }
          }
          
          simpleFunction(); // Window
          o.sayThis(); // Window
          
          
          // 匿名函數
          var o={
              sayThis(){
                  (function(){consoloe.log(this);})();
              }
          } 
          o.sayThis();// Window
          

          對于初學者來說,this在簡單函數內的表現時常讓他們懵逼不已,難道this不應該指向對象本身?這個問題曾經也出現在我的腦海里過,沒錯,在寫代碼時我也踩過這個坑。

          通常的,當我們要在對象方法內調用函數,而這個函數需要用到this時,我們都會創建一個變量來保存對象中的this的引用. 通常, 這個變量名稱叫做self或者that。具體說下所示:

          const o={
              doSomethingLater() {
                  const self=this;
                  setTimeout(function() {
                      self.speakLeet();
                  }, 1000);
              },
              speakLeet() {
                  console.log(`1337 15 4W350M3`);
              }
          }
          
          o.doSomethingLater(); // `1337 15 4W350M3`
          

          心細的同學可能已經發現,這里的簡單函數沒有將箭頭函數包括在內,那么下一個環境是什么想必也能猜到啦,那么現在進入下一個環境,看看this指向什么。

          2.5 this在箭頭函數內

          和簡單函數表現不太一樣,this在箭頭函數中總是跟它在箭頭函數所在作用域的this一樣(在它直接作用域). 所以, 如果你在對象中使用箭頭函數, 箭頭函數中的this總是指向這個對象本身, 而不是指向Window.

          下面我們使用箭頭函數,重寫一下上面的案例:

          const o={
              doSomethingLater() {
                  setTimeout(()=> this.speakLeet(), 1000);
              },
              speakLeet() {
                  console.log(`1337 15 4W350M3`);
              }
          }
          o.doSomethingLater(); // `1337 15 4W350M3`
          

          最后,讓我們來看看最后一種環境 - 事件偵聽器.

          2.6 this在事件偵聽器內

          在事件偵聽器內, this被綁定的是觸發這個事件的元素:

          let button=document.querySelector('button');
          
          button.addEventListener('click', function() {
              console.log(this); // button
          });
          

          3. this綁定規則

          事實上,只要記住上面this在不同環境的綁定值,足以應付大部分工作。然而,好學的同學總是會忍不住想說,為什么呢?對,為什么this在這些情況下綁定這些值呢?學習,我們不能只知其然,而不知所以然。所以,現在就讓我們來探尋,this值獲取的真相吧。

          現在,讓我們回憶一下,在講什么是this的時候,我們說到“this的綁定取決于他所處的環境”。這句話其實不是十分準確,準確的說,this不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件this綁定和函數聲明的位置無關,反而和函數被調用的方式有關

          當一個函數被調用時,會建立一個活動記錄,也稱為執行環境。這個記錄包含函數是從何處(call-stack)被調用的,函數是 如何被調用的,被傳遞了什么參數等信息。這個記錄的屬性之一,就是在函數執行期間將被使用的this引用。this實際上是在函數被調用時建立的一個綁定,它指向什么是完全由函數被調用的調用點來決定的

          僅僅是規則

          現在我們將注意力轉移到調用點 如何 決定在函數執行期間this指向哪里。

          你必須考察call-site并判定4種規則中的哪一個適用。我們將首先獨立的解釋一下這4種規則中的每一種,之后我們來展示一下如果有多種規則可以適用調用點時,它們的優先級。

          3.1 默認綁定規則

          第一種規則來源于函數調用的最常見的情況:獨立函數調用。可以認為這種this規則是在沒有其他規則適用時的默認規則。我們給它一個稱呼“默認綁定”.

          現在來看這段代碼:

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

          當foo()被調用時,this.a解析為我們的全局變量a。為什么?因為在這種情況下,對此方法調用的this實施了 默認綁定,所以使this指向了全局對象。

          在我們的代碼段中,foo()是被一個直白的,毫無修飾的函數引用調用的。沒有其他的我們將要展示的規則適用于這里,所以 默認綁定 在這里適用。

          如果strict mode在這里生效,那么對于 默認綁定 來說全局對象是不合法的,所以this將被設置為undefined。

          'use strict'
          function foo(){
              console.log(this.a); // TypeError: Cannot read property 'a' of undefined
          }
          const a=1;
          foo();
          
          function foo(){
          	'use strict'
              console.log(this.a); // TypeError: Cannot read property 'a' of undefined
          }
          const a=1;
          foo();
          

          微妙的是,即便所有的this綁定規則都是完全基于調用點,如果foo()的 內容 沒有在strint mode下執行,對于 默認綁定 來說全局對象是 唯一 合法的;foo()的call-site的strict mode狀態與此無關。

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

          注意: 在代碼中故意混用strict mode和非strict mode通常是讓人皺眉頭的。你的程序整體可能應當不是 Strict 就是非Strict。然而,有時你可能會引用與你的 Strict 模式不同的第三方包,所以對這些微妙的兼容性細節要多加小心。

          3.2 隱式綁定

          另一種要考慮的規則是:調用點是否有一個環境對象(context object),也稱為擁有者(owning)或容器(containing)對象。

          讓我們來看這段代碼:

          function foo() {
              console.log(this.a);
          }
          let o={
              a: 2,
              foo,
          }
          o.foo(); // 2
          

          這里,我們注意到foo函數被聲明然后作為對象o的方法,無論foo()是否一開始就在obj上被聲明,還是后來作為引用添加(如上面代碼所示),都是這個 函數 被obj所“擁有”或“包含”。這里,調用點使用obj環境來引用函數,所以可以說 obj對象在函數被調用的時間點上“擁有”或“包含”這個 函數引用。

          當一個方法引用存在一個環境對象時,隱式綁定 規則會說:是這個對象應當被用于這個函數調用的this綁定。

          只有對象屬性引用鏈的最后一層是影響調用點的。比如:

          function foo(){
              console.log(this.a);
          }
          
          var obj1={
              a:2,
              obj2:obj2
          };
          var obj2={
              a:42,
              foo:foo
          };
          obj1.obj2.foo(); // 42
          

          隱式綁定的隱患

          當一個 隱含綁定丟失了它的綁定,這通常意味著它會退回到 默認綁定, 根據strict mode的狀態,結果不是全局對象就是undefined。

          下面來看這段代碼:

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

          盡管bar似乎是obj.foo的引用,但實際上它只是另一個foo自己的引用而已。另外,起作用的調用點是bar(),一個直白,毫無修飾的調用,因此 默認綁定 適用于這里。

          這種情況發生的更加微妙,更常見,更意外的方式,是當我們考慮傳遞一個回調函數時:

          function foo(){
              console.log(this.a);
          }
          
          function doFoo(fn){
          	fn();
          }
          
          var obj={
              a:2,
              foo,
          };
          var a="Global variable";
          dooFoo(obj.foo); // "Global variable"
          

          參數傳遞僅僅是一種隱含的賦值,而且因為我們在傳遞一個函數,它是一個隱含的引用賦值,所以最終結果和我們前一個代碼段一樣。同樣的,語言內建,如setTimeout也一樣,如下所示

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

          把這個粗糙的setTimeout()假想實現當做JavaScript環境內建的實現的話:

          function setTimeout(fn, delay){
              // 等待delay毫秒
              fn();
          }
          

          正如我們看到的, 隱含綁定丟失了它的綁定是十分常見的,不管哪一種意外改變this的方式,你都不能真正地控制你的回調函數引用將如何被執行,所以你(還)沒有辦法控制調用點給你一個故意的綁定。但是我們可以使用顯示綁定強行固定this。

          3.3 顯示綁定

          我們看到隱含綁定,需要我們不得不改變目標對象使它自身包含一個對函數的引用,而后使用這個函數引用屬性來間接地(隱含地)將this綁定到這個對象上。

          但是,如果你想強制一個函數調用使用某個特定對象作為this綁定,而不在這個對象上放置一個函數引用屬性呢?

          js有提供call()、apply()方法,ES5中也提供了內置的方法 Function.prototype.bind,可以引用一個對象時進行強制綁定調用。

          考慮這段代碼:

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

          通過foo.call(..)使用 明確綁定 來調用foo,允許我們強制函數的this指向obj。

          如果你傳遞一個簡單原始類型值(string,boolean,或 number類型)作為this綁定,那么這個原始類型值會被包裝在它的對象類型中(分別是new String(..),new Boolean(..),或new Number(..))。這通常稱為“boxing(封箱)”。

          注意: 就this綁定的角度講,call(..)和apply(..)是完全一樣的。它們確實在處理其他參數上的方式不同,但那不是我們當前關心的。

          單獨依靠call和apply,仍然可能出現函數“丟失”自己原本的this綁定,或者被第三方覆蓋等問題。

          但有一個技巧可以避免出現這些問題

          考慮這段代碼:

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

          我們創建了一個函數bar(),在它的內部手動調用foo.call(obj),由此強制this綁定到obj并調用foo。無論你過后怎樣調用函數bar,它總是手動使用obj調用foo。這種綁定即明確又堅定,該方法被開發者稱為 硬綁定(顯示綁定的變種)(hard binding)

          用硬綁定將一個函數包裝起來的最典型的方法,是為所有傳入的參數和傳出的返回值創建一個通道:

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

          另一種表達這種模式的方法是創建一個可復用的幫助函數:

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

          由于 硬綁定 是一個如此常用的模式,它已作為ES5的內建工具提供,即前文提到的Function.prototype.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();
          cobsole.log(b); // 5
          
          

          bind(..)返回一個硬編碼的新函數,它使用你指定的this環境來調用原本的函數。

          注意: 在ES6中,bind(..)生成的硬綁定函數有一個名為.name的屬性,它源自于原始的 目標函數(target function)。舉例來說:bar=foo.bind(..)應該會有一個bar.name屬性,它的值為"bound foo",這個值應當會顯示在調用棧軌跡的函數調用名稱中。

          3.4new 綁定

          第四種也是最后一種this綁定規則

          當在函數前面被加入new調用時,也就是構造器調用時,下面這些事情會自動完成:

          • 一個全新的對象會憑空創建(就是被構建)
          • 這個新構建的對象會被接入原形鏈([[Prototype]]-linked)
          • 這個新構建的對象被設置為函數調用的this綁定
          • 除非函數返回一個它自己的其他 對象,這個被new調用的函數將 自動 返回這個新構建的對象。

          考慮這段代碼:

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

          通過在前面使用new來調用foo(..),我們構建了一個新的對象并這個新對象作為foo(..)調用的this。 new是函數調用可以綁定this的最后一種方式,我們稱之為 new綁定(new binding)。

          3.5 優先級

          • new綁定
          • 顯示綁定
          • 隱式綁定
          • 默認綁定(嚴格模式下會綁定到undefined)

          4. 箭頭函數

          箭頭函數并非使用function關鍵字進行定義,而是通過所謂的“大箭頭”操作符:=>,所以不會使用上面所講解的this四種標準規范,箭頭函數從封閉它的(function或global)作用域采用this綁定,即箭頭函數會繼承自外層函數調用的this綁定。

          執行 fruit.call(apple)時,箭頭函數this已被綁定,無法再次被修改。

          function fruit(){
              return ()=> {
                  console.log(this.name);
              }
          }
          var apple={
              name: '蘋果'
          }
          var banana={
              name: '香蕉'
          }
          var fruitCall=fruit.call(apple);
          fruitCall.call(banana); // 蘋果
          

          5. 小結

          this是JavaScript的一個關鍵字,this不是編寫時綁定,而是運行時綁定。它依賴于函數調用的上下文條件。this綁定和函數聲明的位置無關,反而和函數被調用的方式有關。為執行中的函數判定this綁定需要找到這個函數的直接調用點。找到之后,4種規則將會以 這個 優先順序施用于調用點:

          • 被new調用?使用新構建的對象。
          • 被call或apply(或 bind)調用?使用指定的對象。
          • 被持有調用的環境對象調用?使用那個環境對象。
          • 默認:strict mode下是undefined,否則就是全局對

          與這4種綁定規則不同,ES6的箭頭方法使用詞法作用域來決定this綁定,這意味著它們采用封閉他們的函數調用作為this綁定(無論它是什么)。它們實質上是ES6之前的self=this代碼的語法替代品。

          入淺出 JavaScript 中的 this

          在 Java 等面向對象的語言中,this 關鍵字的含義是明確且具體的,即指代當前對象。一般在編譯期確定下來,或稱為編譯期綁定。而在 JavaScript 中,this 是動態綁定,或稱為運行期綁定的,這就導致 JavaScript 中的 this 關鍵字有能力具備多重含義,帶來靈活性的同時,也為初學者帶來不少困惑。本文僅就這一問題展開討論,閱罷本文,讀者若能正確回答 JavaScript 中的 What 's this 問題,那就會覺得花費這么多功夫,撰寫這樣一篇文章是值得的。

          1.Java 語言中的 this

          在 Java 中定義類經常會使用 this 關鍵字,多數情況下是為了避免命名沖突,比如在下面例子的中,定義一個 Point 類,很自然的,大家會使用 x,y 為其屬性或成員變量命名,在構造函數中,使用 x,y 為參數命名,相比其他的名字,比如 a,b,也更有意義。這時候就需要使用 this 來避免命名上的沖突。另一種情況是為了方便的調用其他構造函數,比如定義在 x 軸上的點,其 x 值默認為 0,使用時只要提供 y 值就可以了,我們可以為此定義一個只需傳入一個參數的構造函數。無論哪種情況,this 的含義是一樣的,均指當前對象。

          清單 1. Point.java

          2.JavaScript 語言中的 this

          由于其運行期綁定的特性,JavaScript 中的 this 含義要豐富得多,它可以是全局對象、當前對象或者任意對象,這完全取決于函數的調用方式。JavaScript 中函數的調用有以下幾種方式:作為對象方法調用,作為函數調用,作為構造函數調用,和使用 apply 或 call 調用。下面我們將按照調用方式的不同,分別討論 this 的含義。

          2.1作為對象方法調用

          在 JavaScript 中,函數也是對象,因此函數可以作為一個對象的屬性,此時該函數被稱為該對象的方法,在使用這種調用方式時,this 被自然綁定到該對象。

          清單 2. point.js

          2.2作為函數調用

          函數也可以直接被調用,此時 this 綁定到全局對象。在瀏覽器中,window 就是該全局對象。比如下面的例子:函數被調用時,this 被綁定到全局對象,接下來執行賦值語句,相當于隱式的聲明了一個全局變量,這顯然不是調用者希望的。

          清單 3. nonsense.js

          對于內部函數,即聲明在另外一個函數體內的函數,這種綁定到全局對象的方式會產生另外一個問題。我們仍然以前面提到的 point 對象為例,這次我們希望在 moveTo 方法內定義兩個函數,分別將 x,y 坐標進行平移。結果可能出乎大家意料,不僅 point 對象沒有移動,反而多出兩個全局變量 x,y。

          清單 4. point.js

          這屬于 JavaScript 的設計缺陷,正確的設計方式是內部函數的 this 應該綁定到其外層函數對應的對象上,為了規避這一設計缺陷,聰明的 JavaScript 程序員想出了變量替代的方法,約定俗成,該變量一般被命名為 that。

          清單 5. point2.js

          2.3作為構造函數調用

          JavaScript 支持面向對象式編程,與主流的面向對象式編程語言不同,JavaScript 并沒有類(class)的概念,而是使用基于原型(prototype)的繼承方式。相應的,JavaScript 中的構造函數也很特殊,如果不使用 new 調用,則和普通函數一樣。作為又一項約定俗成的準則,構造函數以大寫字母開頭,提醒調用者使用正確的方式調用。如果調用正確,this 綁定到新創建的對象上。

          清單 6. Point.js

          2.4使用 apply 或 call 調用

          讓我們再一次重申,在 JavaScript 中函數也是對象,對象則有方法,apply 和 call 就是函數對象的方法。這兩個方法異常強大,他們允許切換函數執行的上下文環境(context),即 this 綁定的對象。很多 JavaScript 中的技巧以及類庫都用到了該方法。讓我們看一個具體的例子:

          清單 7. Point2.js

          在上面的例子中,我們使用構造函數生成了一個對象 p1,該對象同時具有 moveTo 方法;使用對象字面量創建了另一個對象 p2,我們看到使用 apply 可以將 p1 的方法應用到 p2 上,這時候 this 也被綁定到對象 p2 上。另一個方法 call 也具備同樣功能,不同的是最后的參數不是作為一個數組統一傳入,而是分開傳入的。

          2.5換個角度理解

          如果像作者一樣,大家也覺得上述四種方式不方便記憶,過一段時間后,又搞不明白 this 究竟指什么。那么我向大家推薦 Yehuda Katz 的這篇文章:( http://yehudakatz.com/2011/08/11/understanding-javascript-function-invocation-and-this/)。在這篇文章里,Yehuda Katz 將 apply 或 call 方式作為函數調用的基本方式,其他幾種方式都是在這一基礎上的演變,或稱之為語法糖。Yehuda Katz 強調了函數調用時 this 綁定的過程,不管函數以何種方式調用,均需完成這一綁定過程,不同的是,作為函數調用時,this 綁定到全局對象;作為方法調用時,this 綁定到該方法所屬的對象。

          2.6說完了沒?

          通過上面的描述,如果大家已經能明確區分各種情況下 this 的含義,這篇文章的目標就已經完成了。如果大家的好奇心再強一點,想知道為什么 this 在 JavaScript 中的含義如此豐富,那就得繼續閱讀下面的內容了。作者需要提前告知大家,下面的內容會比前面稍顯枯燥,如果只想明白 this 的含義,閱讀到此已經足夠了。如果大家不嫌枯燥,非要探尋其中究竟,那就一起邁入下一節吧。

          3.函數的執行環境

          JavaScript 中的函數既可以被當作普通函數執行,也可以作為對象的方法執行,這是導致 this 含義如此豐富的主要原因。一個函數被執行時,會創建一個執行環境(ExecutionContext),函數的所有的行為均發生在此執行環境中,構建該執行環境時,JavaScript 首先會創建 arguments變量,其中包含調用函數時傳入的參數。接下來創建作用域鏈。然后初始化變量,首先初始化函數的形參表,值為 arguments變量中對應的值,如果 arguments變量中沒有對應值,則該形參初始化為 undefined。如果該函數中含有內部函數,則初始化這些內部函數。如果沒有,繼續初始化該函數內定義的局部變量,需要注意的是此時這些變量初始化為 undefined,其賦值操作在執行環境(ExecutionContext)創建成功后,函數執行時才會執行,這點對于我們理解 JavaScript 中的變量作用域非常重要,鑒于篇幅,我們先不在這里討論這個話題。最后為 this變量賦值,如前所述,會根據函數調用方式的不同,賦給 this全局對象,當前對象等。至此函數的執行環境(ExecutionContext)創建成功,函數開始逐行執行,所需變量均從之前構建好的執行環境(ExecutionContext)中讀取。

          3.1 Function.bind

          有了前面對于函數執行環境的描述,我們來看看 this 在 JavaScript 中經常被誤用的一種情況:回調函數。JavaScript 支持函數式編程,函數屬于一級對象,可以作為參數被傳遞。請看下面的例子 myObject.handler 作為回調函數,會在 onclick 事件被觸發時調用,但此時,該函數已經在另外一個執行環境(ExecutionContext)中執行了,this 自然也不會綁定到 myObject 對象上。

          清單 8. callback.js

          button.onclick=myObject.handler;

          這是 JavaScript 新手們經常犯的一個錯誤,為了避免這種錯誤,許多 JavaScript 框架都提供了手動綁定 this 的方法。比如 Dojo 就提供了 lang.hitch,該方法接受一個對象和函數作為參數,返回一個新函數,執行時 this 綁定到傳入的對象上。使用 Dojo,可以將上面的例子改為:

          清單 9. Callback2.js

          button.onclick=lang.hitch(myObject, myObject.handler);

          在新版的 JavaScript 中,已經提供了內置的 bind 方法供大家使用。

          3.2 eval 方法

          JavaScript 中的 eval 方法可以將字符串轉換為 JavaScript 代碼,使用 eval 方法時,this 指向哪里呢?答案很簡單,看誰在調用 eval 方法,調用者的執行環境(ExecutionContext)中的 this 就被 eval 方法繼承下來了。簡單看個eval示例:

          4.結語

          本文介紹了 JavaScript 中的 this 關鍵字在各種情況下的含義,雖然這只是 JavaScript 中一個很小的概念,但借此我們可以深入了解 JavaScript 中函數的執行環境,而這是理解閉包等其他概念的基礎。掌握了這些概念,才能充分發揮 JavaScript 的特點,才會發現 JavaScript 語言特性的強大。


          主站蜘蛛池模板: 69福利视频一区二区| 亚洲日韩精品无码一区二区三区| AV天堂午夜精品一区二区三区| 国产成人精品视频一区| 久久国产精品一区二区| 91亚洲一区二区在线观看不卡| 精品视频一区在线观看| 久久精品国产免费一区| 无码人妻精品一区二| 91精品乱码一区二区三区| 一区二区三区在线观看中文字幕| 亚洲国产美女福利直播秀一区二区| 福利一区福利二区| 蜜芽亚洲av无码一区二区三区| 亚洲午夜精品第一区二区8050| 无码精品视频一区二区三区| 久久久人妻精品无码一区| 国产在线精品一区二区三区直播| 日本在线一区二区| 精品无码av一区二区三区| 亚洲综合一区二区精品久久| 国产一区三区二区中文在线| 国产成人一区二区三区免费视频| 午夜福利一区二区三区高清视频| 琪琪see色原网一区二区| 国产精品一区二区久久| 久久久久人妻一区精品色| 久久99精品波多结衣一区| 亚洲av成人一区二区三区在线观看| 亚洲av无码一区二区乱子伦as| 合区精品久久久中文字幕一区 | 国产一区中文字幕在线观看 | 骚片AV蜜桃精品一区| 国产免费私拍一区二区三区| 麻豆AV一区二区三区久久| 亚洲视频免费一区| 国产免费一区二区三区| 国产乱码精品一区二区三区四川人| 精品少妇人妻AV一区二区三区| 亚洲综合在线一区二区三区| 成人一区专区在线观看 |