iT邦幫忙

2021 iThome 鐵人賽

DAY 14
1
Modern Web

舌尖上的JS系列 第 14

D14 - 服務生!我要 this this this

前言

想像一下,今天你是負責幫忙點菜的服務生,來了一桌客人,拿著菜單跟你說 「 我要 this this this ! 」,請問你知道他點三小什麼嗎,相信除非你會通靈不然光聽絕對猜不出來,那怎麼判斷呢?
看客人的手指向哪道菜判斷那個 this 對應誰

在 JavaScript 中也會遇到這種不直接告訴你 this 是誰的情況,雖然這裡沒有手指頭做參考,但別怕,教你另一招絕對精準的判斷方式,包你以後一秒指認 this !


Mighty Plants

怎麼知道 this 是誰?

怎麼看出 this 是誰? 從呼叫方式判斷
不同的呼叫方法下的 this 會不相同,呼叫方式可分為以下四種:

  1. 一般函式呼叫
  2. 函式作為 method 呼叫
  3. new constructor 呼叫
  4. call、apply 呼叫

接下來就一一從每個呼叫方法來認識 this 是誰吧!

一般函式呼叫

最直接的函式呼叫方式,就是在函式名稱後加上一個 ()
不論是 function declarationfunction expression立即函式 都算在一般的函式呼叫內。

這時的 this 是誰:在一般函式呼叫下的 this 代表的是全域,在嚴格模式下則是 undefined

//函式宣告
function func(){
    console.log(this)
}
func()  // window

//函式表達式
let func = function(){
    console.log(this)
}
func()  // window

//立即函式
(function (){ console.log (this)})()

作為 Method 呼叫

當函式成為一個物件內的屬性時,就可以稱該函式為一個 方法method,呼叫的語法像是 物件名稱.函式名稱()

這時的 this 是誰:作為 method 呼叫下的 this 會指向 method 所屬的物件。

var dish = '黃金開口笑'
function order() {
    console.log (this.dish)
}

let customer1 = {
    dish: '烈冰鮮鯛山',
    orderMore: order
}

let customer2 = {
    dish: '奇蹟彗星炒飯',
    orderMore: order
}

// 以下請自行加上 console.log
customer1.orderMore()   // '烈冰鮮鯛山' ,this 指向 customer1
customer2.orderMore()   // '奇蹟彗星炒飯',this 指向 customer2
order()   // '黃金開口笑', 一般函式呼叫 this 指向全域

變化題,若是將 customer1.orderMore 這個 method 存入一個變數中再進行呼叫呢?

let secondDish = customer1.orderMore;  // 將 customer1 的 orderMore 存入另一個變數再呼叫
secondDish()     // 黃金開口笑,此時的呼叫屬於一般函式呼叫,this 指向全域

// 除非是將使用 method 呼叫後的結果存入變數,該變數才會是 this 指向物件的結果
function order() {
    return (this.dish)
}
let secondDish = customer1.orderMore();
console.log( secondDish )   //  烈冰鮮鯛山

new constructor 呼叫

上一篇講到使用關鍵字 new 和建構式 constructor 創物件,這時的 this 會指向這個新創的物件,詳見 D13 - 做出雞蛋糕 new + Constructor

這時的 this 是誰:使用 new + constructor 下的 this 是新創的那個物件。

function Order(dish) {
    this.dish = dish
}

let customer1 = new Order('烈冰鮮鯛山')
let cusomter2 = new Order('奇蹟彗星炒飯')

console.log( customer1 ) // Order {dish: "烈冰鮮鯛山"}
console.log( customer2 ) // Order {dish: "奇蹟彗星炒飯"}

call、apply

點菜時除了依照客戶的喜好,服務生也可以推薦菜色,讓客人更改 「this」的選項為餐廳主打的招牌菜。

JavaScript 內也可以指定我們要的 this,透過所有函式都具備的兩種方法:applycall,在呼叫函式的同時,在參數內放入希望 this 指向的物件。

這時的 this 是誰:使用 call 和 apply 小括號內放的第一個引數。

var dish = '黃金開口笑'
function order() {
    console.log (this.dish)
}

let customer1 = {
    dish: '烈冰鮮鯛山',
    orderMore: order
}

let customer2 = {
    dish: '奇蹟彗星炒飯',
    orderMore: order
}

customer1.orderMore.call(customer2) // 奇蹟彗星炒飯,強制 this 指向 customer2
customer2.orderMore.apply(window)   // 黃金開口笑,強制 this 指向全域 window

order()                // 黃金開口笑, 一般函式呼叫 order, this 指向全域
order.call(customer1)  // 烈冰鮮鯛山, 透過 call 綁定 this 指向 customer1

