《你不知道的JavaSript》之this

前言

JavaScripts 的世界中,有不少神奇的 "魔法" ,像使人琢磨不透的原型鏈,也有隱晦的閉包。這篇是關於(《你不知道的JavaScript》上卷中this )的學習筆記,經過總結和反思讓咱們真正掌握複雜而又神奇的機制 —— thisjavascript

使人迷惑的 "動態做用域"

this 被定義在全部函數的做用域中,對於傳統的高級語言,它們有各自的定義,而在 JavaScirpts 中又該如何準確的判斷出這個 this 到底指向誰或者說跟誰綁定,這彷佛是咱們這次討論的重點。可是,this 之因此這麼讓人迷惑大體出於 ——" 動態做用域",咱們先來看一段代碼。java

var a = 2;

function bar() {
    console.log(a);
}

function foo() {
    var a = 3;
    bar();
}

foo();  // 2

首先,經過輸出的結果來看 foo 輸出的並非 2 而不是 3。有人可能會這麼想:當執行 bar()因爲找不到 a 變量的定義時便經過調用棧順着做用域鏈在 foo 方法中找, 這時候發現定義了 a = 3 所以這時候便會輸出 3。若是存在 "動態做用域" 就可以很好的解釋這個誤覺得輸出爲 3 的緣由。可是,結果不會騙人,騙人的是這種嵌套的寫法。閉包

JavaScripts 不存在這種 "動態做用域" 機制,它只有詞法做用域,詞法做用域讓 bar 在定義的時候,經過做用域的提高機制引用到了全局(window)對象上定義的變量 a = 2。所以,當調用 bar 的時候,即使當前處於函數 bar 中,此時的做用域是全局對象,跟代碼中的嵌套無關。app

::: tip
詞法做用域是一套解釋引擎如何查找變量以及在什麼地方找到該變量的規則。詞法做用域在書寫代碼的時候或者定義變量或定義函數的時候就肯定了;不管函數在哪裏被調用,也不論它如何調用,它的詞法做用域都只由被聲明時所處的位置所決定。
:::wordpress

閉包

可是話又說回來,怎麼讓它輸出 3 呢? 咱們經過上述分析以後,獲得的結論是因爲詞法做用域的機制,使得變量a 處於全局做用域下。所以,若是咱們改變 bar 的做用域,讓它處於 foo 中就好了。來看一下以下代碼:函數

var a = 2;

function foo() {
    var a = 3;
    
    function bar() {
        console.log(a);
    }
    
    bar();
}

foo();  // 3

沒錯,利用 閉包 機制來訪問 foo 做用域。可是又有人會感到疑惑,誰是閉包?或者這不就是利用了詞法做用域提高的機制將 bar 所處的做用域提高到了 foo 中了麼。其實,拿閉包或者利用做用域的查找規則來解釋這段代碼都不爲過,利用做用域的查找規則來查找 a 的引用也是閉包的一部分,雖然閉包不是咱們這次講解的重點。學習

咱們換一種更爲通俗的寫法:測試

var a = 2;

function foo() {
    var a = 3;
    
    function bar() {
        console.log(a);
    }
    
    return bar;
}

var baz = foo();
baz();  // 3

咱們先來看下什麼是閉包?this

當函數能夠記住並訪問所在的詞法做用域,即使函數是在它當前的詞法做用域以外被執行,這時候就會產生閉包。prototype

foo 函數就是一個包裝函數,它的返回值是一個內部函數(也就是bar),而後將內部函數的引用賦值給baz,同時內部函數持有外層函數做用域中的變量(a)的引用 ,這個時候bar便持有能夠訪問覆蓋整個 foo 函數內部做用域的引用,這個引用就是閉包。因此 baz 在被調用的時候,其實是執行 foo 上下文環境的 bar ,這時候輸出的變量天然是當前做用域下的 a = 3

::: tip
這裏咱們提到持有該做用域的 引用 ,既然提到引用必然跟對象有關聯。實際上,JavaScirpts 的引擎內部有它自已的一套規則,做用域跟對象相似,可見的操做符都是它的屬性,只不過該做用域 "對象" 只定義在引擎內部。
:::

箭頭函數

