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

更新 發佈閱讀 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,我們下次見!

留言
avatar-img
留言分享你的想法!
avatar-img
Vivian Yeh - 跨領域轉職的軟體工程師
453會員
103內容數
為了追求可以窩在座位上、可以心無旁騖思考問題、座位可以亂七八糟沒關係、不需要到處哈腰點頭跑客戶,不用腳踩十公分、連妝都可以不用化的職場人生,文組少女毅然決然踏上RD的養成日常。
2024/05/23
與 cookie 相比,localStorage 與 sessionStorage 的機制相對單純,兩者皆是瀏覽器中的儲存空間,與 cookie 最大的不同在於:localStorage 與 ⋯⋯
Thumbnail
2024/05/23
與 cookie 相比,localStorage 與 sessionStorage 的機制相對單純,兩者皆是瀏覽器中的儲存空間,與 cookie 最大的不同在於:localStorage 與 ⋯⋯
Thumbnail
2024/05/22
在瀏覽器環境中有許多的儲存空間,想要查看這些空間的話,可以透過「chrome > Dev Tools > Application > Storage」即能進行查看。 瀏覽器內存空間的差異不僅常常被拿來被當作面試考題,在實務開發中更扮演舉足輕重的角色,今天就想透過這系列的文章深度了解這些瀏覽器內存⋯
Thumbnail
2024/05/22
在瀏覽器環境中有許多的儲存空間,想要查看這些空間的話,可以透過「chrome > Dev Tools > Application > Storage」即能進行查看。 瀏覽器內存空間的差異不僅常常被拿來被當作面試考題,在實務開發中更扮演舉足輕重的角色,今天就想透過這系列的文章深度了解這些瀏覽器內存⋯
Thumbnail
2024/02/20
上一篇文章分享了 TypeScript 的定義、前端角色定位,如果你不是很確定「TypeScript 是什麼?」、「TypeScript 作為 JavaScript 的超集,在網頁開發扮演怎麼樣的角色?」這兩個問題的答案,建議可以回到上一篇先了解一下。
Thumbnail
2024/02/20
上一篇文章分享了 TypeScript 的定義、前端角色定位,如果你不是很確定「TypeScript 是什麼?」、「TypeScript 作為 JavaScript 的超集,在網頁開發扮演怎麼樣的角色?」這兩個問題的答案,建議可以回到上一篇先了解一下。
Thumbnail
看更多
你可能也想看
Thumbnail
JSDoc 全名是 JavaScript Documentation,顧名思義是為 JavaScript 所使用的 API 文件,在程式碼內透過註解的方式撰寫,運行後 JSDoc 會自動掃描註解內容,並生成一份網頁版的文件,對於沒有使用 Typescript 開發的專案,也
Thumbnail
JSDoc 全名是 JavaScript Documentation,顧名思義是為 JavaScript 所使用的 API 文件,在程式碼內透過註解的方式撰寫,運行後 JSDoc 會自動掃描註解內容,並生成一份網頁版的文件,對於沒有使用 Typescript 開發的專案,也
Thumbnail
本章節旨在介紹JavaScript中的物件導向編程。內容包括類別(Class)的定義和使用,建構子的作用,以及公開,私有,受保護(Protected)等不同訪問修飾符的概念。此外,還涵蓋了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型、反射等物件導向的主要觀念。
Thumbnail
本章節旨在介紹JavaScript中的物件導向編程。內容包括類別(Class)的定義和使用,建構子的作用,以及公開,私有,受保護(Protected)等不同訪問修飾符的概念。此外,還涵蓋了繼承、多型、封裝、介面、抽象類別、靜態類別、列舉、委派、Lambda表達式、泛型、反射等物件導向的主要觀念。
Thumbnail
JavaScript (簡稱 JS) 是具有一級函數的輕量級、直譯式或即時編譯的程式語言。它因為用作網頁的腳本語言而大為知名,但也用於許多非瀏覽器的環境,像是 Node.js 等。由於 JavaScript 語法上的一些缺點,軟體工程師們又設計出了 CoffeeScript、TypeScript 和
Thumbnail
JavaScript (簡稱 JS) 是具有一級函數的輕量級、直譯式或即時編譯的程式語言。它因為用作網頁的腳本語言而大為知名,但也用於許多非瀏覽器的環境,像是 Node.js 等。由於 JavaScript 語法上的一些缺點,軟體工程師們又設計出了 CoffeeScript、TypeScript 和
Thumbnail
這些章節的目的是為了介紹JavaScript中的各種數據類型,包括基礎類型和物件類型,以及如何將數據從一種類型轉換為另一種類型。此外,還介紹了如何創建自定義類型,以及如何使用JavaScript中的陣列、集合和字典。
Thumbnail
這些章節的目的是為了介紹JavaScript中的各種數據類型,包括基礎類型和物件類型,以及如何將數據從一種類型轉換為另一種類型。此外,還介紹了如何創建自定義類型,以及如何使用JavaScript中的陣列、集合和字典。
Thumbnail
JavaScript是一種具有動態型別、弱型別、原型繼承等特性的高級腳本語言,應用範圍廣泛,包括前端開發、後端開發、移動應用等。它被各種公司和開源社區廣泛使用。學習JavaScript需要掌握ECMAScript標準、異步編程、模塊系統等知識。
Thumbnail
JavaScript是一種具有動態型別、弱型別、原型繼承等特性的高級腳本語言,應用範圍廣泛,包括前端開發、後端開發、移動應用等。它被各種公司和開源社區廣泛使用。學習JavaScript需要掌握ECMAScript標準、異步編程、模塊系統等知識。
Thumbnail
針對 JavaScript 中的原始型別和隱性轉型進行了詳細的探討
Thumbnail
針對 JavaScript 中的原始型別和隱性轉型進行了詳細的探討
Thumbnail
可選串聯(?.)運算符用於訪問 object 的屬性或調用函數。如果使用該運算符訪問的object 或調用的函式為 undefined 或 null,則表達式會回傳 undefined,而不是拋出錯誤。
Thumbnail
可選串聯(?.)運算符用於訪問 object 的屬性或調用函數。如果使用該運算符訪問的object 或調用的函式為 undefined 或 null,則表達式會回傳 undefined,而不是拋出錯誤。
Thumbnail
學習JavaScript的理由有很多,包括容易學習的程式語言、互動式體驗、多功能性、跨平臺、社群和資源豐富、高市場需求。此外,文章提供了設計和前端教學的相關資源連結。文章中還提到了一些與學習JavaScript相關的教學文章和影音教學資源。
Thumbnail
學習JavaScript的理由有很多,包括容易學習的程式語言、互動式體驗、多功能性、跨平臺、社群和資源豐富、高市場需求。此外,文章提供了設計和前端教學的相關資源連結。文章中還提到了一些與學習JavaScript相關的教學文章和影音教學資源。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News