嗨,我是han,今天要來討論另外一個在用JavaScript開發時常常會遇到,但很容易踩到坑的 this;這篇文章小弟以程式小白的角度,整理出開發過程中遇到 this 如何能成功閃避87%的坑,無痛且優雅的使用 this 讓程式碼看起來更簡潔且專業,以下就來進入正題吧!
本篇文章是屬於新手導向,順便也作為學習筆記,如果能幫助到您更了解 this 運作原理就更好了!
文章中會提及三種作用域,即全域作用域(Global Scope)、函式作用域(Function Scope)及區塊作用域(Block Scope);這裡統一都是在瀏覽器環境下,而瀏覽器的頂層也就是全域,預設都是指向 window。
this 的觀念總結及判斷指向的懶人法]這裡先講小弟整理出來的觀念,以及如何快速判斷出 this 指向的流程;已經懂 this 的可以在腦袋突然當機時快速回憶,而初學者可以先了解大方向跟原理,再經由後面各觀念的進一步說明有更清晰的認識。
1. this的綁定對象可以分以下五種:
this指向全域)this指向物件)call、apply、bind三種,this 指向綁定的新對象)new 建構函式(this 會直接綁定在新的實例對象)this,在哪裡定義的就指向哪裡;與前四種不同,需特別單獨記憶
2. 在一般呼叫的情況下,如果是嚴格模式 (strict mode),this 會指向 undefined;如果是非嚴格模式, this 會指向 window(瀏覽器) 或 global (node)
3. 當new 建構式跟 bind 同時存在時,new 的優先權比 bind 還高,this 會變成指向用 new 建立的新實例;唯一例外是箭頭函式,因為他本身沒有constructor,導致無法被 new 呼叫,會噴錯
const fn1 = (name) => {
console.log(this.name);
};
const fn2 = new (fn1);
fn2(); //會噴錯 TypeError: fn1 is not a constructor
4. 箭頭函式為特殊的例外,他只看誰定義他,this就指向誰,後面不管怎麼用顯式綁定轉換,都不會改變 this 的對象;如前一點所提到,而箭頭函式本身沒有建構屬性,所以也不能用 new 建構式;故箭頭函式就只要把握一個原則:在哪出生 this 就指向哪
箭頭函式在哪裡出生(被定義的環境),
this就指向哪,一旦認定出生環境就會很忠心不會變心
箭頭函式在哪裡出生(被定義的環境),this就指向哪,一旦認定出生環境就會很忠心不會變心
箭頭函式在哪裡出生(被定義的環境),this就指向哪,一旦認定出生環境就會很忠心不會變心
5. 在 Javascript中,有三種作用域,分別為全域作用域(Global Scope)、函式作用域(Function Scope)、塊級作用域(Block Scope);其中只有全域作用域跟函式作用域會產生自己的 this (全域的 this 就是 window。
6. 塊級作用域及箭頭函式,則沒有自己的 this;塊級作用域(例如 if、for 所產生的大括號)本身不影響 this 的指向,在判斷 this 的指向時可以視為透明;而箭頭函式則是在定義時,this 直接綁定當時的環境
喝口水休息一下吧~~
this 指向簡易判斷流程]由上而下依序判斷 (因為有考慮到優先權及獨特性)

