目次
1. 快速理解 This 指向位置
2. 綁定 This
3. 箭頭函式與 This
在 JavaScript 裡除了閉包, This 也是前幾個難懂的概念
雖然講解 This 的文章很多,有各種例子、各種解釋的切入點
但多數文章我都看得一頭霧水
今天我就特別選一種我覺得最好懂的方式講解 This,也順便帶到 ES6 的箭頭函式
什麼是 This
當 JavaScript 代碼執行一段可執行代碼 (executable code) 時,會創建對應的執行環境 (execution context)
對於每個執行環境,都有三個重要屬性
- 變量對象 (Variable object,VO)
- 作用域鏈 (Scope chain)
- This
如果對 VO 和 Scope chain 感到陌生,可以閱讀這篇文章
this 是 函數執行時 生成的內部物件,就像 VO 的概念
隨著函數被調用環境的不同,this 指向的值也會不同
眾所周知 JS 的作用域是詞法作用域
詞法作用域意即程式 正要執行前 就已被決定的作用範圍
但 this 他是 在函數執行時,依照調用環境生成的物件
因此他更像是動態作用域的概念
大部分的情況下,他指向的是函數被調用的調用點,也就是呼叫函數的物件,並非函數本身
常用的用法就像是以下的例子:
var getGender = function(){
return this.gender;
};var people1 = {
gender: 'female',
getGender: getGender // 調用點
};
var people2 = {
gender: 'male',
getGender: getGender // 調用點
};console.log( people1.getGender() ); // 'female'
console.log( people2.getGender() ); // 'male'
乍看之下沒有特別的感覺,但今天換一個例子就稍嫌複雜了
var foo = function() {
this.count++;
};
foo.count = 0;
for( var i = 0; i < 5; i++ ) {
foo();
}
感覺 foo.count
應該要是 5 ,結果答案卻是 0
這就是 this 讓人搞不懂的地方
搞不懂 this 除了在寫 code 的過程不懂自己在幹嘛
也會影響 debug 速度
看來快速理解 this 勢在必行 = =!
快速理解 This 指向的位置
在學習快速理解之前,我們要先理解為什麼會有 this
this 主要用於物件導向
在物件導向中,this 單純就是指向 實例本身
class Car {
setName(name) {
this.name = name
}
getName() {
return this.name
}
}
const myCar = new Car() myCar.setName('hello') console.log(myCar.getName()) // hello
他的概念很單純,本應該是易於理解的概念才是
但今天的問題是在 JavaScript 裡面,在任何地方都可以存取到 this
所以在 JS 的 this 跟其他語言慣用的 this 有了差異,這就是為什麼 this 難懂的原因
了解 this 真正的使用方式後,我們先從 ECMAScript 規範的角度了解 JS 世界無所不在的 this
他的概念其實不會很複雜,不要被規範兩個字嚇到了
在 ECMAScript 規範中有兩種類型:
- 語言類型
也就是我們熟知的 Undefined, Null, Boolean, String, Number, 和 Object - 規範類型
用來描述 ECMAScript 語言結構和 ECMAScript 語言類型,他有很多種,我們今天主要談的是:Reference
今天我們就要用 Reference 講解 this 指向的位置!
何謂 Reference
Reference 是什麼?有這種型別嗎?!
Reference 其實可以說是一個抽象的概念,不存在程式碼之中
他是 用來解釋語言底層行為邏輯而生的
Reference 由以下三點組成:
- base value
屬性所在的 Object 或是 EnvironmentRecord - referenced name
屬性名稱 - strict reference
舉個例子:
因為 foo
是直接宣告在全域,所以 base
是 EnvironmentRecord
var foo = 1;
// foo 對應的 Reference 是:
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};
這個例子就不一樣了
我們在全域調用了 foo
物件的 bar
方法, bar
的 base
是 foo
var foo = {
bar: function () {
return this;
}
};
foo.bar(); // foo
// bar 對應的 Reference 是:
var BarReference = {
base: foo,
propertyName: 'bar',
strict: false
};
從 Reference 判斷 this
其實 this 的值大部分就是 Reference 中 base
指向的位置
可能會有人覺得:知道函數是在全域還是在物件中,就能判斷 this 的值啦
但從上面 count 的例子,就知道事情不是這麼的單純
畢竟 this 與我們熟知的詞法作用域不同,他是依據調用點決定他指向的位置的
var foo = function() {
this.count++;
};
foo.count = 0;
for( var i = 0; i < 5; i++ ) {
foo();
}console.log(foo.count); // 0
到底為什麼 foo.count
為 0 呢?
以上面的例子來說,Reference 被定義的瞬間是執行 foo()
的時候
foo()
不是任何物件所屬的方法,他在全域
// foo 對應的 Reference 是:var FooReference = {
base: EnvironmentRecord,
propertyName: 'foo',
strict: false
};
因此 this 其實就是指向全域的 count 屬性,但全域並沒有 count
所以當 foo
在執行的時候都是在對 undefined
+1
因此 foo.count
為 0
Reference 怎麼定義 this 的值呢?
在規範中 11.2.3 Function Calls 就有解釋 this 值是如何被決定的
我整理了一下他人擷取的流程,大概就是這樣:
- 計算 MemberExpression 的結果賦值給 ref
- 判斷 ref 是不是一個 Reference 類型
- 如果 ref 是 Reference, 那麼 this 的值為
base
但如果base
是EnvironmentRecord
,那麼 this 的值為window
(瀏覽器,也就是指向全域)或global
(Node 環境)或undefined
(嚴格模式) - 如果 ref 不是 Reference,那麼 this 的值為
undefined
上述的過程的重點無非就是兩個:MemberExpression 和 Reference
- MemberExpression 值判斷
MemberExpression 就是:表達式,舉凡函數、變量、屬性訪問、創建實例…..皆是
簡單理解的話,其實就是()
左邊的部分
例如:foo.bar()
,MemberExpression 就是foo.bar
通常只要不要太奇怪,像是:(foo.bar = foo.bar)()
,牽扯到取值
他的 MemberExpression 有被處理過,返回的值就不是 Reference 了, this 為undefined
- Reference
base
值判斷
呼叫函式的當下所屬的物件
因此一般情況下,我們可以跳過 MemberExpression 值判斷
直接判斷 Reference 即可!
儘管如此 this 值還是很容易混淆
再看一個例子:
var bar = function() {
console.log( this.a );
};var foo = function() {
var a = 123;
this.bar();
};foo(); // undefined
還不熟悉 this 的你或許會誤判 console 出來的會是 123
讓我們來一一拆解!
foo()
在全域被調用,因此他的 Reference 為:
// foo 對應的 Reference 是:var FooReference = {
base: EnvironmentRecord,
propertyName: 'foo',
strict: false
};
因為在瀏覽器環境 this 為 window
this.bar()
也就是指 window.bar()
目前為止,都還可以理解
但 this.a
為什麼不是 123 呢?
仔細看在 foo()
裡的 this.bar()
由於 this 為 window
我們就先將this.bar()
替換成 window.bar()
如此一來 bar 的 Reference 就應該是:
// bar 對應的 Reference 是:var BarReference = {
base: EnvironmentRecord,
propertyName: 'bar',
strict: false
};
this 仍是 window
,全域環境並沒有 window.a
這個變數,因此回傳 undefined
進階一點,如果我們把例子改一下:
var bar = function() {
console.log( this.a );
};var foo = function() {
var a = 123;
this.bar();
};var obj = {
a: 'hi',
fooObj: foo,
bar: bar
}var fooMethod = obj.fooObj;foo(); // undefined
obj.fooObj(); // 'hi'
fooMethod(); // undefined
這次我們這次我們將 foo 引入 obj
結果就和一開始在全域完全不同了
因為 fooObj()
是 obj
內的 method,因此他的 Reference 是:
// fooObj 對應的 Reference 是:var BarReference = {
base: obj,
propertyName: 'fooObj',
strict: false
};
this 值為 obj,因此 this.bar()
即為 obj.bar()
this.a
是 obj.a
並非上一個例子指向的 window.a
但為什麼
fooMethod()
是undefined
呢?
我們會對此混淆,是因為被他的值所影響
會認為說,他是 obj.fooObj()
,所以他的 this 理當是 obj
才是
但事實上,在執行 fooMethod()
時,不管他的值為何
他就是一個宣告在全域的變數,他的 base
就是 EnvironmentRecord
// fooMethod 對應的 Reference 是:var fooMethodReference = {
base: EnvironmentRecord,
propertyName: 'fooMethod',
strict: false
};
this 值為 window
, this.bar()
即為 window.bar()
this.a
是 window.a
,因此找不到值
使用 call() 判斷 this
如果從 Reference 判斷還是有點太模糊
我也有看到有另外一種理解的方法是用 call()
的方式
函式在 JavaScript 中,其實是一個可被呼叫(Callable)的 物件
除了可執行、擁有例如 apply()
、bind()
、call()
之類的方法外
也擁有一些函式特有的屬性,像是:
name
:函式的名稱length
:可接收參數的數量
function 有三種呼叫方式:
func(p1, p2)
obj.child.method(p1, p2)
func.call(context, p1, p2)
// 這裡不談apply()
其中 call()
的寫法是可以與 1 2 的寫法互換的!
func() // 1
func.call(undefined) // 2
func.call() // 3// 1 = 2 = 3
// this 為 windows
仔細看一下 call()
,他的結構是這樣子的:
func.call(context, 參數1, 參數2)
其中 this 就是我們 call 一個函數時所傳的 context
!
因此,將我們慣用的函數寫法轉成 call()
就能知道 this 了
但要怎麼知道 context
的值呢?
直接把
func()
前面的值搬過去就可以了!
也就是說
func(obj)
=>func.call(undefined,obj)
this =>window
obj.foo()
=>obj.foo.call(obj)
this =>obj
若傳的 context
是 null
或 undefined
對象就是默認的
windowcontext
(嚴格模式下默認 context
是 undefined
)
我們再複習一次 this 的要點:
- this 是函數執行時,生成的內部物件,就像 VO 的概念
- 隨著函數被調用環境的不同,this 指向的值也會不同
- 多數的情況下, this 代表的就是呼叫函數的物件
簡而言之,要判斷 this 就是拋開一切直接看函式引用點
a()
=> a 前面沒有東西,他在全域!!!!this 為undefined
obj.a()
=> a 前面有東西,他是obj
內的方法!!!this 為obj
b.obj.a()
=> a 前面有東西,他是b.obj
內的方法!!!this 為b.obj
如此一來,我們就能看懂 this、寫對 this 了!
綁定 This
既然我們了解了 this 怎麼運作那又為什麼要操作 this 值呢?
原因是我們可能在某些情況下丟失 this 值,像是:
- React
我們一般會在 React 的 constructor 裡面綁定好 method,否則無法確保 this 是指向我們想要他指向的 instance
這是 React 原碼的問題導致我們必須綁定他,常規的 this 寫法是會丟失的 - 監聽事件
監聽事件 callback 中的 this 指向的是事件的元素
但一旦進到 event loop 的 queue 中等待被觸發,他在執行時 this 就已經丟失了,不再是指向預設的事件元素
知道了會有需要操作 this 的情形,那要怎麼綁定 this 呢?
剛剛講到 function 這個物件有三種方法可以調用,分別是:
func.call(context, 參數1, 參數2)
func.apply(context,[參數1, 參數2])
func.bind(context, 參數1, 參數2)
其中, call()
和 apply()
是明確綁定, bind()
是硬綁定
明確綁定
call()
和 apply()
第一個參數是 this
指向的值
若是傳入基本類型的值(String、Number、Boolean)則會包裝在他的對象類型中(new String(..)
、new Boolean(..)
)
他們的用法一樣,只是參數不同:
call()
傳遞的參數要是基本類型apply()
傳遞的參數要是 array
那我們先以 call()
舉例他的使用方法:
var bar = function() {
console.log( this.a );
};
var foo = function() {
var a = 123;
this.bar();
};
var obj = {
a: 'hi',
fooObj: foo,
bar: bar
}// 等於 foo.call(undefined),全域沒有 a 所以 undefined
foo(); // undefined// 若綁定 this 指向 obj,obj 有 a 所以是 hi
foo.call(obj) // 'hi'
乍看之下 call()
和 apply()
可以解決剛剛講到的問題
但事實上,他只能指定無法解決丟失的問題,還是可能被改變、覆蓋
因此我們的重點會放在 bind()
上!
硬綁定
起初在解決丟失問題時
是透過新增一個 function ,明確綁定 this 值解決
function foo() {
console.log( this.a );
}var obj = {
a: 2
};var bar = function() {
foo.call( obj );
};bar(); // 2
setTimeout( bar, 100 ); // 2
後來因為真的太常使用!因此出現了 bind()
這個語法糖!
bind()
與 call()
感覺用法差不多,第一個參數都是 this 值,後面接參數
但如果還記得上面的歷史故事
他們差別就在於: bind()
會回傳一個函式
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
bind()
會在函式呼叫前先將 this 綁定至某個物件
使它不管怎麼被呼叫都能有固定的 this
與事後更改 this 值的明確綁定不同
更能確保 this 的值不會丟失,比明確綁定更加可靠!
箭頭函式與 This
從 ES6 開始,新增了一種叫做 「箭頭函式」 的表示式
const func = function (x) { return x + 1 }
相當於
const func = (x) => { return x + 1 }
為什麼會特別提到他呢?
因為箭頭函式有兩個重要的特性:
- 更簡短的函式寫法
- this 變數強制綁定
箭頭函數的 this 為執行函數的位置
不是指所屬物件,而是他本身被呼叫的位置
聽起來跟我們所了解的知識完全不一樣
這是因為箭頭函式根本沒有 this,所以說是綁定也有點勉強?!
因此無論是使用 'use strict'
或是 bind()
都無法改變 this 的內容,也不能作為物件建構子 (constructor)來使用
在宣告它的地方的 this 是什麼,它的 this 就是什麼
例如:
var tmp = 'a'var obj = {
tmp: 'b',
func: () => console.log(this.tmp)
}console.log(tmp) // a
obj.func() // a
由於 obj.func
是在全域環境中建立,this 就被綁定到全域環境中
因此兩個執行結果都是全域變數 tmp
的值 a
要記得箭頭函式是沒有 this 的
所以在使用箭頭函數時,務必要小心考量
像是在 React 是可以使用箭頭函數來綁定實例的
但 Vue 使用箭頭函數,反而會讓原本指向實例的 this 丟失
希望可以透過以上的舉例讓大家更了解 this 及如何綁定 this !
拍個手讓我知道,這個文章對你們有幫助 ♥(´∀` )人