javaScript系列 [01]-javaScript函式基礎這篇文章中我已經簡單介紹了JavaScript語言在函式使用中this的指向問題,雖然篇幅不長,但其實最重要的部分已經講清楚了,這篇文章我們來單獨談一談神祕的this,或者叫怎麼也搞不清楚的指天指地指空氣的this

1.1 this簡單說明

this關鍵字被認為是JavaScript語言中最複雜的機制之一,跟this相關的知識很多開發者往往總是一知半解,更有甚者很多人完全搞不懂也不願意去搞懂跟this相關的內容,在必須要用到的時候寧願選擇在程式碼中總是使用臨時列印驗證的方式來探知this的指向。這是現實,也許因為他們覺得跟this有關的這一切都混亂不堪,各種文件晦澀難懂,this的指向好似沒有固定的套路,總是變來變去難以捉摸。其實,this原本並沒有那麼複雜,它就是個被自動定義在函式作用域中的變數,總是指向某個特定的“物件”。接下來,我們將嘗試用這樣一篇文章來講清楚跟this有關的以下問題:

this 是什麼?
為什麼要使用this?
this指向誰?
this繫結的幾種情況
this固定規則外的注意事項

   this是什麼?   

在宣告函式的時候,除了宣告時定義的形式引數外,每個函式還接受兩個附加的引數:thisarguments。其中arguments是一個類似於陣列的結構,儲存了函式呼叫時傳遞的所有實際引數,arguments這個引數讓我們有能力編寫能夠接受任意個數引數的函式。引數this在物件導向程式設計中非常重要,它總是指向一個“特定的物件”,至於這個特定的物件是誰通常取決於函式的呼叫模式。

 1 <script>
 2 console.log(this); //預設指向window
 3  
 4 function sum() {
 5 var res = 0;
 6 for (var i = 0; i < arguments.length; i++) {
 7 res += arguments[i];
 8 }
 9 console.log(this);
10 return res;
11 }
12  
13 //呼叫sum函式的時候,this預設指向window
14 console.log(sum(1, 2, 3, 4)); //計算輸入引數的累加和,結果為10
15 </script>

① this是JavaScript中所有函式的隱藏引數之一,因此每個函式中都能訪問this。現在我們知道和this有關的關鍵資訊是:

② 函式中的this總是指向一個特定物件,該物件具體取決於函式的呼叫模式。

說明:在script標籤中我們也可以直接訪問this,它通常總是指向widow,我們討論的this主要特指函式內部(函式體)的this。

   為什麼要使用this?   

this提供一種更優雅的方式來隱士的傳遞一個物件引用,因為擁有this,所以我們可以把API設計得更加的簡潔並且易於複用。簡單點說,那就是this可以幫助我們省略引數。

我們可以通過以下兩個程式碼片段來加深對this使用的理解。

 1 /**程式碼 [ 01 ]**/
 2 var personOne = {name:"文頂頂",contentText:"天王蓋地虎 小雞燉蘑菇"};
 3 var personTwo = {name:"燕赤霞",contentText:"天地無極 乾坤借法 急急如令令"};
 4  
 5 function speak(obj) {
 6 console.log(obj.name+"口訣是:" + getContentText(obj));;
 7 }
 8  
 9 function getContentText(obj) {
10 return obj.contentText + "噠噠噠噠~";
11 }
12  
13 speak(personOne); //文頂頂口訣是:天王蓋地虎 小雞燉蘑菇噠噠噠噠~
14 speak(personTwo); //燕赤霞口訣是:天地無極 乾坤借法 急急如令令噠噠噠噠~
15  
16 getContentText(personOne);
17 getContentText(personTwo);