這張流程圖應該足以應付一般情況下 this 的指向了
this 指向說明]先記住一個口訣,this指向誰,是由呼叫的對象決定
這種呼叫,通常都是指向全域(window),看下面幾段程式碼範例:
//範例一
name = "全域";
function test() {
console.log(this.name);
}
test(); //全域
範例一這種很直觀,函式 test 是在全域呼叫的,這時候 this 就會指向全域(window),並且去尋找有沒有一個有沒有一個叫做 name 的變數;這時候就找到了 name = '全域'
//範例二
name = "全域";
const obj = {
name: "物件區域",
fn: function () {
console.log(this.name);
},
};
obj.fn(); //'物件區域'
範例二就是典型的用物件呼叫裡面的函式
這裡順便提到兩個知識點:
this
this是在函式 fn 中,而函式是透過物件呼叫,所以此時 this 就會指向呼叫函式的對象,也就是物件 obj 本身;所以 this.name 就會指向 obj 中 的 name: "物件區域"
特殊狀況
name = "全域";
const obj = {
name: "物件區域",
fn: function () {
function add() {
console.log(this.name);
}
add();
},
};
obj.fn(); //'全域'
咦?阿我的 fn 明明就是定義在物件中,為什麼 this 結果卻是指向全域的 name = "全域"?
別激動,且聽我娓娓道來!
注意到了嗎?這裡 this 是被函式 add() 呼叫的,由於 add() 前面沒有任何物件,這時候就會被認定為是一般呼叫,還記得一般呼叫 this 會指向什麼嗎? 聰明如你想必已經意識到了!沒錯就是 全域(window)!
那真的沒辦法去取得物件 obj 裡面的 name 了嗎?辦法還是有的,最快的方式就是把一般函式變成箭頭函式,在最後解釋箭頭函式的時候會再次用這個範例程式碼做說明。
call、apply、bind)在多數情況下,這些方法是能強制改變函式在執行時 this 的指向目標,先簡單介紹這三種方法的作用
1. call 立即執行,參數一個一個傳
function greet(city, age) {
console.log(`我是 ${this.name},住在 ${city},今年 ${age} 歲。`);
}
const person = { name: "小明" };
// 立即執行
greet.call(person, "台北", 25);
// 結果:我是 小明,住在 台北,今年 25 歲。
在函式 greet 執行時把 this 強制指向物件 person,後面再分別傳入對應參數值
補充說明: console裡的兩個反引號代表的是模板字串,它的作用就是能在字串中插入變數;想插入變數只要寫
${變數名稱}就好;最主要的作用就是簡化字串跟變數要連在一起,必須一直寫+連起來的窘境,十分方便。
2. apply 立即執行,參數用 陣列傳遞
還是跟 call 一樣的範例,改成 apply 寫法call 跟 apply 作用一樣,差別只有 參數傳遞的方法
function greet(city, age) {
console.log(`我是 ${this.name},住在 ${city},今年 ${age} 歲。`);
}
const person = { name: "小明" };
// 立即執行
greet.apply(person, ["台北", 25]);
// 結果:我是 小明,住在 台北,今年 25 歲。
3. bind 不會立即執行,而是會回傳一個綁定好 this 的新函式,可以自己決定何時執行
一樣拿同樣程式碼舉例
function greet(city, age) {
console.log(`我是 ${this.name},住在 ${city},今年 ${age} 歲。`);
}
const person = { name: "小明" };
const person2 = { name: "小華" };
// this 綁定到 person
const boundGreet = greet.bind(person);
// 想執行了
boundGreeet("台南", 18);
// 結果:我是 小明,住在 台南,今年 18 歲。
// 試圖改變綁定
boundGreet.bind(person2);
boundGreet("台南", 18) ;
// 結果:依然輸出 我是 小明,住在 台南,今年 18 歲。
那以下整理一下這 call、apply、bind 這三種改變 this 指向的函式差異
call 跟 apply 屬於 一次性綁定;主要用於函式執行當下改變 this 指向,函式執行完成後,又恢復原本指向;有點類似以下情境:「你是一間派遣公司老闆,手底下有數名員工(函式);在平常函式的 this 指向基本都是你;今天客戶有需求,你要派出其中一名員工(函式)出去替客戶工作,在替客戶工作這段時間,這個員工的 this 被暫時性強制綁定給這位客戶,一旦客戶交付工作完成,這個員工(函式)的 this 又會重新指向你」bind 屬於 永久性強制綁定,類似終身契約,在第一次綁定後 this 指向就會永久定下來。即使連續綁定或者企圖用 call 或 apply 也依然改不了 this 的綁定對象。 今天異想天開想用連續 bind 測試 this 的忠誠性,this 也只會忠於第一次綁定的對象;例如以下程式碼:greet.bind(person, "台南", 18).bind(person2, "台南", 18)
這裡的 this 永遠指向第一次綁定的 person 物件
3. 這樣聽起來 bind 感覺很霸道不講理?沒事還有一個可以讓 bind 乖乖聽話的大哥,那就是 new 建構函式,接下來就會提到 new
還扛的住嗎?不行就放鬆一下,起來動一動,然後再繼續 ~~
new 建構函式new 簡單說,就是依照已經存在的東西,用這東西的模樣再複製一個一模一樣的新東西;其實白話文就是 ctrl + C 但重新給一個新名字。以下直接用範例程式說明
// 1.
function House(name) {
this.owner = name;
}
// 2.
const han = { owner: '涵' };
// 3.
const HouseForHan = House.bind(han);
// 4.
HouseForHan('Tim');
// 5.
console.log(han.owner); //'Tim'
// 6.
const newHouse = new HouseForHan('愛美');
// 7.
console.log(newHouse.owner); //'愛美'
來逐行解釋一下這段程式碼在做什麼
name 會被當作函式裡 owner 的值;定義函式時不決定 this 指向 誰,要等到誰呼叫這函式時才決定
han (物件)House 的 this 強制且永久性的指向了 han 這個物件,並把這個綁定關係稱作 HouseForHan
this 已經指向 han 這個物件,因此就相當於執行 han.owner = 'Tim'
Tim 沒有問題HouseForHan 這模型,用 new 去複製出一個新房子叫 newHouse,並且將原本 HouseForHan的 this 指向 從 han物件 改為 newHouse; 同時把新房子的主人改為 愛美。愛美
new 的優先權在JavaScript裡天生就比 bind 高,所以用 new 呼叫時,this 直接指向新建的實例,bind 當初綁定的對象在這裡完全不起作用。
文章快結束了,加油 ~~
首先,還記得前面第二種 「物件呼叫(隱式綁定)」最後有提到一個特殊狀況嗎?讓我們再看一遍程式碼
name = "全域";
const obj = {
name: "物件區域",
fn: function () {
function add() {
console.log(this.name);
}
add();
},
};
obj.fn(); //'全域'
教練,我好想要取得物件裡的 name,該怎麼做呢?
其實只要把
function add() {
console.log(this.name);
}
改成
const add = () => {
console.log(this.name);
}
完整程式碼如下
name = "全域";
const obj = {
name: "物件區域",
fn: function () {
const add = () => {
console.log(this.name);
}
add();
},
};
obj.fn(); //'物件區域'
你這時候是不是有疑問,阿不都是函式嗎?為什麼會有這樣不同的結果?
這就是箭頭函式對於 this 指向需特別記住的地方。還記得前面一直強調:箭頭函式的 this 指向被定義時當下的環境,並且一旦認定了就從一而終,add() 是在 fn 函式裡被定義的,fn 執行時 this 指向 obj,箭頭函式會繼承定義當下環境的 this,所以 add() 的 this 也跟著指向 obj,而 this.name 其實就相當於 obj.name,也就是 name: "物件區域"。
如果今天箭頭函式是直接定義在塊級作用域呢?來看以下程式碼
name = "全域";
{
let x = 0;
let name = "物件區域";
const fn = () => {
console.log(this.name)
}
fn();
}
開頭觀念的第六點: 塊級作用域及箭頭函式,則沒有自己的 this
因為 let 跟 const 的出現,才有了塊級作用域(Block Scope)的概念出現;
接下來還有一句: 在判斷 this 的指向時可以視為透明!
fn 這個箭頭函式是在塊級作用域裡定義的,在判斷 this指向時,可以把這區域當作透明;那拿掉{} 之後,是不是就相當於在全域定義這個箭頭函式了?這時這個箭頭函式的 this 就會理所當然地指向全域 (window),name 也同時會指向全域定義的 name = "全域";
這篇文章花了我大概將近半天時間,來來回回修改了好幾遍,腦袋CPU燒了不少;this 真要認真討論,其實篇幅可以很長。
想表達的概念還有很多,寫的可能也不是這麼好理解,小弟已經盡量用口語化打這篇文章了。基於這篇主要是新手向,我就沒有再列舉其他雷點跟大坑,如果有興趣的、或者覺得哪裡有問題都可以再留言討論 ~~ 感謝各位大拿!
祝大家週末愉快 ~~