iT邦幫忙

0

this 心海底針!this 到底是指向誰? 幫助 JavaScript 開發者找到 this 的家

  • 分享至 

  • xImage
  •  

嗨,我是han,今天要來討論另外一個在用JavaScript開發時常常會遇到,但很容易踩到坑的 this;這篇文章小弟以程式小白的角度,整理出開發過程中遇到 this 如何能成功閃避87%的坑,無痛且優雅的使用 this 讓程式碼看起來更簡潔且專業,以下就來進入正題吧!

[前言]

本篇文章是屬於新手導向,順便也作為學習筆記,如果能幫助到您更了解 this 運作原理就更好了!

文章中會提及三種作用域,即全域作用域(Global Scope)函式作用域(Function Scope)區塊作用域(Block Scope);這裡統一都是在瀏覽器環境下,而瀏覽器的頂層也就是全域,預設都是指向 window

[this 的觀念總結及判斷指向的懶人法]

這裡先講小弟整理出來的觀念,以及如何快速判斷出 this 指向的流程;已經懂 this 的可以在腦袋突然當機時快速回憶,而初學者可以先了解大方向跟原理,再經由後面各觀念的進一步說明有更清晰的認識。

6點觀念整理

1. this的綁定對象可以分以下五種:

  • 一般呼叫(this指向全域)
  • 物件呼叫(也稱隱式綁定,this指向物件)
  • 顯式綁定(有分callapplybind三種,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;塊級作用域(例如 iffor 所產生的大括號)本身不影響 this 的指向,在判斷 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(); //'物件區域'

範例二就是典型的用物件呼叫裡面的函式

這裡順便提到兩個知識點:

  1. 函式在定義後就會創建一個函式作用域,會有屬於自己的 this
  2. 範例中,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 了嗎?辦法還是有的,最快的方式就是把一般函式變成箭頭函式,在最後解釋箭頭函式的時候會再次用這個範例程式碼做說明。

 
 

第三種: 顯式綁定 (callapplybind)

在多數情況下,這些方法是能強制改變函式在執行時 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 寫法
callapply 作用一樣,差別只有 參數傳遞的方法

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 歲。

那以下整理一下這 callapplybind 這三種改變 this 指向的函式差異

  1. callapply 屬於 一次性綁定;主要用於函式執行當下改變 this 指向,函式執行完成後,又恢復原本指向;有點類似以下情境:「你是一間派遣公司老闆,手底下有數名員工(函式);在平常函式的 this 指向基本都是你;今天客戶有需求,你要派出其中一名員工(函式)出去替客戶工作,在替客戶工作這段時間,這個員工的 this 被暫時性強制綁定給這位客戶,一旦客戶交付工作完成,這個員工(函式)的 this 又會重新指向你」
  2. bind 屬於 永久性強制綁定,類似終身契約,在第一次綁定後 this 指向就會永久定下來。即使連續綁定或者企圖用 callapply 也依然改不了 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); //'愛美'

來逐行解釋一下這段程式碼在做什麼

  1. 定義一個房子的模型,傳入的參數 name 會被當作函式裡 owner 的值;定義函式時不決定 this 指向 誰,要等到誰呼叫這函式時才決定
  2. 定義一個 han (物件)
  3. 把函式 Housethis 強制且永久性的指向了 han 這個物件,並把這個綁定關係稱作 HouseForHan
  4. 執行函式,因此時 this 已經指向 han 這個物件,因此就相當於執行 han.owner = 'Tim'
  5. 此時 console 輸出 就會是 Tim 沒有問題
  6. 根據 HouseForHan 這模型,用 new 去複製出一個新房子叫 newHouse,並且將原本 HouseForHanthis 指向 從 han物件 改為 newHouse; 同時把新房子的主人改為 愛美
  7. 印證新房子是否改為 愛美

new 的優先權在JavaScript裡天生就比 bind 高,所以用 new 呼叫時,this 直接指向新建的實例,bind 當初綁定的對象在這裡完全不起作用。

 
 

文章快結束了,加油 ~~

 
 

第五種: 很固執、但又專情、又必須特別注意的 箭頭函式 (Arrow Function)

 

首先,還記得前面第二種 「物件呼叫(隱式綁定)」最後有提到一個特殊狀況嗎?讓我們再看一遍程式碼

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
因為 letconst 的出現,才有了塊級作用域(Block Scope)的概念出現;
接下來還有一句: 在判斷 this 的指向時可以視為透明

fn 這個箭頭函式是在塊級作用域裡定義的,在判斷 this指向時,可以把這區域當作透明;那拿掉{} 之後,是不是就相當於在全域定義這個箭頭函式了?這時這個箭頭函式的 this 就會理所當然地指向全域 (window),name 也同時會指向全域定義的 name = "全域";

 
 

[結語]

 

這篇文章花了我大概將近半天時間,來來回回修改了好幾遍,腦袋CPU燒了不少;this 真要認真討論,其實篇幅可以很長。
想表達的概念還有很多,寫的可能也不是這麼好理解,小弟已經盡量用口語化打這篇文章了。基於這篇主要是新手向,我就沒有再列舉其他雷點跟大坑,如果有興趣的、或者覺得哪裡有問題都可以再留言討論 ~~ 感謝各位大拿!

 
 

祝大家週末愉快 ~~


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言