本篇為 [JS201] 進階 JavaScript:那些你一直搞不懂的地方 這門課程的學習筆記。如有錯誤歡迎指正!
學習目標:
P1 你知道大部分情況下 this 的值是什麼
P2 你知道物件導向的基本概念(類別、實體、繼承、封裝)
從物件導向到 this
在上一篇JavaScript 進階 - 物件導向 & Prototype 筆記中,其實我們就有提到 this 相關概念。this 的主要用途就在於物件導向,用來指哪個 instance 在呼叫這個 function,透過 this 我們能夠進一步操作。
那如果是在和物件導向無關的情況下,呼叫 this 會代表什麼?
Global context 下的 this
如果在全域環境下呼叫 this,根據不同執行環境(瀏覽器或 node.js),預設值可能會是 global 或 window:
在 node.js 運行
function test() {
console.log(this); // Object [global] { ...}
console.log(this === global) // true
}
test();
在瀏覽器運行
function test() {
console.log(this); // Window { ...}
console.log(this === window) // true
}
test();
use strict 嚴格模式
在不需要的地方呼叫 this,卻還是會有預設值 global 或 window。這時候,只要設定 'use strict'
(嚴格模式)就能避免,this 的預設值會是 undefined:
'use strict';
function test() {
console.log(this);
}
test(); // undefined
DOM 元素中的 this
此外,對 DOM 元素進行事件監聽時,this 就代表當下操作的元素,其實非常直覺。例如監聽按鈕的點擊事件,那 this 就會是那個按鈕:
document.querySelector('.btn').addEventListener('click', function() {
console.log(this) // this: 觸發這個 funciont 的物件
})
因此我們可以說,除了在物件導向跟 DOM 之外,this 是沒有意義的。
Function context 下的 this
在 function 裡面時,this 的值是什麼取決於該 function 是怎麼被呼叫的。
也就是說,function 沒有被呼叫的時候,預設值就會是 window, global 或 undefined。一旦某個 function 被呼叫時,裡面的 this 就可以理解成,是指向誰在 call 這個 function:
var obj = {
foo: function(){
console.log(this)
}
}
var bar = obj.foo
obj.foo() // foo(): obj 在 call,obj 就是 this
bar() // bar(): 沒人 call,window 就是 this
其中,bar()
這種呼叫函式的方法,其實是 .call()
的語法糖,要更詳細了解呼叫 function 背後在做的事,就要深入理解 .call()
、.apply()
、bind()
的行為。
另外兩種呼叫 function 的方法:call 與 apply
.call()
第一個值傳入什麼 this 就是什麼。
'use strict';
function test() {
console.log(this);
}
test.call(123); // 123
test.call({}); // {}
test.call([1]); // [ 1 ]
test.call('abc'); // 'abc'
.apply()
和 .call()
一樣,第一個值傳入什麼 this 就是什麼,差異在於第二個參數會接一個 array。
可由下方範例來比較三種方式呼叫 function 的差異:
'use strict'; // 嚴格模式
function test(a, b, c) {
console.log(this);
console.log(a, b, c);
}
test(1, 2, 3)
// undefined
// 1 2 3
test.call(123, 1, 2, 3);
// 123
// 1 2 3
test.apply(123, [1, 2, 3]);
// 123
// 1 2 3
透過 .call
和 .apply
的第一個參數,我們可以設定 function 中 this 的值。兩者差別在於,一個可以傳入無限的參數,另外一個只能傳入兩個參數;一個要用 array 帶入參數,另一個則否。
此外,我們也可以把一般呼叫 function 的方式都轉成 call 的形式:
const obj = {
value: 1,
hello: function() {
console.log(this.value)
}
}
obj.hello() // 1
obj.hello.call(obj) // 1
再舉一個例子,透過 call 來呼叫 function,我們就能夠指定 this 的值:
function hello() {
console.log(this)
}
var a = {
value: 1, hello
}
var b = {
value: 2, hello
}
hello() // global
a.hello() // a { value: 1, hello}
b.hello.call(a) // a { value: 1, hello}
如何判斷 this 的值
因為 this 是針對物件導向設計的,從以下範例,可知道 this 就是 obj 物件本身:
'use strict';
var obj = {
a: 10,
test: function(a, b, c) {
console.log(this); // obj
}
}
obj.test();
// { a: 10, test: [Function: test] } === obj
重點就是,this 的值和程式碼在哪無關,而是和怎麼呼叫有關係:
'use strict';
var obj = {
a: 10,
test: function (a, b, c) {
console.log(this); // obj
}
}
obj.test(); // { a: 10, test: [Function: test] }
var func = obj.test; // 使 func 等同於 obj.test()
func() // undefined
結果卻會得到不同結果,這是因為 obj 呼叫 function,所以 this 會指向 obj;另一方面,this 所在的 instance 中,沒有被 func 呼叫,所以會得到 undefined。
我們可以透過 .call()
來看看 this 指的是什麼:
'use strict';
var obj = {
a: 10,
inner: {
test: function(a, b, c) {
console.log(this);
}
}
}
obj.inner.test();
// { test: [Function: test] }
// 改用 .call() 呼叫
obj.inner.test.call(obj);
// { a: 10, inner: { test: [Function: test] } }
obj.inner.test.call(obj.inner);
// { test: [Function: test] }
.bind():強制綁定 this
除了 .call()
和 .apply()
,我們還可以利用 .bind()
來強制綁定 this 的值,但需注意的是,一旦綁定之後就不能再用別的方法進行更改。
將前面提到的例子修改成 .bind(),這裡需注意 .bind() 會是直接回傳一個 function:
'use strict';
var obj = {
a: 10,
test: function () {
console.log(this);
}
}
// 第一種寫法:直接呼叫
obj.test.bind(obj)();
// { a: 10, test: [Function: test] }
// 第二種寫法:先宣告,再呼叫
const bindTest = obj.test.bind(obj);
bindTest();
// { a: 10, test: [Function: test] }
const bindTestA = obj.test.bind(obj.test);
bindTestA();
// [Function: test]
const bindTestB = obj.test.bind("test B");
bindTestB();
// test B
const bindTestC = obj.test.bind("test C");
bindTestC.call('CCC');
// test C
利用 .bind()
就可以綁定 this 的值,也就是鎖定想要呼叫的區域,即便後面再 .call()
指定 this 的值也無法改變。
牛刀小試
以下範例預設在嚴格模式進行:
'use strict';
function log() {
console.log(this);
}
var a = { a: 1, log: log };
var b = { a: 2, log: log };
log();
a.log();
b.log.apply(a);
思考步驟:
1. log();
// undefined
// 在嚴格模式下,如果在非物件導向的情況呼叫 this,其值會是 undeined
2. a.log();
// { a: 1, log: [Function: log] }
// 等於 a.log.call(a),所以 this 就是 a 本身
3. b.log.apply(a);
// { a: 1, log: [Function: log] }
// .apply() 會把 this 的值蓋過,所以 this 也會是 a 本身
arrow function 的 this
arrow function 本身其實沒有 this,在 arrow function 裡面的 this,其實就和 arrow function 外部的 this 是同一個東西。
先以一般宣告 function 為例:
class Test {
run() {
console.log('run this', this);
setTimeout(function() {
console.log(this);
}, 100)
}
}
const t = new Test();
t.run();
在瀏覽器上的運行結果,會發現第二個 this 等於全域的 Window:
也就是說,在 100 毫秒後才呼叫,其實作用就等同於在全域呼叫這個 function:
function() {
console.log(this);
}
但如果改成箭頭函式,結果就會不同了:
class Test {
run() {
console.log('run this', this);
setTimeout( () => {
console.log(this);
}, 100)
}
}
const t= new Test();
t.run();
在瀏覽器上的運行結果,此時兩個 this 會變相同:
這就是箭頭函式的特性,和箭頭函式中的 this 怎麼呼叫沒關係。而是類似 Scope(作用域)的機制,看程式碼定義在哪裡,就會利用定義的 block 來呈現這個 this 的值。
結語
瞭解到物件導向的相關概念後,接著要理解 this 是什麼就沒那麼困難了。或許是因為在實際學 JacaScript 以前,就預設 this 是很難是高手在用的東西,透過慢慢理解物件導向與 this 的關聯,以及如何判斷 this 的值,似乎也感覺到自己的進化,對於未知的恐懼總是需要克服的。
關於 this 的重點,就是記得 this 的值和程式碼在哪無關,而是和怎麼呼叫有關係。
總結前面提到的觀念,其實 this 大致可分成四種綁定方式:
- 默認綁定
在和物件導向無關的情況下,this 會被指定為全域物件。又依照執行環境不同,其值會是 global 或 window,而在嚴格模式下會是 undefined:
function test() {
console.log(this); // Window
}
test();
- 隱式綁定
若在 function 中, this 有被某物件指定為屬性並呼叫,this 就是呼叫 function 的物件。以下方範例來說 this 就是 obj:
function func() {
console.log(this.a);
}
var obj = {
a: 4,
test: func
};
obj.test(); // 4
- 顯示綁定
若是透過 .call()
、.apply()
或 .bind()
方式指定 this,this 就會是傳入的參數:
var obj = {
a: 10,
test: function () {
console.log(this);
}
}
obj.test.call(obj)
obj.test.apply(obj)
// 第一種寫法:直接呼叫 function
obj.test.bind(obj)();
// 第二種寫法:先宣告,再呼叫
const bindTest = obj.test.bind(obj);
bindTest();
// 均印出: { a: 10, test: [Function: test] }
- new 綁定
透過建構函式 new 出一個 instance,this 就會是 instance 物件本身:
class Dog {
constructor(name) {
this.name = name;
console.log(this); // Dog {name: "dog A"}
console.log(this.name); // dog A
}
}
var a = new Dog('dog A');
- 例外:箭頭函式中的 this 是看程式碼定義在哪,和怎麼呼叫沒關係。
參考資料:
- 淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂
- 前端中階:JS令人搞不懂的地方-this
- JavaScript: Object-oriented JavaScript, Prototype Chain & This
- 關於this綁定的四種方式