上面爲了幫助咱們理解詞法做用域引出了閉包的概念。固然,具體關於閉包的介紹不是咱們討論的重點。另外說關於 this 還有一個不得不說的就是 () => 箭頭函數 。

固然,箭頭函數的引入不僅僅是爲了簡寫 function 而引入的,更爲有意義的是它可以 "繼承" 外層函數的this 綁定,讓 this 在某些場合變得更加 "單純" 一些,咱們來看幾個簡單的例子:

var name = "hello~~";

var obj = {
    name: "kkxiao",
    show: function() {
        console.log(this.name);
    }
}
// 第一種調用方式
obj.show();  // kkxiao

// 第二種調用方式
setTimeout(obj.show, 200); // hello~~

var obj = {
    name: "kkxiao",
    show: () => {
        console.log(this.name)
    }
}

// 改寫後第三種調用方式
setTimeout(obj.show, 200); // hello~~

var obj = {
    name: "kkxiao",
    show: function() {
        setTimeout(function() {
            console.log(this.name);
        }, 200)
    }
}

// 改寫後第四種調用方式
obj.show();  // hello~~

var obj = {
    name: "kkxiao",
    show: function() {
        setTimeout(() => {
            console.log(this.name);
        }, 200)
    }
}
// 改寫後第五種調用方式
obj.show();  // kkxiao

var obj = {
    name: "kkxiao",
    show: function() {
         setTimeout(function() {
            console.log(this.name);
        }.bind(this), 200)
    }
}
// 第六種調用方式
obj.show();  // kkxiao

這裏有個很容易讓人疑惑,稍不留神可能就會出現錯誤(第二種和第四種)。這裏遇到的問題能夠詳見隱式綁定,但這個例子咱們想要說明的是 () => 箭頭函數能夠放棄普通 this 的綁定規則,而且能夠繼承它外層的 this 綁定。

::: tip

這也不是意味着箭頭函數能勝任各類狀況,因爲它是匿名的,因此在一些場景下它並不比具名函數更有使用的價值。具體來說,具名函數擁有以下的優勢:

  1. debug 模式下,因爲沒有合適的名稱,調試起來可能不那麼方便
  2. 在須要引用自身的場景下,匿名函數就顯得很無力。好比,在遞歸的時候須要引用自身,或着在註冊監 聽事件後(addEventListener),須要解綁註冊函數的時候具名函數就很重要了
  3. 在平時開發的過程當中,代碼的可讀性也是很重要的。

因此合理的使用它,讓它發揮出最大的用途。

:::

::: warning

箭頭函數雖然能夠繼承父級做用域,可是它一旦被綁定後就沒法更改,稍後咱們會講到。

:::

以上這些彷佛都沒法解釋 this 的機制,咱們也沒弄懂它到底如何工做。不過不要着急,前面只是一些鋪墊,理解 this 首先要理解詞法做用域。

若是不理解詞法做用域,咱們可能會對 this 產生錯誤的理解:

1、錯覺得 this 指向自身:

function timer() {
    this.count++;
}

timer.count = 0;

for(let i = 0; i < 10; i++) {
    if (i % 2 === 0) {
        timer();
    }
}

console.log(timer.count);  // 0

這彷佛並不像 this 字面量那樣指向 timer 函數自身,但爲何 count 會是 0,或着說 this.count++ 沒被執行呢?

前面咱們有講過,執行 timer 的時候會檢查當前詞法做用域中是否存在 count 變量,沒有的話會發生做用域提高,也就是說會檢查全局做用域中是否存在 count 。然而依舊不存在,因此執行完 timer 以後會在全局對象window 下建立 count 屬性並自增,最後的到 NaN

既然是執行函數的時候當前上下文屬於全局對象 window ,手動讓其引用自身:

function timer() {
    timer.count++;
}

timer.count = 0;

for(let i = 0; i < 10; i++) {
    if (i % 2 === 0) {
        timer();
    }
}

console.log(timer.count);  //  5

咱們手動將其引用自身的屬性 count,這也驗證了剛剛說起的具名函數的優勢(能夠引用自身)。這樣在調用函數的時候即使當前的調用位置是 window 對象,也不影響函數自身建立的屬性。或者,咱們使用 call 來改變當前上下文對象:

function timer() {
    this.count++;
}

timer.count = 0;

for(let i = 0; i < 10; i++) {
    if (i % 2 === 0) {
        timer.call(timer);
    }
}

console.log(timer.count);

2、錯覺得 this 指向函數的詞法做用域:

function showName() {
    var name = "abc";
    this.say()
}

function say() {
    console.log(name)
}

showName(); // undefined

這裏咱們稍後會講 this 的具綁定規則,首先明確調用 showName 的時候 this 使用默認綁定,此時的 this 指向 window,不要錯覺得在 this 指向 showName 的詞法做用域,進而會覺得在 say 中輸出 abcsay 函數的上下文對象依然是 window

實際上函數在被調用的時候,會建立上下文對象(context),這個 context 對象裏面記錄着函數的調用棧(哪裏調用的)、調用方式、入參信息以及 this 綁定的對象。

所以this 既不指向函數自身,也不指向函數的此法做用域,而是經過調用位置的上下文對象來判斷 this的指向 。

綁定規則

剛剛咱們提到,this 的綁定是在函數執行時才確認的,而執行時會建立 context,而 context 中的 this 則是根據當前執行上下文的詞法做用域來確認的。因此,找到函數的 調用位置 就顯得很重要。

即使有如上的分析,可是有的時候函數的調用位置會迷惑咱們。接下來咱們就來具體分析 this 在綁定過程當中的規則,主要有以下四點。

默認綁定

咱們首先來介紹最多見的函數調用:獨立函數調用。這也是四類 this 綁定規則的默認規則:

function intro() {
    console.log(this.name);
}

var name = "kkxiao";

intro();  // kkxiao

你們能夠看到 intro 被調用時是不帶任何修飾的函數引用進行調用的 ,咱們都知道當前的調用位置是在全局做用域中,進而直接輸出全局對象中的 name 屬性,相似與這樣的獨立函數調用即是應用了默認綁定規則。或者咱們能夠理解爲也是使用了的修飾的函數引用調用的,只不過是經過 window.intro()調用罷了。所以 this 綁定到了全局對象當中。

在嚴格模式下會報異常錯誤 TypeError ,而在普通模式下正常。

function intro() {
    "use strict";
    console.log(this.name);
}

var name = "kkxiao";

intro();  // TypeError: Cannot read property 'name' of undefined

這裏有個小細節須要另外關注:

function intro() {
    console.log(this.name);
}

var name = "kkxiao";

(function() {
    "use strict";
    foo();    // kkxiao
})()

::: warning

對於默認綁定來講,決定 this 綁定對象的並非調用位置是否處於嚴格模式,而是函數體是否處於嚴格模式。正如上述代碼輸出的結果:若是函數體處於嚴格模式下,this 會被綁定到 undefined;不然,this 會綁定到全局對象。

:::

隱式綁定

應用該規則的函數調用位置一般存在 上下文 對象,可是這裏面會有陷阱:

var obj = {
    name: "kkxiao",
    say: showName
}

function showName() {
    console.log(this.name)
}

obj.say() // kkxiao

// 或者
var obj = {
    name: "kkxiao",
    say: function() {
        console.log(this.name)
    }
}

obj.say() // kkxiao

咱們觀察它的調用方式:obj.say() ;調用的時候 obj 對象包裹着 say 方法,或者說是上下文環境的 this 指向 obj,所以這種方式的調用 this 會自動綁定到上下午對象上。

此外,經過使用 obj.say 這種方式調用,被調用函數前面帶着 obj 引用;若是對象屬性引用鏈有不止一層的話,那麼只有最後一層引用會綁定到 this 上:

function showName() {
    console.log(this.name)
}

// 注意: 須要先聲明 obj2,不然 obj2 會被聲明爲 undefined, 進而致使 TypeError
var obj2 = {
    name: "kkxiao2",
    say: showName
}

var obj1 = {
    name: "kkxiao1",
    ref: obj2
}

obj1.ref.say()  // kkxiao2

::: warning