程式碼說明:上面的程式碼宣告瞭兩個函式:speak和getContentText,這兩個函式都需要訪問物件中的屬性,上面的程式碼中每個函式都接收一個obj物件作為引數。

 1 /**程式碼 [ 02 ]**/
 2 var personOne = {name:"文頂頂",contentText:"天王蓋地虎 小雞燉蘑菇"};
 3 var personTwo = {name:"燕赤霞",contentText:"天地無極 乾坤借法 急急如令令"};
 4  
 5 function speak() {
 6 console.log(this.name+"口訣是:" + getContentText.call(this));;
 7 }
 8  
 9 function getContentText() {
10 return this.contentText + "噠噠噠噠~";
11 }
12  
13 speak.call(personOne); //文頂頂口訣是:天王蓋地虎 小雞燉蘑菇噠噠噠噠~
14 speak.call(personTwo); //燕赤霞口訣是:天地無極 乾坤借法 急急如令令噠噠噠噠~
15  
16 getContentText.call(personOne); //天王蓋地虎 小雞燉蘑菇噠噠噠噠~
17 getContentText.call(personTwo); //天地無極 乾坤借法 急急如令令噠噠噠噠~

1.2 函式和this

程式碼說明:完成相同的功能,還是兩個同樣的函式,區別在於我們藉助this省略掉了函式必須要傳遞的物件引數,實現更優雅。而且如果你的程式碼越來越複雜,那麼需要顯式傳遞的上下文物件會讓程式碼變得越來越混亂而難以維護,使用this則不會如此。

this指向誰繫結給哪個物件並不是在編寫程式碼的時候決定的,而是在執行時進行繫結的,它的上下文取決於函式呼叫時的各種條件 。this的繫結和函式宣告的位置沒有任何關係,只取決於函式的呼叫方式。

當函式被呼叫時,會建立一個執行上下文。該上下文會包含一些特殊的資訊,例如函式在哪裡被呼叫,函式的呼叫方式,函式的引數等,this其實是該上下文中的一個屬性,它指向誰完全取決於函式的呼叫方式。

現在我們已經弄明白了this最核心的知識:this的指向取決於函式的呼叫方式。

函式基礎   

在接著講解之前,有必要對函式的情況進行簡單說明,比如函式的建立、引數的傳遞、函式的呼叫以及返回值等等。

函式的建立
在開發中我們有多種方式來建立(宣告)函式,可以使用function關鍵字直接宣告一個具名函式或者是匿名函式,也可以使用Function建構函式來建立一個函式例項物件。

 1 //01 function關鍵字宣告函式
 2 function f1() {
 3 console.log("命名函式|具名函式");
 4 }
 5  
 6 var f2 = function () {
 7 console.log("匿名函式");
 8 }
 9  
10 //02 Function建構函式建立函式例項物件
11 var f3 = new Function(`console.log("函式例項物件的函式體")`);

函式的引數 

函式的引數有兩種,一種是形式引數,一種是實際引數。

形式引數
在函式宣告(建立)的時候,我們可以通過一定的方式來指定函式的引數,相當於在函式體內宣告瞭對應的臨時區域性變數。

實際引數
在函式呼叫的時候,會把實際引數的值傳遞給形式引數,存在一個隱藏的賦值操作,實際引數就是函式呼叫時()中的引數。

隱藏引數
JavaScript中所有函式中均可以使用this和arguments這兩個附加的隱藏引數。

 1 //[1] 函式的宣告
 2 //01 function關鍵字宣告函式
 3 function f1(a,b) {
 4 //a和b為函式的形式引數,相當於在此處寫上程式碼 var a,b;
 5 console.log("命名函式|具名函式","a的值:" +a , "b的值:"+b);
 6 console.log(this); //此處指向window全域性物件
 7 console.log(arguments); //此處列印的是["f1的a","f1的b"]結構的資料
 8 }
 9  
