this指向

1. 迷之this

對於剛開始進行 JavaScript 編程的開發者來講,this 具備強大的魔力,它像謎團同樣須要工程師們花大量的精力去真正理解它。express

在後端的一些編程語言中,例如 Java、PHP,this僅僅是類方法中當前對象的一個實例,它不能在方法外部被調用,這樣一個簡單的法則並不會形成任何疑惑。編程

在 JavaScript 中,this 是指當前函數中正在執行的上下文環境,由於這門語言擁有四種不一樣的函數調用類型:後端

  • 函數調用 alert('Hello World!')數組

  • 方法調用 console.log('Hello World!')瀏覽器

  • 構造函數調用 new RegExp('\\d')安全

  • 間接調用 alert.call(undefined, 'Hello World')app

在以上每一項調用中,它都擁有各自獨立的上下文環境,就會形成 this 所指意義有所差異。此外,嚴格模式也會對執行環境形成影響。編程語言

理解 this 關鍵字的關鍵在於理解各類不一樣的函數調用以及它是如何影響上下文環境的。函數

 

這篇文章旨在解釋不一樣狀況下的函數調用會怎樣影響 this 以及判斷上下文環境時會產生的一些常見陷阱。this

 

在開始講述以前,先熟悉如下一些術語:

  • 調用 是執行當前函數主體的代碼,即調用一個函數。例:parseInt 函數的調用爲 parseInt(15)

  • 上下文環境 是方法調用中 this 所表明的值

  • 做用域 是一系列方法內可調用到的變量,對象,方法組成的集合

 

2. 函數調用

函數調用 表明了該函數接收以成對的引號包含,用逗號分隔的不一樣參數組成的表達式。舉例:parseInt('18')。這個表達式不能是屬性訪問如 myObject.myFunction 這樣會形成方法調用。[1, 5].join(',') 一樣也不是一個函數調用而是方法調用。

 

函數調用的一個簡單例子:

hello('World') 是一個函數調用:hello表達式表明了一個函數對象,接受了用成對引號包含的 World 參數。

 

高級一點的例子,當即執行函數 IIFE (immediately-invoked function expression):

 

2.1. 函數調用中的 this

this is the global object in a function invocation

全局對象取決於當前執行環境,在瀏覽器中,全局對象即 window。

 

在函數調用中,上下文執行環境是全局對象,能夠在如下函數中驗證上下文:

當 sum(15, 16) 被調用時,JavaScript 自動將 this 設置爲全局對象,即 window。

 

當 this 在任何函數做用域之外調用時(最外層做用域:全局執行上下文環境),也會涉及到全局對象。

 

2.2. 嚴格模式下,函數調用中的 this

this is undefined in a function invocation in strict mode

 

嚴格模式由 ECMAScript 5.1 引進,用來限制 JavaScript 的一些異常處理,提供更好的安全性和更強壯的錯誤檢查機制。使用嚴格模式,只須要將 'use strict' 置於函數體的頂部。這樣就能夠將上下文環境中的this 轉爲 undefined。這樣執行上下文環境再也不是全局對象,與非嚴格模式恰好相反。

 

在嚴格模式下執行函數的一個例子:

當 multiply(2, 5) 執行時,這個函數中的 this 是 undefined。

 

嚴格模式不只在當前做用域起到做用,它還會影響內部做用域,即內部聲明的一切內部函數的做用域。

use strict 被插入函數執行主體的頂部,使嚴格模式能夠控制到整個做用域。由於 concat 在執行做用域內部聲明,所以它繼承了嚴格模式。此外,concat('Hello', ' World!') 的調用中,this 也會成爲undefined。

 

一個簡單的 JavaScript 文件可能同時包含嚴格模式和非嚴格模式,因此在同一種類型調用中,可能也會有不一樣的上下文行爲差別。

2.3. 陷阱:this 在內部函數中

一個常見的陷阱是理所應當的認爲函數調用中的,內部函數中 this 等同於它的外部函數中的 this。

 

正確的理解是內部函數的上下文環境取決於調用環境,而不是外部函數的上下文環境。

 

爲了獲取到所指望的 this,應該利用間接調用修改內部函數的上下文環境,如使用 .call() 或者 .apply或者建立一個綁定函數 .bind()。

 

下面的例子表示計算兩個數之和:

numbers.sum() 是對象內的一個方法調用,所以 sum 的上下文是 numbers 對象,而 calculate 函數定義在 sum 函數內,因此會誤覺得在 calculate 內 this 也指向的是 numbers。

 

然而 calculate() 在函數調用(而不是做爲方法調用)時,此時的 this 指向的是全局對象 window 或者在嚴格模式下指向 undefined ,即便外部函數 sum 擁有 numbers對象做上下文環境,它也沒有辦法影響到內部的 this。

 