注意:這裏有一個很容易致使隱式丟失的問題,那就是無論是先聲明具名 function 再將該方法關聯到對象屬性上也好,仍是直接在對象上定義 function 也罷,該方法其實不是真正屬於這個對象。致使隱式丟失 this 也基本上跟這個問題有關,那就是引用在傳遞後原來綁定在上下午對象可能會改變或丟失。

:::

隱式丟失

剛剛咱們已經提到關於隱士綁定會出現很是常見的問題 —— 隱式丟失。一旦先前綁定的對象(在運行時經過上下文確認)丟失,那它極可能會綁定到全局 window 或者 undefined (嚴格模式下)上。咱們經過幾個例子來分析一下:

第一種:引用經過顯示的賦值給某一變量

function showName() {
    console.log(this.name)
}

var obj = {
    name: "kkxiao",
    say: showName
}

var toSay = obj.say;

var name = "hello~~";

toSay();  // hello~~

致使這個緣由是將 obj.say 引用賦值給 toSay ,但 obj.say 引用的是 showName,因此最後經過 soSay() 調用至關於全局做用域下調用 window.toSay() ,只不過這裏的上下文環境是 window 或者說使用了默認綁定規則。

第二種:使用回調函數

function showName() {
    console.log(this.name)
}

function toSay(fn) {
    // 這裏 this 指向 window
    fn(); // <-- 調用位置
}

var obj = {
    name: "kkxiao",
    say: showName
}
// 或者函數直接聲明在對象上
// var obj = {
//    name: "kkxiao",
//    say: function() {
//        console.log(this.name)
//    }
//}

var name = "hello~~"

toSay(obj.say); // hello~~

首先,fn是經過 toSay 方法的參數進行隱式傳遞,前面咱們在講默認綁定的時候,函數經過不帶任何修飾的函數引用進行調用或者說經過獨立函數調用的時候,this 默認綁定全局對象(window)。因此,showName 方法的上下文對象是 window 。可是咱們看到爲何 toSay 方法的上下文對象也是指向 window ? 同理,調用 toSay 方法的時候也是獨立函數調用呀。

或許有的小夥伴還有疑問:那若是強制改變toSay 上下午環境對象會怎麼樣?咱們知道 callapplybind能夠改變上下午對象指向,這個其實屬於另一種 this 綁定規則 —— 顯示綁定,稍後咱們會講到。可是,爲了說明如今遇到的問題,咱們先來使用 call 測試一下:

function showName() {
    console.log(this.name)
}

function toSay(fn) {
    // 此時 this 指向 obj
    fn(); // <-- 調用位置
}

var obj = {
    name: "kkxiao",
    say: showName
}

var name = "hello~~";

toSay.call(obj, obj.say); // hello~~ 
//或者
toSay.bind(obj, obj.say)(); // hello~~

是否是以爲會輸出kkxiao? 咱們來分析一下: 使用 call 後如今的 toSay 的上下文對象變成了 obj ,可是輸出結果依舊沒有變化。這個緣由以前咱們已經提到過 this 的指向既不指向函數自身也不指向函數的詞法做用域(函數toSay的詞法做用域是window),經過 call 得知此時的上下文雖然指向 obj,可是真正執行 fn 的時候是不帶任何修飾函數的引用調用的(獨立函數調用)。因此,這時的 this 綁定依然是使用默認規則即fnthis 指向 window

咱們來看一下使用 call 或者 bind 後怎麼才能讓它輸出咱們想要的結果:

function toSay(fn) {
    // 此時 this 指向 obj
    this.say(); // <-- 調用位置
}

其實只須要輸出固然上下文對象的 say 方法便可,由於上下文對象已經改變。

除此以外,對於內置函數的 callback 調用也是如此,像 setTimeoutsetInterval等:

function showName() {
    console.log(this.name)
}

var obj = {
    name: "kkxiao",
    say: showName
}

var name = "hello~~";

setTimeout(obj.say, 200); // hello~~

想這些經過回調函數調用的例子,很容易出現 this 隱式丟失的問題。setTimeout 定時器跟咱們寫的 toSay 方法裏執行 fn 是同樣的,最後都是應用了默認綁定規則。

第三這種:間接引用

function showName() {
    console.log(this.name)
}

var name = 'kkxiao';

var obj = {
    name: "hello",
    say: showName
}

