玩轉 JavaScript 之詳解 this
this 關鍵字作為 JavaScript 中自動定義的特殊識別符號,是我們不得不去面對、瞭解的知識點,很多初學者對 this 關鍵字可能會有含糊不清的感覺,但其實稍微理一理, this 並不複雜、不混亂。
this 是什麼?
概述中我們說了 this 是 JavaScript 中的一個特殊關鍵字,this 和執行上下文相關,當一個函式被呼叫時,會建立一個活動記錄,也稱為執行環境。這個記錄包含函式是從何處(call-stack)被呼叫的,函式是如何被呼叫的,被傳遞了什麼引數等資訊。這個記錄的屬性之一,就是在函式執行期間將被使用的 this 引用。 但是本文中並不準備往深層次分析其到底是什麼,而是說明在應用場景和淺層原理中分析 this 到底是什麼。 在分析之前,我們先來看一看當初的我們為什麼會用 this。
為什麼用 this?
以下是一段程式碼例項,其中用到了this
。
let me = { name: 'seymoe' } function toUpperCase() { return this.name.toUpperCase() } toUpperCase.call(me)// 'SEYMOE' 複製程式碼
當然以上程式碼也完全可以不用this
而採用傳參的形式實現。
let me = { name: 'seymoe' } function toUpperCase(person) { return person.name.toUpperCase() } toUpperCase(me)// 'SEYMOE' 複製程式碼
在這裡為什麼用this
而不用傳參的形式,是因為this
機制用更優雅的方式隱含的傳遞一個物件的引用,可以擁有更乾淨的 API 設計和簡單複用。使用模式越複雜,通過明確引數傳遞執行環境和傳遞this
執行環境相比,就越複雜,當然以上只是一個應用場景之一。
呼叫棧與呼叫點
this 不是編寫時繫結,而是執行時繫結。它依賴於函式呼叫的上下文條件。this 繫結和函式宣告的位置無關,反而和函式被呼叫的方式有關,被呼叫的這個位置就叫呼叫點。所以我們分析 this 是什麼的時候就必須分析呼叫棧(使我們到達當前執行位置而被呼叫的所有方法的堆疊)和呼叫點。
function baz() { // 呼叫棧是‘baz’,呼叫點是全域性作用域 console.log('baz') bar()2. // bar的呼叫點 } function bar() { // 呼叫棧是‘baz - bar’,呼叫點位於baz的函式作用域內 console.log('bar') } baz()// 1. baz的呼叫點 複製程式碼
以上應該比較容易理解baz
和bar
函式相對應的呼叫點和呼叫棧。
重點!this 的指向規則
現在我們知道了呼叫點,而呼叫點決定了函式呼叫期間 this 指向哪裡的四 種規則,所以排好隊一個一個來分析吧~
1. 預設繫結
顧名思義,就是 this 沒有其他規則適用時的預設規則,獨立函式呼叫就是最常見的情況。
var a = 2 function foo() { console.log(this.a) } function bar() { foo() } foo()// 2 bar()// 2 複製程式碼
foo
是一個直白的毫無修飾的函式引用呼叫,所以預設綁定了全域性物件,當然如果是嚴格模式"use strict"
this 將會是undefined
。
注意:雖然是基於呼叫點,但只要foo的內容沒在嚴格模式下,那就預設繫結全域性物件。
var a = 2 function foo() { console.log(this.a) } (function (){ "use strict"; foo()// 2 })() 複製程式碼
2. 隱含繫結
呼叫點是否擁有一個環境物件,或(擁有者、容器物件)。
function foo() { console.log(this.a) } let obj = { a: 2, foo: foo } obj.foo()// 2 複製程式碼
當一個方法引用存在一個環境物件,隱含規則為該物件應該被用於這個函式呼叫的this繫結。
隱含繫結的情況下,容易出現丟失 的情況!當隱含繫結丟失了它的繫結,意味著它會回退到預設繫結,下面是例子:
var a = 3 function foo() { console.log(this.a) } let obj = { a: 2, foo: foo } let bar = obj.foo bar()// 3 // 另一種微妙的情況 function doFoo(fn) { fn && fn() } doFoo(obj.foo)// 3 複製程式碼
函式的引數傳遞只是一種隱含的賦值,fn是foo函式的一個引用,而呼叫fn則是毫無掩飾的呼叫一個函式,預設繫結規則 。
3. 明確繫結
隱含繫結需要我們改變物件自身包含一個函式的引用來使 this 隱含的繫結到這個物件上,預設繫結也是不確定的情況,但是很多時候我們希望能夠明確的使一個函式呼叫時使用某個特定物件作為 this 繫結,而不在這個物件上放置一個函式引用屬性。
這個時候,call
和apply
就該上場了。
JavaScript 中幾乎所有的函式都能訪問這兩個方法,這兩個方法接收的第一個引數都是一個用於 this 的物件,之後用這個指定的 this 來呼叫函式,這種方式就叫明確繫結。
function foo() { console.log(this.a) } let obj = { a: 2 } foo.call(obj)// 2 複製程式碼
一種明確繫結的變種可以保證一個函式始終被obj呼叫,無論如何也不會改變,這種方式叫硬繫結,通過bind
方法實現。
var obj = { a: 2 } function foo(something) { console.log(this.a, something) return this.a + something } var bar = foo.bind(obj) bar(' is a number.')// 2 ,'is a number.' 複製程式碼
我們注意到採用bind
方式進行硬繫結時,該方法返回一個函式,這和call
和apply
是有所區別的。
4. new 繫結
傳統面嚮物件語言中,通過new
操作符呼叫建構函式會生成一個類例項。在 JavaScript 中其實沒有構造器、類的概念,new 呼叫的函式僅僅只是一個函式,只是被new
呼叫時改變了行為。所以不存在構造器函式,只存在函式的構造器呼叫。
new
操作符呼叫時會建立一個全新物件,連線原型鏈,並將這個新建立的物件設定為函式呼叫的 this 繫結,(預設情況)自動返回這個全新物件。
function Foo(a) { this.a = a } let bar = new Foo(2) console.log(bar.a)// 2 複製程式碼
優先順序順序
以上的規則在適用時存在優先順序,級別如下:
硬繫結 > new 繫結 > 明確繫結 > 隱含繫結 > 預設繫結
所以我們已經能夠總結出判定 this 的一般流程了。
判定 this 一般流程
-
如果是
new
呼叫,this 是新構建的物件; -
call
、apply
或bind
,this 是明確指定的物件; - 是用環境物件(或容器)呼叫的,this 是這個容器;
-
預設繫結,嚴格模式為
undefined
,否則是global(全域性)物件。
箭頭函式中的 this
單獨將箭頭函式中的 this 列出來是因為並不能因為 this 在箭頭函式中就有特殊的指向,而是因為
箭頭函式不會像普通函式去使用 this, 箭頭函式的 this 和外層的 this 保持一致。這種保持一致是強力的,無法通過call
、apply
或bind
來改變指向。
const obj = { a: () => { console.log(this) } } obj.a() // window obj.a.bind({})()// window 複製程式碼
測驗
最後,下面這個簡單的測驗,並不是很繞很難的面試題,有興趣不妨做做,評論區回覆答案一起探討一下~
var a = 1 var obj = { a: 2, } var bar obj.foo = foo bar = obj.foo function foo() { var a = 3 console.log(this.a) } foo()// 1. ??? ;(function (a) { "use strict"; foo()// 2. ??? bar.bind(a)// 3. ??? })(this) obj.foo()// 4. ??? obj.foo.call(this)// 5. ??? bar()// 6. ??? bar.apply(obj)// 7. ??? var b = new foo()// 8. ??? console.log(b.a)// 9. ??? 複製程式碼