this 到底在幹嘛?
this 為什麼要學?他很好用嗎?其實 this 可以方便我們處理很多狀況,舉個例來說:
老師要改班上同學的考卷,這個班的同學假設有 10 個人,老師可能需要:
看 甲同學 的考卷: {
看 甲同學 考卷的第一題答案對不對,對了打勾,錯了打叉
看 甲同學 考卷的第二提答案對不對,對了打勾,錯了打叉
.
.
.
把 甲同學 最後的成績算出來,並打上分數
}
================================================
看 乙同學 的考卷: {
看 乙同學 考卷的第一題答案對不對,對了打勾,錯了打叉
看 乙同學 考卷的第二提答案對不對,對了打勾,錯了打叉
.
.
.
把 乙同學 最後的成績算出來,並打上分數
}
================================================
看 丙同學 的考卷: {
...
}
.
.
.
================================================
看 第 10 位同學 的考卷: {
...
}
因為 10 位同學的考券都不相同,所以一樣的動作要寫 10 遍。
假如我們能把改考卷的動作,套用到不同的學生身上,這樣只需要寫一遍,豈不是方便很多?
像下面這樣:
看 this同學 的考卷: {
看 this同學 考卷的第一題答案對不對,對了打勾,錯了打叉
看 this同學 考卷的第二提答案對不對,對了打勾,錯了打叉
.
.
.
把 this同學 最後的成績算出來,並打上分數
}
這份改考卷流程,可以讓我們在拿到不同同學的考卷,自動將 this
設定為該同學
- 改 甲同學 的考卷時,this 就是甲同學
- 改 乙同學 的考卷時,this 就是乙同學
這樣一來,就可以看出 this
的方便性了。
所以 this 可以讓在不確定角色的情況,先留一個角色的位子在哪邊,等到有角色要套入的時候,自動將 this 變為該角色。
以物件導向的例子來看:
class Dog(name) {
this.name = name // 還不確定角色,先用 this 留一個位置給他
}
// 當有角色要套入的時候,自動將 this 變為該角色
const TaiwanDog = new Dog(Peter) // this 就會是 TaiwanDog
const KoreanDog = new Dog(Marry) // this 就會是 KoreanDog
先有個概念,知道為什麼我們需要搞懂 this,再來探討如何分辨 this 的值會是多少。
this 是什麼? 為什麼這麼複雜?
首先,可以先看看文章:淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂,讀完內文先記得一件事: 「 一但脫離了物件,就不太需要關注 this 的值,因為沒什麼意義 」。
沒什麼太大意義的 this
像下面這段程式碼,在一個 function 裡面印出 this 看看會是什麼
function hello(){
console.log(this)
}
hello()
在這種情況下我會跟你說 this 沒有任何意義,this 的值在瀏覽器底下就會是 window,在 node.js 底下會是 global,如果是在嚴格模式,this 的值就會是 undefined。
- 在非物件導向的環境下,this 會是預設值,而 this 的預設值會依據環境的不同而有所改變。
- node.js : global
- 瀏覽器 : window
- 嚴格模式 : undefined ( node.js 跟瀏覽器都一樣 )
- 想要把 this 統一的話,其實可以改成嚴格模式: 'use strict';,這樣 this 就會變成 undefined
更改 this 的值
僅管 this 可能有預設的值,但我們可以透過一些方法來改它。這改的方法也很簡單,一共有三種。
- call()
- apply()
- bind()
☞ 第一種跟第二種方法: call / apply
可以傳參數進去,參數傳什麼,裡面 this 的值就會是什麼。儘管原本已經有 this,也依然會被這種方法給覆蓋掉
call & apply
class Car {
hello() {
console.log(this)
}
}
const myCar = new Car()
myCar.hello() // myCar instance
myCar.hello.call('yaaaa') // yaaaa
myCar.hello.apply('nonono') // nonono
原本 this 的值應該要是 myCar 這個 instance,可是卻被我們在使用 call 時傳進去的參數給覆蓋掉了。
'use strict';
function test(a, b, c) {
console.log('this: ', this);
console.log(a, b, c);
}
test(1, 2, 3); // this: undefined
test.call('I am call', 1, 2, 3); // this: I am call
test.apply('I am apply', [1, 2, 3]); // this: I am apply
- 第一個輸出:
- 預設值用嚴格模式下是 undefined
- 第二個輸出:
.call(<this>, <arugemt_1, arugemt_2...>)
改變 this 為字串I am call
- 第三個輸出:
.apply(<this>, [<arugemt_1, arugemt_2...>])
改變 this 為字串I am apply
,第二個參數為一個陣列,裡面放參數
所以看得出來 call & apply 的差別,其實就只有參數是不是 array 的形式而已。
☞ 第三種方法,bind : 回傳一個指定 this 的 function
如果只想要固定 this 值、沒有要立刻執行,這時就可以用 bind,用 function bind 會回傳固定 this 的 function 本身。
這樣就不用擔心因為呼叫當下而改變 this,且之後就算用 call or apply 也改變不了 this。
const obj = {
sayThis: function() {
console.log(this);
}
}
const say = obj.sayThis.bind('hello'); // 將 sayThis 函式中的 this 綁定為 hello,並且賦值到 say 這個變數
say(); // => 輸出 [String: 'hello']
say.call('who'); // => 還是輸出 [String: 'hello']
綜合以上兩個重點:
- 在物件以外的 this 基本上沒有任何意義,硬要輸出的話會給個預設值
- 可以用 call、apply 與 bind 改變 this 的值
怎麼判斷 this 的數值?
this 的值跟作用域跟程式碼的位置在哪裡完全無關,只跟「你如何呼叫」有關
const obj = {
value: 1,
hello: function() {
console.log(this.value)
}
}
obj.hello() // 1
const hey = obj.hello
hey() // undefined
明明就是同一個函式,但是呼叫的方式不同,this 的值就會不同
判斷 this 的方式:
在呼叫 function 以前是什麼東西,this 就會是什麼。
- obj.hello() 的 this 就是 obj
- hey()前面沒有東西,所以 this 就是預設值
以下兩個範例可以給大家練習:
const obj = {
value: 1,
hello: function() {
console.log(this.value)
},
inner: {
value: 2,
hello: function() {
console.log(this.value)
}
}
}
const obj2 = obj.inner
const hello = obj.inner.hello
obj.inner.hello()
obj2.hello()
hello()
公佈答案:
obj.inner.hello()
的 this 是 obj.inner,所以執行結果是2
obj2.hello()
的 this 是 obj2,obj2 又等於 obj.inner,所以執行結果也是2
hello()
的 this 是預設值,因為 window 底下沒有 value,所以執行結果是 undefined
var x = 10
var obj = {
x: 20,
fn: function() {
var test = function() {
console.log(this.x)
}
test()
}
}
obj.fn()
這題可能大家會以為 this 是 obj,因為感覺 obj 呼叫了 fn() 函式,所以 this 應該是 obj。
但其實有被「呼叫」到真正跟 this 有關的 function 是 test()
所以其實 test() 被呼叫的時候,this 是預設值,也就是 window,而 window.x 得到的結果是 10,10 才是這題的答案。
所以一定要記住判斷 this 最關鍵的原則:要看 this,就看這個函式「怎麽」被呼叫
this 的例外情況
☞例外( 一 ) 事件監聽中的 this
DOM 物件綁定某種事件時,this 會變成綁定的 DOM 元素本身:
document.querySeletor('.btn').addEventListener('click', function(){
console.log(this); // => btn 這個元素
});
☞例外( 二 )箭頭函示 arrow function 中的 this
之前不是說過 this 跟在哪裡定義無關、而是跟在哪裡呼叫有關嗎?
但有個例外喔,就是箭頭函式。
example1: 一般例子
'use strict';
class Test {
run() {
console.log('run: ', this);
setTimeout(function() {
console.log('setTimout: ', this);
}, 1000);
}
}
const test = new Test();
test.run();
// run: Test {}
// setTimout: undefined ( 一秒後 )
以上例子看起來很正常,沒有問題,那如果換成 arrow function 呢? 會發現 setTimout 的 this 變成 Test 了。
example2: 改成箭頭函式 arrow function
'use strict';
class Test {
run() {
console.log('run: ', this);
setTimeout(() => {
console.log('setTimout: ', this);
}, 1000);
}
}
const test = new Test();
test.run();
// run: Test {}
// setTimout: Test {} ( 一秒後 )
this 在 arrow function 中,跟在哪裡定義有關,此時的 this 有點像變數的行為,會依照作用域去抓外部的 this,依據上一層的 run: this 是什麼,setTimout: this 就會是什麼。
example3: 用 bind 改變 this , arrow function 裡面的 this 也會跟著換
要驗證的話,可以把 run function 的 this 固定成一個字串 hello,所以此處的 setTimout: this 也會跟著變成 hello。
class Test {
run() {
console.log('run: ', this);
setTimeout(() => {
console.log('setTimout: ', this);
}, 1000);
}
}
const test = new Test();
const newTest = test.run.bind('hello'); // => 把 this 固定成 hello
newTest();
// run: hello
// setTimout: hello ( 一秒後 )
this 到底是什麼?
有很多因素要考慮:
- 非物件導向底下,要看執行環境有不同預設值:
- node.js : global
- 瀏覽器 : window
- 嚴格模式 : undefined ( node.js 跟瀏覽器都一樣 )
- 物件導向中, this 就是 new 出來的 instance
- DOM 事件綁定時, this 為綁定的 DOM 物件
- 單純在物件底下,this 跟如何呼叫 function 有關
- 如果是在物件本身上呼叫,this 為物件本身
- 把 function 抽出來呼叫,this 則為預設值 ( global or window or undefined )
- arrow function 中,根據外部的 this 是什麼而決定
如何改變 this:
- bind : 將 this 值固定下來,回傳 function
- call or apply : 指定 this,傳入參數並執行 function
小測驗
function log() {
console.log(this);
}
var a = { name: 'a', log: log };
var b = { name: 'b', log: log };
log(); // => global or window
a.log(); // => a 本身
b.log.apply(a); // => a 本身
const newLog = b.log.bind(b);
newLog.apply(a); // => b 本身