10 var f2 = function (a,b) {
11 //a和b為函式的形式引數,相當於在此處寫上程式碼 var a,b;
12 console.log("匿名函式","a的值:" +a , "b的值:"+b);
13 }
14  
15 //02 Function建構函式建立函式例項物件
16 //a和b為新建立的函式物件的形式引數
17 var f3 = new Function(`a`,`b`,`console.log("函式例項物件的函式體","a的值:" +a , "b的值:"+b)`);
18  
19  
20 //[2] 函式的呼叫
21  
22 //"f1的a"和"f1的b"這兩個字串作為f1函式此處呼叫傳遞的實際引數
23 //在呼叫函式的時候,會把"f1的a"這個字串賦值給形參a,把"f1的b"這個字串賦值給形參b
24 f1("f1的a","f1的b"); //命名函式|具名函式 a的值:f1的a b的值:f1的b
25  
26 f2("f2的a","f3的b"); //匿名函式 a的值:f2的a b的值:f3的b
27 f3("f3的a","f3的b"); //函式例項物件的函式體 a的值:f3的a b的值:f3的b

函式呼叫和this繫結   函式呼叫

函式名後面跟上呼叫運算子[()]的程式碼,我們稱為函式呼叫,當函式被呼叫的時候,會把實參賦值給形參並自上而下的執行函式體中的程式碼。

因為this的繫結完全取決於函式的呼叫方式,所以要搞清楚this繫結問題只需要搞清楚函式呼叫方式即可,函式的呼叫方式通常來說有以下四種:

 普通函式呼叫(預設繫結)
 物件方法呼叫(隱式繫結)
 建構函式呼叫(new繫結)
 函式上下文呼叫(顯式繫結)

函式的呼叫方式只有上面的四種情況,而要確定其具體的呼叫方式,需要先確定函式呼叫的位置。

函式呼叫位置
函式呼叫位置也就是函式在程式碼中被呼叫的位置[函式名+()的形式],我們可以通過下面的示例程式碼來理解函式的呼叫位置。

 1 function f1() {
 2 console.log("f1");
 3 //當前的函式呼叫棧:f1
 4 f2(); //函式f2呼叫的位置
 5 }
 6  
 7 function f2() {
 8 console.log("f2");
 9 //當前函式呼叫棧:f1 --> f2
10 f3(); //函式f3呼叫的位置
11 }
12  
13 function f3() {
14 //當前函式呼叫棧:f1-->f2-->f3
15 console.log("f3");
16 }
17 f1(); //函式f1呼叫的位置

 

1.3 this繫結淺析 

   

① 普通函式呼叫(預設繫結)    

普通函式呼叫就是函式名後面直接更上呼叫運算子呼叫,這種情況下函式呼叫時應用了this的預設繫結,如果是在非嚴格模式下,該this指向全域性物件window,如果是在嚴格模式下,不能將全域性物件用於預設繫結,該this會繫結到undefined。

 1 //宣告全域性變數 t
 2 var t = 123; //所有全域性變數自動成為全域性物件的屬性
 3 function foo() {
 4 console.log("foo"); //foo
 5 console.log(this); //this ---> 全域性物件window
 6 console.log(this.t);//123
 7 }
 8  
 9  
10 foo(); //非嚴格模式下:以普通函式方式呼叫
11  
12 function fn() {
13 "use strict"; //作用域開啟嚴格模式
14 console.log("fn"); //fn
15 console.log(this); //this --->undefined
16 //Uncaught TypeError: Cannot read property `t` of undefined
17 console.log(this.t);
18 }
19  
20 fn(); //嚴格模式下:以普通函式方式呼叫

   ② 物件方法呼叫(隱式繫結)    

物件方法呼叫又稱為隱式繫結,當函式引用有上下文物件的時候,隱式繫結規則會把函式呼叫中的this繫結到這個上下文物件。需要注意的是,如果存在引用鏈,那麼只有物件屬性引用鏈中的最後一層在呼叫位置中起作用,下面我們通過一個程式碼片段來理解這種呼叫方式。

 1 var name = "wenidngding";
 2 function showName() {
 3 console.log(this.name);
 4 }
 5  
 6 //普通函式呼叫,函式中的this預設繫結到全域性物件,列印wendingding
 7 showName();
 8  
 9 var obj = {
10 name:"小豬佩奇",
11 showName:showName
12 }
13  
14 //物件方法呼叫,函式中的this繫結到當前的上下文物件obj,列印小豬佩奇
15 obj.showName();

 

