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

1.1 this簡單說明

this關鍵字被認爲是JavaScript語言中最複雜的機制之一,跟this相關的知識不少開發者每每老是隻知其一;不知其二,更有甚者不少人徹底搞不懂也不肯意去搞懂跟this相關的內容,在必需要用到的時候寧願選擇在代碼中老是使用臨時打印驗證的方式來探知this的指向。這是現實,也許由於他們以爲跟this有關的這一切都混亂不堪,各類文檔晦澀難懂,this的指向好似沒有固定的套路,老是變來變去難以捉摸。其實,this本來並無那麼複雜,它就是個被自動定義在函數做用域中的變量,老是指向某個特定的「對象」。接下來,咱們將嘗試用這樣一篇文章來說清楚跟this有關的如下問題:編程

❐ this 是什麼?
❐ 爲何要使用this?
❐ this指向誰?
❐ this綁定的幾種狀況
❐ this固定規則外的注意事項數組

   this是什麼?   安全

在聲明函數的時候,除了聲明時定義的形式參數外,每一個函數還接受兩個附加的參數:thisarguments。其中arguments是一個相似於數組的結構,保存了函數調用時傳遞的全部實際參數,arguments這個參數讓咱們有能力編寫可以接受任意個數參數的函數。參數this在面向對象編程中很是重要,它老是指向一個「特定的對象」,至於這個特定的對象是誰一般取決於函數的調用模式。app

 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老是指向一個特定對象,該對象具體取決於函數的調用模式。post

說明:在script標籤中咱們也能夠直接訪問this,它一般老是指向widow,咱們討論的this主要特指函數內部(函數體)的this。

   爲何要使用this?   測試

this提供一種更優雅的方式來隱士的傳遞一個對象引用,由於擁有this,因此咱們能夠把API設計得更加的簡潔而且易於複用。簡單點說,那就是this能夠幫助咱們省略參數。this

咱們能夠經過如下兩個代碼片斷來加深對this使用的理解。spa

 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,不然綁定到全局對象。