前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調試工具:點擊使用
1. 關于 this 的簡單介紹
this 關鍵字是 JavaScript 中最復雜的機制之一。它是一個很特別的關鍵字,被自動定義在所有函數的作用域中。但是即使是非常有經驗的 JavaScript 開發者也很難說清它到底指向什么。
任何足夠先進的技術都和魔法無異。— Arthur C. Clarke
實際上,JavaScript 中 this 的機制并沒有那么先進,但是開發者往往會把理解過程復雜化, 毫無疑問,在缺乏清晰認識的情況下,this 對你來說完全就是一種魔法。
2. 為什么使用 this?
const obj = { title: '掘金', reading() { console.log(this.title + ',一個幫助開發者成長的社區'); } }
this 提供了一種更優雅的方式來隱式“傳遞”一個對象引用,因此可以將 API 設計得更加簡潔并且易于復用。
隨著你的使用模式越來越復雜,顯式傳遞上下文對象會讓代碼變得越來越混亂,使用 this 則不會這樣。當我們介紹對象和原型時,你就會明白函數可以自動引用合適的上下文對象有多重要
3. 關于this 的常見的誤解
人們很容易把 this 理解成指向函數自身,JavaScript 的新手開發者通常會認為,既然把函數看作一個對象(JavaScript 中的所有函數 都是對象),那就可以在調用函數時存儲狀態(屬性的值)。但結果通常讓他們大吃一驚,例如下面這段代碼
function foo() { // 讓新添加的 count + 1 this.count++ } // 向函數對象 foo 添加了一個屬性 count foo.count = 0 foo() console.log(foo.count); // 0
這段代碼看起來沒什么問題,但請把目光聚焦到最后一行,輸出 foo.count
的結果竟然是0 ?!
疑問:為什么會這樣?我明明向函數對象 foo
添加了一個屬性 count
,并且函數內部也寫了 this.count++
,為什么最后還是0
呢?
解答:因為this.count
中此時的 this,根本不是指向 foo 函數自身,而是指向全局 window
。再細心一點,我們可以發現,在 window
身上,被添加了個 count
屬性,值為 NaN。 (為什么 this 指向 window 后面會闡述)
所以,單純的把 this 理解為指向函數自身是錯誤的。
this 實際上是在函數被調用時發生的綁定,它指向什么完全取決于函數在哪里被調用。
4. this 的綁定規則
我們來看看在函數的執行過程中調用位置如何決定 this 的綁定對象。
找到調用位置,然后判斷需要應用下面四條規則中的哪一條。我首先會分別解釋 這四條規則,然后解釋多條規則都可用時它們的優先級如何排列。
4.1 默認綁定
首先要介紹的是最常用的函數調用類型:獨立函數調用。可以把這條規則看作是無法應用其他規則時的默認規則。
function foo() { console.log(this.a) } var a = 2 foo() // 2
-
我們可以看到當調用
foo()
時,this.a
被解析成了全局變量a
。為什么?因為在本例中,函數調用時應用了this
的默認綁定,因此this
指向全局對象。 -
那么我們怎么知道這里應用了默認綁定呢?可以通過分析調用位置來看看
foo()
是如何調 用的。在代碼中,foo()
是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則 -
這條規則也解釋了上面 count 的代碼中,為什么函數里面的
this
指向了window
,因為foo
是屬于獨立函數調用的,觸發了默認綁定,從而指向全局window。(瀏覽器中全局是 window對象,node 中是空對象{}) -
屬于默認綁定規則的還有:
- 函數調用鏈(一個函數又調用另外一個函數)
- 將函數作為參數,傳入到另一個函數中
(擴展:如果使用嚴格模式(strict mode),則不能將全局對象用于默認綁定,因此 this 會綁定到 undefined:)
結論:默認綁定的
this
,都指向全局。
4.2 隱式綁定
4.2.1 一般的對象調用
這一條需要考慮的規則是調用位置是否有上下文對象,或者說是通過某個對象發起的函數調用
function foo() { console.log(this.a) } const obj = { a: 2, foo: foo } // 通過 obj 對象調用 foo 函數 obj.foo() // 2
-
調用位置會使用 obj 上下文來引用函數,因此你可以說函數被調用時 obj 對象“擁 有”或者“包含”它。
-
foo()
被調用時,它的前面確實加上了對obj
的引用。當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this
綁定到這個上下文對象。因為調用foo()
時this
被綁定到obj
上,因此this.a
和obj.a
是一樣的。
4.2.2 對象屬性引用鏈
對象屬性引用鏈中只有上一層或者說最后一層在調用位置中起作用。舉例來說:
function foo() { console.log(this.a) } var obj2 = { a: 2, foo: foo } var obj1 = { a: 1, obj2: obj2 } obj1.obj2.foo() // 2
最終 this
指向的是 obj2
4.2.3 隱式丟失
一個最常見的 this 綁定問題就是被隱式綁定的函數會丟失綁定對象,也就是說它會應用默認綁定,從而把 this 綁定到全局對象或者 undefined 上(取決于是否是嚴格模式)
第一種情況:將對象里的函數賦值給一個變量
function foo() { console.log(this.a) } var obj = { a: 2, foo: foo } var bar = obj.foo // 函數別名! var a = 'global' // a 是全局對象的屬性 bar() // "global"
雖然 bar
是 obj.foo
的一個引用,但是實際上,它引用的是 foo
函數本身,因此此時的 bar()
其實是一個不帶任何修飾的函數調用,因此應用了默認綁定。
第二種情況:傳入回調函數時
function foo() { console.log(this.a) } function doFoo(fn) { // fn 其實引用的是 foo fn() // <-- 調用位置! } var obj = { a: 2, foo: foo } var a = 'global' // a 是全局對象的屬性 doFoo(obj.foo) // "global"
參數傳遞其實就是一種隱式賦值,因此我們傳入函數時也會被隱式賦值,所以結果和上一 個例子一樣。
結論:隱式綁定的 this,指向調用函數的上下文對象。
4.3 顯式綁定
4.3.1 使用 call(…) 和 apply(…)
如果我們不想在對象內部包含函數引用,而想在某個對象上強制調用函數,該怎么做呢?可以使用函數的 call(..)
和 apply(..)
方法
- JavaScript 提供的絕大多數函數以及你自 己創建的所有函數都可以使用
call(..)
和apply(..)
方法。
這兩個方法是如何工作的呢?它們的第一個參數是一個對象,是給 this 準備的,接著在調用函數時將其綁定到 this。因為你可以直接指定 this 的綁定對象,因此我們稱之為顯式綁定。 思考以下代碼:
function foo() { console.log(this.a) } var obj = { a: 2 } foo.call(obj) // 2
-
通過
foo.call(..)
,我們可以在調用foo
時強制把它的this
綁定到obj
上。 -
如果你傳入了一個原始值(字符串類型、布爾類型或者數字類型)來當作 this 的綁定對 象,這個原始值會被轉換成它的對象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。這通常被稱為“裝箱”。
從 this 綁定的角度來說,call(..) 和 apply(..) 是一樣的,它們的區別體現在參數上:第一個參數是相同的,后面的參數,call為參數列表,apply為數組,他們內部的實現原理也不難理解,詳細請看以下兩個手寫方法
手寫 call 方法 超級詳細 ⚡⚡⚡
手寫 apply 方法 超級詳細 ⚡⚡⚡
4.3.2 硬綁定(一個函數總是顯示的綁定到一個對象上)
由于硬綁定是一種非常常用的模式,所以 ES5 提供了內置的方法 Function.prototype.bind
, 它的用法如下
function foo(num) { console.log(this.a, num) return this.a + num } var obj = { a: 2 } // 調用 bind() 方法,返回一個函數 var bar = foo.bind(obj) var b = bar(3) // 2 3 console.log(b) // 5
調用 bind(...)
方法,會返回一個新函數,那么這個新函數的 this
,永遠指向我們傳入的obj
對象
關于 bind 方法的簡單實現,可以前往:手寫 bind 方法,超級詳細 ⚡⚡⚡
4.3.3 API調用的 “上下文(內置函數)
第三方庫的許多函數,以及 JavaScript 語言和宿主環境中許多新的內置函數,都提供了一個可選的參數,通常被稱為“上下文”(context),其作用和 bind(..)
一樣,確保你的回調函數使用指定的 this
。例如:
(1)數組方法 forEach()
function foo(el) { console.log(el, this.id) } var obj = { id: 'bin' }; [1, 2, 3].forEach(foo, obj) // 輸出: // 1 bin // 2 bin // 3 bin
- 調用 foo(..) 時把 this 綁定到 obj 身上
(2)setTimeout()
setTimeout(function() { console.log(this); // window }, 1000);
- 在使用 setTimeout 時會傳入一個回調函數,而這個回調函數中的
this
一般指向window
,這個和 setTimeout 源碼的內部調用有關,這個不再展開贅述
結論:顯式綁定的 this,指向我們指定的綁定對象。
4.4 new 綁定
在 JavaScript 中,普通函數可以使用 new
操作符去調用,此時的普通函數則被稱為 “構造函數”。沒錯,凡是由 new
操作符調用的函數,都稱為 “構造函數”
使用 new 來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。
-
在內存中創建一個新對象。
-
這個新對象內部的[[Prototype]] 特性被賦值為構造函數的 prototype 屬性。
-
構造函數內部的this 被賦值為這個新對象(即this 指向新對象)。
-
執行構造函數內部的代碼(給新對象添加屬性)。
-
如果構造函數返回非空對象,則返回該對象;否則,返回剛創建的新對象。
function foo(a) { this.a = a } var bar = new foo(2) console.log(bar.a) // 2
使用 new
來調用 foo(..)
時,我們會構造一個新對象并把它綁定到 foo(..)
調用中的 this
上。new
是最后一種可以影響函數調用時 this
綁定行為的方法,我們稱之為 new 綁定。
結論:new 綁定的 this,都指向通過 new 調用的函數的實例對象(就是該函數)
5. 綁定規則的優先級
現在我們已經了解了函數調用中 this 綁定的四條規則,你需要做的就是找到函數的調用位置并判斷應當應用哪條規則。但是,如果某個調用位置可以應用多條規則呢?所以就需要有綁定規則的優先級。
它們之間的優先級關系為:
默認綁定 < 隱式綁定 < 顯示綁定(bind) < new綁定
這里提前列出優先級,想看詳細代碼解析的可以往下看,也可以直接拖到最后面的例題部分
5.1 默認綁定的優先級最低
毫無疑問,默認規則的優先級是最低的,因為存在其他規則時,就會通過其他規則的方式來綁定this
5.2 隱式綁定和顯式綁定的優先級比較
測試一下即可知道,有以下代碼:
function foo() { console.log(this.a) } var obj1 = { a: 1, foo: foo } var obj2 = { a: 2, foo: foo } // 同時使用隱式綁定和顯示綁定 obj1.foo.call(obj2) // 2
可以看到,輸出的結果為 2,說明 foo
函數內 this
指向的是 obj2
,而 obj2 是通過顯示綁定調用的,所以:顯示綁定的優先級更高
5.3 隱式綁定和 new 綁定的優先級比較
有以下測試代碼:
function foo() { console.log(this); } var obj = { title: "juejin", foo: foo } // 同時使用隱式綁定和new綁定 new obj.foo(); // foo對象
最后 foo
函數輸出的 this
為 foo 對象,說明new綁定優先級更高(否則應該輸出 obj 對象),所以:new 綁定的優先級更高
5.4 new 綁定和顯示綁定的優先級比較
最后,new 綁定和顯式綁定誰的優先級更高呢?
new綁定和call、apply是不允許同時使用的,只能和 bind 相比較,如下:
function foo() { console.log(this) } var obj = { title: "juejin" } var foo = new foo.call(obj); // 直接報錯
但是 new 綁定可以和 bind 方法返回后的函數一起使用
function foo() { console.log(this); } var obj = { title: "juejin" } var bar = foo.bind(obj); var foo = new bar(); // foo 對象, 說明使用的是new綁定
最后 foo
函數輸出的 this
為 foo 對象,說明new綁定優先級更高(否則應該輸出 obj 對象),所以:new 綁定的優先級更高
優先級結論:默認綁定 < 隱式綁定 < 顯示綁定(bind)< new綁定
6. 判斷this
現在我們可以根據優先級來判斷函數在某個調用位置應用的是哪條規則。可以按照下面的 順序來進行判斷:
-
函數是否在 new 中調用(new 綁定)?如果是的話 this 綁定的是新創建的對象。 var bar = new foo()
-
函數是否通過 call、apply(顯式綁定)或者硬綁定調用?如果是的話,this 綁定的是 指定的對象。 var bar = foo.call(obj2)
-
函數是否在某個上下文對象中調用(隱式綁定)?如果是的話,this 綁定的是那個上 下文對象。 var bar = obj1.foo()
-
如果都不是的話,使用默認綁定。如果在嚴格模式下,就綁定到 undefined,否則綁定 到全局對象。 var bar = foo()
就是這樣。對于正常的函數調用來說,理解了這些知識你就可以明白 this 的綁定原理了。 不過……凡事總有例外
7. 綁定例外
規則總有例外,這里也一樣。在某些場景下 this 的綁定行為會出乎意料,你認為應當應用其他綁定規則時,實際上應用的可能是默認綁定規則。
7.1 箭頭函數
箭頭函數不使用 this 的四種標準規則,而是根據外層(函數或者全局)作用域來決定 this。
7.2 被忽略的this
如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call、apply 或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規則:
function foo() { console.log(this.a) } var a = 2 foo.call(null) // 2 foo.call(undefined) // 2 foo.bind(null)();
最后輸出的結果都是 2,說明 this
指向的是全局 window
7.3 間接引用
另一個需要注意的是,你有可能(有意或者無意地)創建一個函數的間接引用,在這種情況下,調用這個函數會應用默認綁定規則。 間接引用最容易在賦值時發生:
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
的返回值是目標函數的引用,因此調用位置是 foo()
屬于獨立函數調用,而不是 p.foo()
或者 o.foo()
。根據我們之前說過的,這里會應用默認綁定。
8. this 判斷例題
請說出例題中的輸出結果
8.1 例題一
var name = "window"; var person = { name: "person", sayName: function () { console.log(this.name); } }; function sayName() { var sss = person.sayName; sss(); person.sayName(); (person.sayName)(); (b = person.sayName)(); } sayName();
解析:
function sayName() { var sss = person.sayName; // 獨立函數調用,沒有和任何對象關聯 sss(); // window // 關聯 person.sayName(); // person (person.sayName)(); // person (b = person.sayName)(); // window }
8.2 例題二
var name = 'window' var person1 = { name: 'person1', foo1: function () { console.log(this.name) }, foo2: () => console.log(this.name), foo3: function () { return function () { console.log(this.name) } }, foo4: function () { return () => { console.log(this.name) } } } var person2 = { name: 'person2' } person1.foo1(); person1.foo1.call(person2); person1.foo2(); person1.foo2.call(person2); person1.foo3()(); person1.foo3.call(person2)(); person1.foo3().call(person2); person1.foo4()(); person1.foo4.call(person2)(); person1.foo4().call(person2);
解析:
// 隱式綁定,肯定是person1 person1.foo1(); // person1 // 隱式綁定和顯示綁定的結合,顯示綁定生效,所以是person2 person1.foo1.call(person2); // person2 // foo2()是一個箭頭函數,不適用所有的規則 person1.foo2() // window // foo2依然是箭頭函數,不適用于顯示綁定的規則 person1.foo2.call(person2) // window // 獲取到foo3,但是調用位置是全局作用于下,所以是默認綁定window person1.foo3()() // window // foo3顯示綁定到person2中 // 但是拿到的返回函數依然是在全局下調用,所以依然是window person1.foo3.call(person2)() // window // 拿到foo3返回的函數,通過顯示綁定到person2中,所以是person2 person1.foo3().call(person2) // person2 // foo4()的函數返回的是一個箭頭函數 // 箭頭函數的執行找上層作用域,是person1 person1.foo4()() // person1 // foo4()顯示綁定到person2中,并且返回一個箭頭函數 // 箭頭函數找上層作用域,是person2 person1.foo4.call(person2)() // person2 // foo4返回的是箭頭函數,箭頭函數只看上層作用域 person1.foo4().call(person2) // person1
9. 總結
如果要判斷一個運行中函數的 this 綁定,就需要找到這個函數的直接調用位置。找到之后就可以順序應用下面這四條規則來判斷 this 的綁定對象。
-
由 new 調用?綁定到新創建的對象。
-
由 call 或者 apply(或者 bind)調用?綁定到指定的對象。
-
由上下文對象調用?綁定到那個上下文對象。
-
默認:在嚴格模式下綁定到 undefined,否則綁定到全局對象。
每文一句:如果把生活比喻為創作的意境,那么閱讀就像陽光。
ok,本次的分享就到這里,如果本章內容對你有所幫助的話可以點贊+收藏,希望大家都能夠有所收獲。有任何疑問都可以在評論區留言,大家一起探討、進步!
【推薦學習:javascript高級教程】