上下文物件 

上下文物件可以簡單理解為函式呼叫時該函式的擁有者,或者引用當前函式的物件。

this丟失的問題

我們在確定this繫結問題的時候不能一根筋的把該函式是否是物件的方法作為判斷的準則,而要抓住問題的本質,而且程式碼中可能存在this隱式繫結丟失的問題。外在的所有形式其實都不重要,最根本的就是看函式呼叫的時候,用的是什麼方式?

 1 //字面量方式建立物件,該物件擁有name屬性和showName方法
 2 var obj1 = {
 3 name:"小豬佩奇",
 4 showName:function () {
 5 console.log(this.name);
 6 }
 7 }
 8  
 9 //呼叫位置(001)
10 //物件方法呼叫,函式中的this繫結到當前的上下文物件obj1,列印小豬佩奇
11 obj1.showName();
12  
13 //[1] 把obj.showName方法賦值給其他的物件
14 var obj2 = {name:"阿文"};
15 obj2.show = obj1.showName;
16  
17 //呼叫位置(002)
18 //物件方法呼叫,函式中的this繫結到當前的上下文物件obj2,列印阿文
19 obj2.show();
20  
21 //[2] 把obj.showName方法賦值給一個變數
22 var fn = obj1.showName;
23  
24 //呼叫位置(003)
25 //普通函式呼叫,函式中的this指向全域性物件,列印空字串(window.name屬性值是空字串)
26 //注意:函式呼叫方式發生了改變,this丟失了
27 fn();
28  
29 //[3] 把obj.showName方法作為其他函式的引數(回撥函式)來使用
30 //宣告函式,該函式接收一個函式作為引數
31 function foo(callBack) {
32 //呼叫位置(004)
33 //普通函式呼叫,函式中的this指向全域性物件,列印空字串(window.name屬性值是空字串)
34 //注意:函式呼叫方式發生了改變,this丟失了
35 callBack();
36 }
37 //呼叫位置(005) 此處不涉及this
38 foo(obj1.showName);


思考:能否縮短對DOM操作相關的方法?
 

1 console.log(document.getElementById("demoID")); //正確
2  
3 //宣告getById函式,該函式指向document.getElementById方法
4 var getById = document.getElementById;
5  
6 console.log(getById("demoID"));//報錯:Uncaught TypeError: Illegal invocation

程式碼說明 有的朋友可能嘗試過像上面這樣來寫程式碼,發現通過這樣簡單的處理想要縮短DOM操作相關方法的方式是不可取的,為什麼會報錯?原因在於document.getElementById方法內部的實現依賴於this,而上面的程式碼偷換了函式的呼叫方式,函式的呼叫方式由物件方法呼叫轉變成了普通函式呼叫,this繫結的物件由document變成了window。 

怎麼解決呢,可以嘗試使用顯式的繫結指定函式內的this,參考程式碼如下:

1 var getById = function () {
2 //顯式的設定document.getElementById函式內部的this繫結到document物件
3 return document.getElementById.apply(document,arguments)
4 };
5 console.log(getById("demoID")); //正確

   ③ 建構函式呼叫(new繫結)     

建構函式方式呼叫其實就是在呼叫函式的時候使用new關鍵字,這種呼叫方式主要用於建立指定建構函式對應的例項物件。

建構函式
建構函式就是普通的函式,本身和普通的函式沒有任何區別,其實建構函式應該被稱為以構造方式呼叫的函式,這樣也許會更準確一些。因為在呼叫的時候總是以new關鍵字開頭[例如:new Person() ],所以我們把像Person這樣的函式叫做建構函式。雖然建構函式和普通函式無異,但因為它們呼叫的直接目的完全不同,為了人為的區分它們,開發者總是約定建構函式的首字母大寫。

