【前端基礎】JavaScript 的 this 到底是什麼?call、apply 與 bind 的差異

2024/01/28閱讀時間約 11 分鐘

在之前的文章當中曾經提到過 JavaScript 中的物件有一個特別的機制:傳參考(Called by reference),如果正確性再高一點的話,則可以稱之為傳共享(Called by sharing)

在 JavaScript 中的物件,被生成後會出現一個參考位置,如果有其他變數或是在使用函式運算的過程中,會修改到這個物件,所有沿用(例如:拿這個物件去賦予給其他變數)、有使用到這個物件的地方,都會因為這個物件被改動,而連帶被受到影響,這樣的機制就稱為傳共享,因為所有的變數其實都共用了這個物件。

在理解 JavaScript 中 this 以前,理解 JavaScript 中的物件與傳共享機制是非常必要的,如果你已經夠熟悉這些機制的話就讓我們看下去吧。

註:這邊所提到所有內容都是基於在瀏覽器中執行的情境為主,在 Node.js 環境中的 this 概念略有不同。

JavaScript 的 this 到底是什麼?

**this 是一個被 JavaScript 保留的關鍵字,是一種抽象概念上的參考物件,也就是說:當我們使用到了 this ,這個 this 實際上指的某個參考物件。**

這裡為了避免誤會,我再講陳述的清楚一點: this 實際上指的某個參考物件,並不是某個物件,因為同一個物件可以被很多變數沿用、甚至在短時間內被不同的函式操作。

理解完 this 是一種參考物件後,再來聊聊其最重要的特性:會受範圍鏈(Scope Chain)以及被呼叫的位置(要根據上下文決定)來決定這個參考物件到底會是什麼。

全域中的的 this

之前在講 varletconst 的差異時,也有提到所謂範圍鏈的概念,如果不是那麼清楚的人可以參考看看這篇文章

在全域底下的 this 其實就是 window 物件,不過在嚴格模式中是取用不到 window 物件的(而且會報錯),所以同理上不會取用到 this 的值,不過並不會報錯,而是會回傳 undefined

'use strict';

console.log(this);
// -> undefined

講到這邊,我自己本人目前接觸前端已經三年了,是沒有遇到有需要使用到全域 this 的狀況,看一些 Youtube 上的超級資深網頁開發者會分享,以前為了方便所以會把一些方法實作後掛載到 window 上,不過前後端分離後這樣的時代也漸漸遠去,還需要理解這個東西的原因在於「理解 JavaScript 」機制,以及面試滿常考到的。

在函式中調用 this

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 變數、全域屬性的這篇文章

箭頭函式中沒有 this

在先前的範例中,可以發現幾乎都是使用「函式陳述式(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

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,我們下次見!

為了追求可以窩在座位上、可以心無旁騖思考問題、座位可以亂七八糟沒關係、不需要到處哈腰點頭跑客戶,不用腳踩十公分、連妝都可以不用化的職場人生,文組少女毅然決然踏上RD的養成日常。
留言0
查看全部
發表第一個留言支持創作者!