var obj1 = {
    name: "world"
}

obj.say();  // hello  隱式綁定規則

(obj1.say = obj.say)(); // kkxiao

針對於 obj.say 你們應該都很清楚這是應用了隱式綁定規則,可是(obj1.say = obj.say)() 這種方式調用爲何會是輸出全局做用域下的變量呢?

你們仔細想想,obj1.say = obj.say 它們都引用了誰? 其實,它們都是引用了全局做用欲下的 showName 方法。但致使輸出這一結果的或者說讓人產生疑惑的地方在於 obj1.say ,覺得采用了隱式綁定規則,其實否則,咱們稍微留下神就會發現,它實際上是經過 showName() 獨立函數調用的。既然是獨立函數調用那就是採用了默認綁定規則,普通模式下 this 指向 window , 嚴格模式下 this 綁定爲 undefined

剛開始是否是以爲很疑惑? 與咱們分析的過程相比,其實結果自己並不那麼重要了,重要的是咱們經過這些例子來搞懂了 this 在隱士綁定的規則。

再次回到咱們討論的話題,既然隱式綁定容易形成 this 丟失,那該如何作能固定住咱們指望的 this 呢?下面咱們接着介紹顯示綁定。

顯示綁定

咱們再將隱式綁定的時候提到過,那就是經過callapplybind。這三種均可以顯示的改變上下文對象,可是 callapply 的區別就在於參數上,而 bind 會返回綁定函數的的拷貝函數,同時支持柯里化。

還有一些細節咱們稍後會講到,咱們先來看下顯示綁定:

var obj = {
    name: 'kkxiao'
}

function showName() {
    console.log(this.name)
}

showName.call(obj); // kkxiao

硬綁定

咱們先來看一下什麼是硬綁定,其實再講隱式綁定的時候咱們提到過:

function showName() {
    console.log(this.name)
}

function toSay(fn) {
    fn.call(obj); // <-- 調用位置
}

var obj = {
    name: "kkxiao",
    say: showName
}

var name = "hello~~"

toSay(obj.say); // kkxiao
// 或者
setTimeout(showName.bind(obj), 100);  // kkxiao

你們注意到 toSay 方法裏面顯示的使用 call 來改變上下午對象,這樣的話即使是獨立函數調用也不受影響,由於上下文對象已經改變。其次 bind 跟它思路相似,都是能夠手動強制更改上下文對象,只不過調用方式會有些不一樣。此外,bind 的功能不限於更改上下文對象,它還能夠用做函數柯里化

須要注意一點,當使用顯示綁定(call、apply)的時候若是不關心當前的上下文對象,當傳入 null

undefined ,這時候 this 會被綁定到 window(非嚴格模式下):

function foo() {
    console.log(this.a);
}

var a = 123;

foo.call(null); // 123

就像這樣,一旦傳入 nullundefined 的時候須要主要是否會形成負面做用,須要謹慎。

此外須要說一下,即使強制更改上下文對象,可是有些狀況 this 丟失的問題依然存在:

var obj = {
    name: "kkxiao"
}

var name = "hello"

function showName() {
    return function() {
        console.log(this.name)
    }
}

var say = showName.call(obj);

var say1 = showName.bind(obj)();

say();   // hello
say1();  // hello

小夥伴們可能會有疑惑,這裏好像是應用了閉包,可是爲何卻應用了默認綁定規則呢? 咱們來分析一下,若是調用 showName.call 或者 showName.bind 產生了一下閉包,那麼即使是獨立函數調用,也不會影響到閉包,由於 saysay1 若是是閉包引用,那麼它關聯的是覆蓋整個 showName 內部整個做用域 this 天然是咱們強制更改後的對象 obj,最後會如願輸出 kkxiao

事實並不是咱們想的那樣,結果輸出的是全局變量 hello ,說明 saysay1 引用的不是指向 showName 內部做用域的閉包。仔細想一下,這個問題和咱們討論隱式綁定間接引用的例子很接近,當時咱們討論最後確認緣由是間接引用的函數的調用方式爲獨立函數調用。咱們回頭看一下這個例子,showName 返回的是一個 function 而後賦值給 say 變量,最後調用 say 方法不就是間接引用的例子是一個問題麼;因此,拋除其它因素,單看這個例子它確實是採用了隱式綁定規則。

