js–this指向的相關問題

前言

  關於this的指向問題是前端面試中常考的知識點,也是我們開發學習中較難理解的問題。作為JavaScript的基礎,需要我們徹底理解這一關鍵詞。this作為JavaScript中非常複復雜的機制,值得我們付出更大的代價來學習理解。這裡分享一下我的學習筆記。

正文

  1.this是什麼?this指向什麼?

  學習this之前首先要知道this永遠指向一個對象,this就是函數運行時候所在的環境,我們在學習執行上下文的時候提到,每一個執行上下文都包含三個重要對象,分別是變數對象、作用域鏈和 this,JavaScript支援運行環境動態切換,也就是說,this的指向是動態的,並不是固定的指向一個對象,this的指向完全取絕於函數調用的位置。接下來我們看下下面的程式碼:

        var a=1
        function foo(){
            var a=3
            console.log(a)
        }
        foo()//3 

  上面的程式碼執行用到函數的作用域問題,foo()函數執行的時候,函數內部定義一個變數a等於3,然後輸出列印a,此時foo上下文中存在a變數,因此輸出3,若函數foo()內部沒有定義a這個變數,在執行列印這句程式碼的時候,函數內部找不到該變數,此時會沿著作用域鏈向上次查找,即全局作用域,此時列印結果便為1。

        var a=1
        function foo(){
            var a=3
            console.log(this.a)
        }
        foo()//1

  上面的程式碼執行結果會列印出1,對比第一段程式碼你會發現就這裡多了this這一個關鍵詞,為什麼加了this之後輸出結果會發生改變呢?莫非此時this指向window全局對象?

        var a=1
        function foo(){
            var a=3
            console.log(this.a)
        }
        var obj={a:100,foo:foo}
        obj.foo()//100

 

  再來看下上面的程式碼,列印結果變為100,對比前面兩段程式碼,foo函數增加了obj對象的一層引用,輸出的this.a結果再次發生改變?難道此時this指向obj對象?

    this的指向為什麼會發生改變,this的指向到底什麼時候發生的?是因為函數在JavaScript中既可以當作值傳遞和返回,也可以當作對象和構造函數。所有函數在運行的時候才能確定其當前的運行環境,所以this就產生了,this會根據函數的運行環境的改變而改變,同時,函數中的this也只能在函數運行時最終確定其運行環境。

  2.this不指向它自身。

    function foo(num) { 
        console.log( "foo: " + num ); // 記錄 foo 被調用的次數
        this.count++; 
    }
    foo.count = 0;
    for (var i=0; i<5; i++) {
            foo( i ); 
    }
    console.log("foo.count:"+foo.count)
    //foo: 0
    //foo: 1
    //foo: 2
    //foo: 3
    //foo: 4
    //foo.count:0

  上面的程式碼列印輸出foo:01234,說明foo函數被調用了五次,然後列印foo.count輸出還是0,說明this.count中的this並不是指向的函數foo本身,foo.count只是函數foo的一個屬性,this無法指向他本身。我們可以通過arguments.callee來引用當前正在執行的函數對象,即對象本身。

  3.this不指向它的作用域。

  另一種常見的誤解就是this指向函數的作用域。我們需要牢記,this在任何情況下都不指向函數的詞法作用域,this的指向完全取決於函數調用的位置。因此不能使用this來引用一個函數詞法作用域內部的東西。

  4.this的綁定規則。

  this是什麼?當一個函數被調用時,會創建一個活動記錄(有時候也稱為執行上下文)。這個記錄會包含函數在哪裡被調用(調用棧)、函數的調用方法、傳入的參數等資訊。this 就是記錄的 其中一個屬性,會在函數執行的過程中用到。
        如何尋找函數的調用位置,從而判斷函數在執行過程中會如何綁定 this?首先找到調用位置:使用開發者工具得到調用棧,然後找到棧中第二個元素,這就是真正的調用位置。

  通過函數調用的位置來確定函數綁定的this對象,需要遵守以下四條規則:

  (1)默認綁定

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

  上面的程式碼中,foo()函數直接調用,不帶任何修飾的函數引用進行調用,因此採用默認綁定,此時this指向window對象,無法應用其他規則。在非 strict mode 下時,默認綁定才能綁定到全局對象,嚴格模式下 this 綁定在undefined。

  (2)隱式綁定–需要考慮的規則是調用位置是否有上下文對象,或者說是否被某個對象擁有或者包含。

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

  上面的程式碼中,foo函數被調用時 obj 對象 「 擁有 」 或者 「 包含 」 它。當函數引 用有上下文對象時,隱式綁定規則會把函數調用中的  this綁定到這個上下文對象,因為調 用 foo() 時 this 被綁定到 obj,因此 this.a 和 obj.a 是一樣的。

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

  上面的程式碼需要值得注意的是:對象屬性引用鏈中只有最頂層或者說最後一層會影響調用位置。

              function foo() { 
                    console.log( this.a ); 
                }
                var obj = { a: "local", foo: foo };
                var bar = obj.foo; // 函數別名!
                var a = " global"; // a 是全局對象的屬性 
                bar(); // "global"

  上面的程式碼,雖然 bar 是 obj.foo 的一個引用,但是實際上,它引用的是 foo 函數本身,因此此時的 bar() 其實是一個不帶任何修飾的函數調用,因此應用了默認綁定。

          function foo(){
                    console.log(this.a)
                }
                var a="global"
                var obj={a:"local",foo:foo}
                var bar=obj.foo
                function doFnc(fn){
                    fn()
                }
                doFnc(bar)//global

  上面的程式碼需要注意的是:被隱式綁定的函數會丟失綁定對象。

  (3)顯示綁定或者硬綁定–call,apply,bind直接指定this的綁定對象 

  call() apply() bind()直接指定 this 的綁定對象,返回一個硬編碼的新函數,它會把參數設置為 this 的上下文並調用原始函數。但是如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值 在調用時會被忽略,實際應用的是默認綁定規則。foo.call( obj );以在調用 foo 時強制把它的 this 綁定到 obj 上

  (4)new綁定

  JavaScript 中 new 的機制實 際上和面向類的語言完全不同。

  在 JavaScript 中,構造函數只是一些 使用 new 操作符時被調用的函數。它們並不會屬於某個類,也不會實例化一個類。實際上, 它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操作符調用的普通函數而已。包括內置對象函數(比如 Number(..))在內的所有函數都可 以用 new 來調用,這種函數調用被稱為構造函數調用。

       使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。 

                1. 創建(或者說構造)一個全新的對象。

                2. 這個新對象會被執行 [[ 原型 ]] 連接。 

                3. 這個新對象會綁定到函數調用的 this。

                4. 如果函數沒有返回其他對象,那麼 new 表達式中的函數調用會自動返回這個新對象

            new 來調用 foo(..) 時,我們會構造一個新對象並把它綁定到 foo(..) 調用中的 this 上。

  5.綁定的優先和一些特殊情況。

  判斷優先順序:
            (1)函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象。 var bar = new foo() 
            (2)函數是否通過 call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是 指定的對象。 var bar = foo.call(obj2) 
            (3)函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上 下文對象。 var bar = obj1.foo() 
            (4)如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 undefined,否則綁定到 全局對象。 var bar = foo()
  特殊情況:

     (1)間接綁定

         function foo() { console.log( this.a ); }
                var a = 2;
                var o = { a: 3, foo: foo };
                var p = { a: 4 }; 
                o.foo(); // 3 
                (p.foo = o.foo)(); // 2
                賦值表達式 p.foo = o.foo 的返回值是目標函數的引用

    對比隱式綁定

          function foo() { console.log( this.a ); }
                var a = 2;
                var o = { a: 3, foo: foo };
                var p = { a: 4 ,fo:o.foo};
                p.fo() //4   對比間接引用

    (2)事件綁定中的this

          1.行內綁定 <input type='button' onclick="this">
                          <input onclick="clickFuc"> function(){  this ...}
                          //this指向window對象
                2.事件監聽與動態綁定
                        <input type="button" value="按鈕" id="btn">
                        <script> var btn = document.getElementById('btn');
                            btn.onclick = function(){
                                this ;  // this指向本節點對象
                            }</script>
                 //動態綁定的事件本身就是對應節點的一個屬性,重新賦值為一個匿名函數,this自然就指向了本節點對象

    (3)箭頭函數

    ES6 中的箭頭函數並不會使用四條標準的綁定規則,而是根據當前的詞法作用域來決定this,具體來說,箭頭函數會繼承外層函數調用的 this 綁定(無論 this 綁定到什麼)。這其實和 ES6 之前程式碼中的 self = this 機制一樣。

總結

  以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。