call 和 apply 的差異?

兩者唯一的差異是引數的格式,第一個都是放 this 指定的物件,剩下的放欲帶入函式的引數

function.call    ( 指定 this 的物件, 引數1, 引數2, 引數3)
function.apply   ( 指定 this 的物件, [引數1, 引數2, 引數3])

call 可以接受任一數量的引數,適用在有多個彼此不相關的變數或實質
apply 的引數須包在陣列內

bind、箭頭函式

除了上面 call 和 apply 可以指定 this,另外兩種綁定 this 的方式為: bind 和 箭頭函式

為什麼要分開寫呢? 因為這兩種並不會進行函數呼叫,無法從呼叫判定,兩者的 this 都是在定義時就綁定好

bind 的 this 綁定跟 apply 和 call 一樣,在 () 中放入 指定的 this 物件

這時的 this 是誰:bind 依照小括號內放置的物件

var dish = '黃金開口笑'
function order() {
    console.log (this.dish)
}

let customer1 = {
    dish: '烈冰鮮鯛山',
    orderMore: order// 綁定 this 為 customer 2
}

let customer2 = {
    dish: '奇蹟彗星炒飯',
    orderMore: order.bind(customer1)
}

customer2.orderMore()                // 烈冰鮮鯛山
customer2.orderMore.call(customer2)  // 烈冰鮮鯛山,無法透過 call, apply 重新指定 this


let specialMenu = order.bind({dish: 'JS吃到飽'}) // 綁定 order function 內的 this
specialMenu()   // JS吃到飽

箭頭函式為 ES6 加入的 function expression 新寫法

  • 寫法更簡潔
  • this 的強制綁定

箭頭函式下的 this 會指向創建時的物件,一般幾乎指向全域,除非是使用建構式生成時,箭頭函式的 this 指向新創的物件,因此在箭頭函式內使用 this 要特別留意,一但定義了就無法重新綁定囉!

這時的 this 是誰:箭頭函式下的 this 可能指向全域或是創建的物件。

var dish = '黃金開口笑'

let customer1 = {
    dish: '烈冰鮮鯛山',
    orderMore: ()=> this.dish,
    orderMoreMore: function(){
        return (this.dish)
    }
}

let customer2 = {
    dish: '奇蹟彗星炒飯',
}
 
// 雖然使用 method 方式呼叫,但因 orderMore 使用箭頭函式, this 已綁定在全域
customer1.orderMore()                // 黃金開口笑
customer1.orderMore.call(customer2)  // 黃金開口笑,綁定在全域下無法透過 call、apply 更改 this

// 一般匿名函式的 this 不會先被綁定,隨呼叫不同而改變
customer1.orderMoreMore()               // 烈冰鮮鯛山
customer1.orderMoreMore.call(customer2) // 奇蹟彗星炒飯

一般情況下的 this 都會指向全域,但在 constructor 內使用箭頭函式的 this 可以綁定在新物件上


function Order(dish) {
    this.dish = dish;
    this.orderMore = ()=> this.dish
}

let customer1 = new Order('烈冰鮮鯛山')
let customer2 = new Order('奇蹟彗星炒飯')
customer1.orderMore()   // 烈冰鮮鯛山,箭頭函式內的 this 指向新建立的物件 customer1
customer2.orderMore()   // 奇蹟彗星炒飯,箭頭函式內的 this 指向新建立的物件 customer2

總結

以後遇到 this 想知道它到底指向誰,記住先看它是怎麼被呼叫的,再次整理如下

  • 一般函式呼叫: 非嚴格模式 - 全域、嚴格模式 - undefined
  • 作為 method 呼叫:this 指向 method 屬於的該物件
  • new constructor 呼叫:this 指向 new 出來的新物件
  • apply、call:this 為 apply 及 call 放的第一個引數值
  • bind:this 為 bind 括號內指定的物件
  • 箭頭函式:this 指向箭頭函式建立時的物件,一般為全域,在 constructor 內建立的指向新物件

Reference

[008重新認識 JavaScipt]
忍者 JavaScript 開發技巧探秘第二版 by John Resig, Bear Bibeault, Josip Maras


上一篇
D13 - 做出雞蛋糕 new + Constructor
下一篇
D15 - 那個圓圓的東西 - OOP 物件導向程式設計
系列文
舌尖上的JS30

1 則留言

1
Chiahsuan
iT邦新手 4 級 ‧ 2021-09-29 15:46:52

/images/emoticon/emoticon12.gif
讚讚~一直有小當家的畫面跑出來

一般匿名函式的 this 不會先被綁定,隨呼叫不同而改變

學到一個新觀念~謝謝!!!

我要留言

立即登入留言