話又說回來,這個showName若是建立了閉包環境,那結果就又不同了。

咱們回顧一下前面咱們討論閉包的時候,產生閉包須要具有兩前提條件:一是調用了想要建立內部做用域的包裝函數;二是包裝函數的返回值必須至少包括一個對內部做用域的引用。咱們再來分析一下上述的showName 方法,能夠發現其實咱們少了一個很關鍵的因素 —— 返回值必須至少包括一個對內部做用域的引用。

咱們先來打印一下當前上下文對象都是什麼:

var obj = {
    name: "kkxiao"
}

var name = "hello"

function showName() {
    console.log(this); // {name: "kkxiao"}
    return function() {
        console.log(this.name) // this 指向 window
    }
}

var say = showName.call(obj);

var say1 = showName.bind(obj)();

say();   // hello
say1();  // hello

能夠看到返回的函數外層做用域綁定的 this {name: "kkxiao"} ,這符合預期(使用顯示綁定更改上下文對象)。但如何產生閉包呢? 咱們只須要一個外層做用域的一個引用:

function showName() {
    console.log(this); // {name: "kkxiao"}
    var that = this;   // 引用自身便可
    return function() {
        console.log(that.name) // this 指向 window
    }
}

就像這樣,返回的函數中有外層做用域的一個引用,這樣就會建立一個指向 showName 內部做用域的一個閉包並把它賦值給 saysay1 並利用利用了詞法做用域的查找規則成功訪問到 showName 的內部做用域。

new綁定

前面介紹了三種 this 的綁定規則,最後一種即是 new 綁定。具體來說當使用相似 new myFunction() 的時候會發生什麼,咱們能夠參見 new 運算符,它默認執行以下操做:

  1. 建立一個空的簡單JavaScript對象(即**{}**
  2. 這個新對象會被執行 [[Prototype]] 鏈接(或者繼承 myFunction.prototype
  3. this 會被綁定到該新對象上
  4. 若是 myFunction 未返回其它對象,最後的 new 操做會返回這個新建立的對象

如:

function Person(name, age, sex) {
   this.name = name;
   this.age = age;
   this.sex = sex;
}

var kk = new Persion('kkxiao', 25, '男');

kk.name; // kkxiao
kk.age;  // 25
kk.sex;  // 男

這也是最多見的或者說構建 "類" 對象的操做,這裏的 this 綁定便稱爲 new 綁定。

優先級

說完了四種 this 的綁定規則,咱們在來講說它們之間優先級。平常開發中,可能這些不起眼的操做時常會出如今你的代碼中,同一種代碼中可能應用了好幾種規則,可是它們的優先級是須要咱們格外注意的。

由於默認綁定(window 或 undefined)的優先級毫無疑問是最低的,剩下三種的優先級咱們逐步查看。這裏的例子是咱們這次學習的書中的提到的例子。

隱式綁定和顯示綁定:

function foo() {
    console.log(this.a);
}

var obj1 = {
    a: 123,
    foo
}

var obj2 = {
    a: 456,
    foo
}

obj1.foo(); // 123
obj2.foo(); // 456

obj1.foo.call(obj2); // 456
obj2.foo.call(obj1); // 123

這說明隱式綁定和顯示綁定同時存在的話,顯示綁定的優先級更高。

隱式綁定和 new 綁定:

function foo(id) {
    this.id = id;
}

var obj1 = {
    foo
}

var obj2 = {}

obj1.foo(1);
console.log(obj1.id); // 1

obj1.foo.call(obj2, 2);
console.log(obj2.id); // 2

var bar = new obj1.foo(3);
console.log(obj1.id); // 1
console.log(bar.id); // 3

這個 demo 說明了在隱身規則和 new 綁定規則存在的狀況之下,new 綁定規則的優先級更高。可是咱們也一樣看到了,顯示綁定和 new 綁定它倆之間的優先級誰會更高呢?

由於 callapply 不能使用 new運算符,可是 bind方法可使用,而且 new 運算符和 bind 一塊兒使用的時候 this 會忽略傳入的上下午對象,而是和當前調用的 new 運算符的對象之上:

function foo(id) {
    this.id = id;
}

var obj = {};

var bar = foo.bind(obj);
bar(123);
console.log(obj.id); // 123

var baz = new bar(456);
console.log(obj.id); // 123
console.log(baz.id); // 456

咱們看到在當使用 new 運算符調用經過 bind返回的綁定函數的時候,它並無將 this 綁定到咱們提供的 obj 對象之上,而是將 this 綁定到了一個新對象之上。

接下來咱們來看一下MDN上面的上面的 bind polyfill實現 :

if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    var thatFunc = this, thatArg = arguments[0];
    var args = slice.call(arguments, 1);
    if (typeof thatFunc !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - ' +
             'what is trying to be bound is not callable');
    }
    return function(){
      var funcArgs = args.concat(slice.call(arguments))
      return thatFunc.apply(thatArg, funcArgs);
    };
  };
})();

