在之前的文章當中曾經提到過 JavaScript 中的物件有一個特別的機制:傳參考(Called by reference),如果正確性再高一點的話,則可以稱之為傳共享(Called by sharing)。
在 JavaScript 中的物件,被生成後會出現一個參考位置,如果有其他變數或是在使用函式運算的過程中,會修改到這個物件,所有沿用(例如:拿這個物件去賦予給其他變數)、有使用到這個物件的地方,都會因為這個物件被改動,而連帶被受到影響,這樣的機制就稱為傳共享,因為所有的變數其實都共用了這個物件。
在理解 JavaScript 中 this
以前,理解 JavaScript 中的物件與傳共享機制是非常必要的,如果你已經夠熟悉這些機制的話就讓我們看下去吧。
註:這邊所提到所有內容都是基於在瀏覽器中執行的情境為主,在 Node.js 環境中的
this
概念略有不同。
**this
是一個被 JavaScript 保留的關鍵字,是一種抽象概念上的參考物件,也就是說:當我們使用到了 this
,這個 this
實際上指的某個參考物件。**
這裡為了避免誤會,我再講陳述的清楚一點: this
實際上指的某個參考物件,並不是某個物件,因為同一個物件可以被很多變數沿用、甚至在短時間內被不同的函式操作。
理解完 this
是一種參考物件後,再來聊聊其最重要的特性:會受範圍鏈(Scope Chain)以及被呼叫的位置(要根據上下文決定)來決定這個參考物件到底會是什麼。
之前在講 var
、let
與 const
的差異時,也有提到所謂範圍鏈的概念,如果不是那麼清楚的人可以參考看看這篇文章。
在全域底下的 this
其實就是 window
物件,不過在嚴格模式中是取用不到 window
物件的(而且會報錯),所以同理上不會取用到 this
的值,不過並不會報錯,而是會回傳 undefined
:
'use strict';
console.log(this);
// -> undefined
講到這邊,我自己本人目前接觸前端已經三年了,是沒有遇到有需要使用到全域 this
的狀況,看一些 Youtube 上的超級資深網頁開發者會分享,以前為了方便所以會把一些方法實作後掛載到 window
上,不過前後端分離後這樣的時代也漸漸遠去,還需要理解這個東西的原因在於「理解 JavaScript 」機制,以及面試滿常考到的。
JavaScript 是一種擁有多重設計典範的語言,我們可以在 JavaScript 撰寫物件導向風格的程式碼,也可以撰寫函式程式設計方格的程式碼,而 this
就比較偏向前者的風格。
我們可以透過在函式中使用 this
取用到物件的狀態,舉例來說:
const person = {
name: 'Jack',
age: 1,
gender: 'male',
print: function() {
console.log(`Jack is ${this.age} years old.`)
}
};
person.print();
// -> Jack is 1 years old.
由於受範圍鏈影響, this
實際上會指向到哪一層物件,是透過向外層查找的方式來決定的,萬一沒有外層的那一層物件怎麼辦?
function print () {
console.log(`Jack is ${this.age} years old.`)
};
print();
/ -> ?
那就會是 undefined
,因為此時外層並沒有物件,當然這也不是絕對的答案,在不嚴格的模式下,如果你的全域物件上有 age
這個屬性,也許答案會有所不同。
age = 11;
function print () {
console.log(`Jack is ${this.age} years old.`)
};
print();
/ -> ?
看到這裡如果你會覺得有夠詭異的話,我推薦你閱讀我之前寫的有關於 JavaScript 變數、全域屬性的這篇文章。
在先前的範例中,可以發現幾乎都是使用「函式陳述式(Function Statement)」,原因是箭頭函式其實被設計成沒有 this
的存在,這樣的設計是為了在一些 Callback 的情境中可以保留指定的 this
狀態,例如:
function Timer() {
this.seconds = 0;
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
}
var timer = new Timer();
因為箭頭函式本身沒有 this
,所以會透過範圍鏈往上查找最近的 this
,簡單來說,可以透過不同函式的運作方式,來決定 this
的影響範疇。
個人是覺得這樣的機制是沒有到很方便,甚至我也不太會用到,也是一種屬於了解 JavaScript 這門語言的切入點。
this
除了會受範圍鏈影響外,也可以透過一些方式來綁定參考物件做到動態的參考物件指向,通常是透過 call、apply 與 bind 這三個函式來做到,接著就來看看這三者有什麼樣的差異:
call()
:是一個可以傳入多個參數的函式方法,主要用於動態指向參考物件,第一個參數帶入參考物件,第二個以後的參數則是函式本身,語法為:fun.call(thisArg[, arg1[, arg2[, ...]]])
舉例來說,透過 call()
方法,我們將 print()
函式的參考物件指給另外一個 person
物件。
const person = {
name: 'Jack',
age: 1,
gender: 'male',
};
function print (time) {
console.log(`${time}: ${this.name} is ${this.age} years old.`)
};
print.call(person, '2023/01/01');
// -> 2023/01/01: Jack is 1 years old.
apply()
:是一個可以傳入兩個參數的函式方法,主要用於動態指向參考物件,第一個參數帶入參考物件,第二個的參數是陣列,依序帶入此函式所需要的參數,語法為:fun.apply(thisArg, [argsArray])
我們可以用 apply
改寫上方的範例:
const person = {
name: 'Jack',
age: 1,
gender: 'male',
};
function print (time) {
console.log(`${time}: ${this.name} is ${this.age} years old.`)
};
print.apply(person, ['2023/01/01']);
// -> 2023/01/01: Jack is 1 years old.
bind()
:使用方式跟 call 很像,只是會再額外回傳一個新的函式,屬於靜態綁定 this
的方式,若要使用就需要額外呼叫,兩者的關係很像 forEach
之於 map
這樣,語法為:fun.bind(thisArg[, arg1[, arg2[, ...]]])
延續上方的範例,我們使用 bind
改寫,綁定完 this
還需要再呼叫一次函式才會再次執行:
const person = {
name: 'Jack',h
age: 1,
gender: 'male',
};
function print (time) {
console.log(`${time}: ${this.name} is ${this.age} years old.`)
};
// 做法一:像 call 一樣帶入函式原本的 n 個參數
const bindPrint = print.bind(person, '2023/01/01');
bindPrint();
// -> 2023/01/01: Jack is 1 years old.
// 做法二:科里化用法
const bindPrint = print.bind(person);
bindPrint('2023/01/01');
// -> 2023/01/01: Jack is 1 years old.
關於科里化作法是什麼,可以參考這篇文章。
最近我課金買了 chatGPT-4 的模型,我請它出了 this
的面試題,各位可以做做看來確認自己對於 this
的理解是否正確,需要注意的題目是有一點小陷阱的:
function Employee(name, position) {
this.name = name;
this.position = position;
this.details = function() {
return this.name + " is a " + this.position;
}
}
let emp1 = new Employee('John', 'Engineer');
let emp2 = new Employee('Jane', 'Manager');
let empDetails = emp1.details;
console.log(empDetails());
let empDetailsBound = emp1.details.bind(emp2);
console.log(empDetailsBound());
let empDetailsCall = emp1.details.call(emp2);
console.log(empDetailsCall);
let empDetailsApply = emp1.details.apply(emp2);
console.log(empDetailsApply);
接下來就留給各位自己練習啦,希望今天的文章可以讓大家更加理解 this
的用法跟原理,我是 Vivian,我們下次見!