numbers.sum() 調用的結果是 NaN 或者在嚴格模式下直接拋出錯誤 TypeError: Cannot read property 'numberA' of undefined,而絕非期待的結果 5 10 = 15,形成這樣的緣由是 calculate 並無正確的被調用。

 

爲了解決這個問題,正確的方法是使 calculate 函數被調用時的上下文同 sum 調用時同樣,爲了獲得屬性numberA 和 numberB,其中一種辦法是使用 .call() 方法。

calculate.call(this) 一樣執行 calculate 函數,可是格外的添加了 this做爲第一個參數,修改了上下文執行環境。此時的 this.numberA this.numberB 等同於 numbers.numberA numbers.numberB,其最終的結果就會如期盼的同樣爲 result 5 10 = 15。

 

3. 方法調用

方法是做爲一個對象屬性存儲的函數,舉個例子:

helloFunction 是屬於 myObject 的一個方法,調用這個方法可使用屬性訪問的方式myObject.helloFunction。

 

方法調用表現爲對象屬性訪問的形式,支持傳入用成對引號包裹起來的一系列參數。上個例子中,myObject.helloFunction() 其實就是對象 myObject 上對屬性 helloFunction 的方法調用。一樣,[1, 2].join(',') 和 /\s/.test('beautiful world') 都是方法調用。

 

區分函數調用和方法調用是很是重要的,它們是不一樣類型的調用方式。主要的差異在於方法調用爲訪問屬性的形式,如:<expression>.functionProperty() 或者 <expression>['functionProperty'](),而函數調用爲<expression>()。

 

3.1. 方法調用中的 this

this is the object that owns the method in a method invocation

當在一個對象裏調用方法時,this 表明的是對象它自身。讓咱們建立一個對象,其包含一個能夠遞增屬性的方法。

calc.increment() 調用意味着上下文執行環境在 calc 對象裏,所以使用 this.sum 遞增 num 這個屬性是可行的。

 

一個 JavaScript 對象繼承方法來自於它自身的屬性。當一個被繼承方法在對象中調用時,上下文執行環境一樣是對象自己。

Object.create() 建立了一個新的對象 myDog 而且設置了屬性,myDog 對象繼承了 myName方法。當myDog.sayName() 被執行時,上下文執行環境指向 myDog。

 

在 ECMAScript 5 的 class 語法中, 方法調用指的是實例自己。

 

3.2. 陷阱:方法會分離它自身的對象

一個對象中的方法可能會被提取抽離成一個變量。當使用這個變量調用方法時,開發者可能會誤認爲 this指向的仍是定義該方法時的對象。

 

若是方法調用不依靠對象,那麼就是一個函數調用,即 this 指向全局對象 object 或者在嚴格模式下爲undefined。建立函數綁定能夠修復上下文,使該方法被正確對象調用。

 

下面的例子建立了構造器函數 Animal 而且建立了一個實例 myCat,在 setTimeout() 定時器 1s 後打印myCat 對象信息。

開發者可能認爲在 setTimeout 下調用 myCat.logInfo() 會打印出 myCat 對象的信息。但實際上這個方法被分離了出來做爲了參數傳入函數內 setTimeout(myCat.logInfo),而後 1s 後會發生函數調用。當logInfo 被做爲函數調用時,this 指向全局對象 window 或者在嚴格模式下爲 undefined,所以對象信息沒有正確地被打印。

 

方法綁定可使用 .bind() 方法。若是被分離的方法綁定了 myCat 對象,那麼上下文問題就能夠被解決了:

此時,myCat.logInfo.bind(myCat) 返回的新函數調用裏的 this 指向了 myCat。

 

4. 構造函數調用

構造函數調用使用 new 關鍵詞,後面跟隨可帶參數的對象表達式,例:new RegExp('\\d')。

 

如下的例子聲明瞭一個構造函數 Country,並調用。

new City('Paris') 是一個構造器調用,這個對象初始化使用了類中特殊的方法 constructor,其中的this 指向的是新建立的對象。

 

構造器調用建立了一個空的新對象,從構造器的原型中繼承屬性。這個構造器函數的意義在於初始化對象,所以這個類型的函數調用建立實例。

 

當一個屬性訪問 myObject.myFunction 前擁有 new 關鍵詞,那麼 JavaScript 會執行構造器調用而不是方法調用。舉個例子:new myObject.myFunction() 意味着首先這個函數會解析爲一個屬性訪問函數extractedFunction = myObject.myFunction,而後用構造器建立一個新對象 new extractedFunction。

 

4.1. 在構造函數調用中的 this

this is the newly created object in a constructor invocation

構造器調用的環境是新建立的對象。經過傳遞構造函數參數來初始化新建的對象,添加屬性初始化值以及事件處理器。

 

讓咱們來驗證如下這個例子的上下文環境:

new Foo() 創建構造器調用,它的上下文環境爲 fooInstance,在 Foo 對象中初始化了 this.property 這個屬性並賦予初始值。

 

在使用 class 語法時也是一樣的狀況(在 ES6 中),初始化只發生在它的 constructor 方法中。

當執行 new Bar() 時,JavaScript 建立了一個空對象而且它的上下文環境爲 constructor 方法,所以添加屬性的辦法是使用 this 關鍵詞:this.property = 'Default Value'。

 

4.2. 陷阱:忘記添加 new 關鍵詞

一些 JavaScript 函數建立實例,不只僅可使用構造器的形式調用也能夠利用函數調用,下面是一個RegExp 的例子:

當執行 new RegExp('\\w ') 和 RegExp('\\w ') 時,JavaScript 建立了兩個相等的普通表達式對象。

 

可是使用函數調用建立對象會產生潛在的問題(包括工廠模式),當失去了 new 關鍵詞,一些構造器會取消初始化對象。

 

如下例子描述了這個問題:

Vehicle 是一個在對象上設置了 type 和 wheelsCount 屬性的函數。

 

當執行了 Vehicle('Car', 4) 時,會返回對象 car,它擁有正確的屬性值:car.type 指向Car,car.wheelsCount 指向 4,開發者會誤覺得這樣建立初始化對象沒有什麼問題。

 

然而,當前執行的是函數調用,所以 this 指向的是 window 對象,因此它設置的屬性實際上是掛在 window對象上的,這樣是徹底錯誤的,它並無建立一個新對象。

 

應該正確的執行方式是使用 new 關鍵詞來保證構造器被正確調用:

new Vehicle('Car', 4) 能夠正確運行:一個新的對象被建立和初始化,由於 new 關鍵詞表明瞭當前爲構造器調用。

 

在構造器函數中添加驗證:this instanceof Vehicle,能夠保證當前的執行上下文是正確的對象類型。若是 this 不是指向 Vehicle,那麼就存在錯誤。 若是 Vehicle('Broken Car', 3) 表達式沒有 new 關鍵詞而被執行,就會拋出錯誤:Error: Incorrect invocation。

 

5. 間接調用

間接調用表現爲當一個函數使用了 .call() 或者 .apply() 方法。

 

在 JavaScript 中,函數爲一等對象,這意味着函數是一個對象,對象類型即爲 Function。

 

在函數的一系列方法中,.call() 和 .apply() 被用來配置當前調用的上下文環境。

方法 .call(thisArg[, arg1[, arg2[, ...]]]) 接收第一個參數 thisArg 做爲執行的上下文環境,以及一系列參數 arg1, arg2, ...做爲函數的傳參被調用。

 

而且,方法 .apply(thisArg, [args]) 接收 thisArg做爲上下文環境,剩下的參數能夠用類數組對象[args] 傳遞。

 

間接調用的例子:

increment.call() 和 increment.apply() 同時傳遞了參數 10 調用 increment 函數。

 

兩個方法最主要的區別爲 .call() 接收一組參數,如 myFunction.call(thisValue, 'value1', 'value2'),而 .apply() 接收一串參數做爲類數組對象傳遞,如 myFunction.apply(thisValue, ['value1', 'value2'])。

 

5.1. 間接調用中的 this

this is the first argument of .call() or .apply() in an indirect invocation

 

很明顯,在間接調用中,this 指向的是 .call() 和 .apply()傳遞的第一個參數。

當函數執行須要特別指定上下文時,間接調用很是有用,它能夠解決函數調用中的上下文問題(this 指向window 或者嚴格模式下指向 undefined),同時也能夠用來模擬方法調用對象。

 

另外一個實踐例子爲,在 ES5 中的類繼承中,調用父級構造器。

Runner.call(this, name) 在 Rabbit 裏間接調用了父級方法初始化對象。

 

6. 綁定函數調用

綁定函數調用是將函數綁定一個對象,它是一個原始函數使用了 .bind() 方法。

 

原始綁定函數共享相同的代碼和做用域,可是在執行時擁有不一樣的上下文環境。

 

方法 .bind(thisArg[, arg1[, arg2[, ...]]]) 接收第一個參數 thisArg 做爲綁定函數在執行時的上下文環境,以及一組參數 arg1, arg2, ... 做爲傳參傳入函數中。 它返回一個新的函數,綁定了 thisArg。

 

下列代碼建立了一個綁定函數並在以後被調用:

multiply.bind(2) 返回一個新的函數對象 double,它綁定了數字 2。multiply 和 double 函數擁有相同的代碼和做用域。

 

對比方法 .apply() 和 .call(),它倆都當即執行了函數,而 .bind() 函數返回了一個新方法,綁定了預先指定好的 this ,並能夠延後調用。

 

6.1. 綁定函數中的 this