當函式被以普通方式呼叫的時候,會完成實參向形參的賦值操作,繼而自上而下的執行函式體中的程式碼,當建構函式被呼叫的時候,目的在於獲得對應的例項物件。

 1 //宣告一個Person函式
 2 function Perosn(name,age) {
 3 this.name = name;
 4 this.age = age;
 5 this.show = function () {
 6 console.log("姓名:" + this.name + " 年齡:" + this.age);
 7 }
 8 }
 9  
10 //函式呼叫位置(001)
11 //建構函式方式呼叫(new繫結) Person函式內部的this指向新建立的例項物件
12 var p1 = new Perosn("zs",18);
13  
14 //函式呼叫位置(002)
15 //物件方法的方式呼叫(隱式繫結) show方法內部的this指向的是引用的物件,也就是p1
16 //列印:姓名:zs 年齡:18
17 p1.show();

使用new以建構函式的方式來呼叫Person的時候,內部主要做以下操作建構函式內部細節

① 建立空的Object型別的例項物件,假設為物件o
② 讓函式內部的this指向新建立的例項物件o
③ 設定例項物件o的原型物件指向建構函式預設關聯的原型物件
④ 在函式內通過this來新增屬性和方法
⑤ 在最後預設把新建立的例項物件返回

總結 如果以建構函式方式呼叫,函式內部的this繫結給新建立出來的例項物件。

   ④ 函式上下文呼叫(顯式繫結)    
在開發中我們可以通過call()或者是apply()方法來顯式的給函式繫結指定的this,使用call或者是apply方法這種呼叫方式我們稱為是函式上下文呼叫。

JavaScript語言中提供的絕大多數函式以及我們自己建立的所有函式都可以使用call和apply方法,這兩個方法的作用幾乎完全相同,只有傳參的方式有細微的差別。

call方法和apply方法的使用

作用:借用物件的方法並顯式繫結函式內的this。
語法:物件.方法.call(繫結的物件,引數1,引數2...) | 物件.方法.apply(繫結的物件,[引數1,引數2...])

使用程式碼示例

 1 var obj1 = {
 2 name:"zs",
 3 showName:function (a,b) {
 4 console.log("姓名 " + this.name,a, b);
 5 }
 6 };
 7  
 8 var obj2 = {name:"ls"};
 9  
10 //函式呼叫位置(001)
11 //以物件方法的方式呼叫函式,函式內部的this指向引用物件,也就是obj1
12 //列印結果為:姓名 zs 1 2
13 obj1.showName(1,2);
14  
15 //函式呼叫位置(002)
16 //obj2物件並不擁有showName方法,此處報錯:obj2.showName is not a function
17 //obj2.showName();
18  
19 //函式呼叫位置(003)
20 //函式上下文的方式(call)呼叫函式,函式內部的this繫結給第一個引數obj2
21 //列印結果為:姓名 ls 哈哈 嘿嘿
22 //第一個引數:obj2指定函式內this的繫結物件
23 //其它的引數:哈哈和嘿嘿這兩個字串是傳遞給showName函式的實參,呼叫時會賦值給函式的形參:a和b
24 obj1.showName.call(obj2,"哈哈","嘿嘿");
25  
26 //函式呼叫位置(004)
27 //函式上下文的方式(apply)呼叫函式,函式內部的this繫結給第一個引數obj2
28 //列印結果為:姓名 ls 呵呵 嘎嘎
29 //第一個引數:obj2指定函式內this的繫結物件
30 //其它的引數:呵呵和嘎嘎這兩個字串是傳遞給showName函式的實參,呼叫時會賦值給函式的形參:a和b
31 obj1.showName.apply(obj2,["呵呵","嘎嘎"]);

總結 如果以函式上下文的方式來呼叫,函式內部的this繫結call或者是apply方法的第一個引數,如果該引數不是物件型別那麼會自動轉換為對應的物件形式。 

1.4 this的注意事項

我們已經介紹了一般情況下this繫結的問題,雖然上面的規則可以適用絕大多數的程式碼場景,但也並非總是百分百如此,也有例外。

   例外的情況 ①    