可是這段 polyfill 沒法使用 new 運算符,由於不管如何 this 都會強制綁定到傳入的對象上(nullundefined)會應用默認綁定規則。現在咱們使用的 bind 是支持 new 操做符的,下面咱們稍微改造一下:

if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - ' +
             'what is trying to be bound is not callable');
    }
      
      var thatFunc = this, thatArg = arguments[0],
          args = slice.call(arguments, 1),
          F = function () {},
          fBind = function () {
              var funcArgs = args.concat(slice.call(arguments))
              return thatFunc.apply(
                  (this instanceof F ? this : thatArg), funcArgs)
          };


        F.prototype = this.prototype;
        fBind.prototype = new F();
        // fBind.prototype = Object.create(this.prototype)
      
      return fBind;
  };
})();

this 的優先級的問題:

  • new 綁定

new綁定的優先級最高,經過new綁定建立的對象的過程上文已經提到。所以,經過new綁定的對象的this指向很容易區分。

  • 顯示綁定

其次即是顯示綁定,涉及到的方式以 callapplybind 爲主,其中 bind 又能夠稱做爲硬綁定。經過顯示綁定的對象能夠更改上下午對象。

  • 隱式綁定

再後就是隱式綁定,隱式綁定是關於 this 指向中最讓人產生疑惑的一種,因爲 this 在函數調用時的位置不定,因此此時的上下午對象也會不確認。不過,就其 this 指向來說,咱們已經分析了大部分的狀況 。所以,只要確認了 this調用時候的上下午對象就能確認出此時的 this 指向。

  • 默認綁定

這也是四種規則中最基礎的一種,它的優先級最低。須要注意的一點是,在嚴格模式下,默認綁定規則中的 this 會被綁定到 undefined,不然會綁定到全局對象(window)上。

箭頭函數的 this 指向

關於箭頭函數,以前咱們已經介紹了一部分。這裏咱們再補充幾點與 this 指向相關的內容:

  • 繼承外層函數上下午對象
function Fn() {
    setTimeout(() => {
        console.log(this.a)
    }, 0)
}

Fn.call({a: '測試箭頭函數'})   // 測試箭頭函數
  • 箭頭函數一旦被綁定就沒法被修改
function Fn() {
    return () => {
        console.log(this.a)
    }
}

var obj1 = {a: 'obj1.a'}
var obj2 = {a: 'obj2.a'}

var fn = Fn.call(obj1);
fn.call(obj2);  // obj1.a

function Fn() {
    setTimeout(() => {
        console.log(this.a);
        setTimeout((() => {
            console.log(this.a);
        }).bind({a: '強制更換綁定'}), 0)
    }, 0)
}

Fn.call({a: '首次綁定'});
// 首次綁定
// 首次綁定

箭頭函數沒有自已的 thisargumentssuper或者使用 new.target,而且不能看成構造函數進行調用。所以,它更適用於匿名的場景。

總結

咱們經過實例講解了 this 指向的問題,若是想要真正的掌握它還須要在平時寫代碼的時候仔細品味。不過,理解它的前提條件不會改變: this 是在函數調用時發生的綁定,它的指向取決於函數在哪裏被調用(確認被調用位置的上下午對象)。只要明確這一點,this指向問題就能清晰的辨析。

相關文章
相關標籤/搜索