this is the first argument of .bind() when invoking a bound function

 

.bind() 方法的做用是建立一個新的函數,執行時的上下文環境爲 .bind() 傳遞的第一個參數,它容許建立預先設置好 this 的函數。

 

讓咱們來看看在綁定函數中如何設置 this :

numbers.countNumbers.bind(numbers) 返回了綁定 numbers 對象的函數 boundGetNumbers,它在調用時的this 指向的是 numbers 而且返回正確的數組對象。

 

.bind() 建立了一個永恆的上下文鏈並不可修改。一個綁定函數即便使用 .call() 或者 .apply()傳入其餘不一樣的上下文環境,也不會更改它以前鏈接的上下文環境,從新綁定也不會起任何做用。

 

只有在構造器調用時,綁定函數能夠改變上下文,然而這並非特別推薦的作法。

 

下面這個例子聲明瞭一個綁定函數,而後試圖更改其預約上下文的狀況:

只有 new one() 時能夠改變綁定函數的上下文環境,其餘類型的調用結果是 this 永遠指向 1。

 

7. 箭頭函數

箭頭函數的設計意圖是以精簡的方式建立函數,並綁定定義時的上下文環境。

箭頭函數使用了輕便的語法,去除了關鍵詞 function 的書寫,甚至當函數只有一個句子時,能夠省去return 不寫。

 

箭頭函數是匿名的,意味着函數的屬性 name 是一個空字符串 '',它沒有一個詞彙式的函數名,意味着不利於使用遞歸或者解除事件處理。

 

同時它不一樣於普通函數,它不提供 arguments 對象,在 ES6 中能夠用另外的參數代替:

 

7.1. 箭頭函數中的 this

this is the enclosing context where the arrow function is defined

 

箭頭函數並不建立它自身執行的上下文,使得 this 取決於它在定義時的外部函數。

 

下面的例子表示了上下文的透明屬性:

setTimeout 調用了箭頭函數,它的上下文和 log()方法同樣都是 myPoint 對象。

能夠看出來,箭頭函數「繼承」了它在定義時的函數上下文。

 

若是嘗試在上述例子中使用正常函數,那麼它會建立自身的做用域(window 或者嚴格模式下undefined)。所以,要使一樣的代碼能夠正確運行就必須人工綁定上下文,即 setTimeout(function() {...}.bind(this))。使用箭頭函數就能夠省略這麼詳細的函數綁定,用更加乾淨簡短的代碼綁定函數。

 

若是箭頭函數在最外層做用域定義,那麼上下文環境將永遠是全局對象,通常來講在瀏覽器中即爲window。

 

箭頭函數一次綁定上下文後便不可更改,即便使用了上下文更改的方法:

函數表達式能夠間接調用 .call(numbers) 讓 this 指向 numbers,然而 get 箭頭函數的 this 也是指向numbers 的, 由於它綁定了定義時的外部函數。

 

不管怎麼調用 get 函數,它的初始化上下文始終是 numbers,間接地調用其餘上下文(使用 .call() 或者.apply()),或者從新綁定上下文(使用 .bind())都沒有任何做用。

 

箭頭函數不能夠用做構造器,若是使用 new get() 做構造器調用,JavaScript 會拋出錯誤:TypeError: get is not a constructor。

 

7.2. 陷阱:使用箭頭函數定義方法

開發者可能會想使用箭頭函數在對象中聲明方法,箭頭函數的聲明((param) => {...})要比函數表達式的聲明(function(param) {...})簡短的多。

 

下面的例子在類 Period 中 使用箭頭函數定義了方法 format():

當 format 是一個箭頭函數, 且被定義在全局環境下,它的 this 指向的是 window 對象。

 

即便 format 執行的時候掛載在對象上 walkPeriod.format(),window 對象依舊存在在調用的上下文環境中。這是由於箭頭函數擁有靜態的上下文環境,不會由於不一樣的調用而改變。

 

this 指向的是 window,所以 this.hour 和 this.minutes 都是 undefined。方法返回的結果爲:'undefined hours and undefined minutes'。

 

正確的函數表達式能夠解決這個問題,由於普通函數能夠改變調用時的上下文環境:

walkPeriod.format() 是一個在對象中的方法調用,它的上下文環境爲 walkPeriod,this.hours 指向2,this.minutes 指向 30,所以能夠返回正確的結果:'2 hours and 30 minutes'。

 

8. 結論

由於函數調用會極大地影響到 this,因此從如今開始不要直接問本身:

this 是從哪裏來的?

 

而是要開始思考:

當前函數是怎麼被調用的?

 

遇到箭頭函數時,考慮:

當箭頭函數被定義時,this 是指向什麼?

 

以上思路能夠幫助開發者減小判斷 this 帶來的煩惱。

相關文章
相關標籤/搜索