在使用call或者apply方法的時候,非嚴格模式下如果我們傳遞的引數是null或者是undefined,那麼這些值在呼叫的時候其實會被忽略,this預設繫結的其實是全域性物件。

 1 /**[程式碼 01]**/
 2 //宣告全域性變數用於測試
 3 var name = "測試的name";
 4 var obj1 = {
 5 name:"zs",
 6 showName:function (a,b) {
 7 console.log("姓名 " + this.name,a, b);
 8 }
 9 };
10  
11 //注意:雖然此處以上下文的方式呼叫,但是因為傳遞的第一個引數是null,實際這裡應用的是預設繫結規則
12 obj1.showName.call(null,1,2); //姓名 測試的name 1 2
13 obj1.showName.call(undefined,1,2); //姓名 測試的name 1 2

嚴格模式下,傳遞null或者是undefined作為call和apply方法的第一個引數,this的繫結和上下文呼叫保持一致。

 1/**[程式碼 02]**/
2
//開啟嚴格模式 3 "use strict"; 4 5 //宣告全域性變數用於測試 6 var obj = { 7 name:"zs", 8 showName:function () { 9 console.log(this); 10 } 11 }; 12 13 obj.showName.call(null); //null 14 obj.showName.apply(undefined); //undefined 15 16 //建議的處理方式 17 obj.showName.apply(Object.create(null));

建議 以前我們在以函式上下文方式來呼叫函式的時候,如果並不關心函式內部的this繫結,那麼一般會傳遞null值或者undefined值。如果這樣的話,在非嚴格模式下,函式內部的this預設繫結給全域性物件並不安全,建議傳遞空物件[可以使用Object.create(null)方式建立],這樣函式操作會更安全而且程式碼可讀性會更好。 

   例外的情況 ②    

ES6中推出了一種特殊的函式型別:箭頭函式。箭頭函式使用=>操作符來定義,需要注意的是箭頭函式內部的this繫結並不適用於既定的四種規則,this的繫結由外層作用域來決定。

 1 //宣告函式
 2 function fn() {
 3 console.log("fn",this);
 4 //fn函式中返回一個箭頭函式
 5 return ()=>{
 6 console.log(this);
 7 }
 8 }
 9  
10 var o = {name:"zs"};
11 //fn以普通函式方式呼叫,fn中的this指向全域性物件
12 //箭頭函式中的this繫結由外部的詞法作用域來決定,this指向window
13 fn()();
14  
15 //fn以函式上下文方式呼叫,fn中的this指向物件o
16 //箭頭函式中的this繫結由外部的詞法作用域來決定,this指向物件o
17 fn.call(o)(); //this指向{name:"zs"}物件

   例外的情況 ③     

需要特別注意的是:在程式碼中我們可能會建立函式的“間接引用”,這種情況下呼叫函式會使用預設繫結規則。

 1 var objA = {
 2 name:"zs",
 3 showName:function(){
 4 console.log(this.name);
 5 }
 6 }
 7  
 8 var objB = {name:"ls"};
 9 objA.showName(); //物件方法呼叫,this指向objA 列印zs
10  
11 (objB.showName = objA.showName)(); //列印 空字串

程式碼說明 我們重點看最後一行程式碼,賦值表示式objB.showName = objA.showName的返回值是目標函式的引用,這種間接引用呼叫方式符合普通函式呼叫的規則,this會被繫結給全域性物件。最後一行程式碼,拆開來寫的形式: 

1 var f = objB.showName = objA.showName;
2 f(); //列印 空字串

1.5 this繫結總結 

當函式的呼叫位置確定後,我們可以順序應用下面的四條規則來判斷this的繫結物件

① 是否由new呼叫? 如果是,則繫結到建構函式新建立的例項物件身上。
② 是否由call或者apply呼叫?如果是,則繫結到第一個引數指定的物件身上。
③ 是有作為物件的方法呼叫?如果是,則繫結到這個引用的物件身上。
④ 預設普通函式呼叫,如果是嚴格模式則繫結到undefined,否則繫結到全域性物件。