[ 筆記 ] JavaScript 進階 09 - What is 「this」?


Posted by krebikshaw on 2020-09-30

this 到底在幹嘛?

this 為什麼要學?他很好用嗎?其實 this 可以方便我們處理很多狀況,舉個例來說:
老師要改班上同學的考卷,這個班的同學假設有 10 個人,老師可能需要:

看 甲同學 的考卷: {
  看 甲同學 考卷的第一題答案對不對,對了打勾,錯了打叉
  看 甲同學 考卷的第二提答案對不對,對了打勾,錯了打叉
                 .
                 .
                 .
  把 甲同學 最後的成績算出來,並打上分數
}
================================================
看 乙同學 的考卷: {
  看 乙同學 考卷的第一題答案對不對,對了打勾,錯了打叉
  看 乙同學 考卷的第二提答案對不對,對了打勾,錯了打叉
                 .
                 .
                 .
  把 乙同學 最後的成績算出來,並打上分數
}
================================================
看 丙同學 的考卷: {
  ...
}
                 .
                 .
                 .
================================================
看 第 10 位同學 的考卷: {
  ...
}

因為 10 位同學的考券都不相同,所以一樣的動作要寫 10 遍。
假如我們能把改考卷的動作,套用到不同的學生身上,這樣只需要寫一遍,豈不是方便很多?

像下面這樣:

看 this同學 的考卷: {
  看 this同學 考卷的第一題答案對不對,對了打勾,錯了打叉
  看 this同學 考卷的第二提答案對不對,對了打勾,錯了打叉
                 .
                 .
                 .
  把 this同學 最後的成績算出來,並打上分數
}

這份改考卷流程,可以讓我們在拿到不同同學的考卷,自動將 this 設定為該同學

  • 改 甲同學 的考卷時,this 就是甲同學
  • 改 乙同學 的考卷時,this 就是乙同學

這樣一來,就可以看出 this 的方便性了。
所以 this 可以讓在不確定角色的情況,先留一個角色的位子在哪邊,等到有角色要套入的時候,自動將 this 變為該角色。

以物件導向的例子來看:

class Dog(name) {
  this.name = name   // 還不確定角色,先用 this 留一個位置給他
}

//  當有角色要套入的時候,自動將 this 變為該角色
const TaiwanDog = new Dog(Peter)  //  this 就會是 TaiwanDog
const KoreanDog = new Dog(Marry)  //  this 就會是 KoreanDog

先有個概念,知道為什麼我們需要搞懂 this,再來探討如何分辨 this 的值會是多少。

this 是什麼? 為什麼這麼複雜?

首先,可以先看看文章:淺談 JavaScript 頭號難題 this:絕對不完整,但保證好懂,讀完內文先記得一件事: 「 一但脫離了物件,就不太需要關注 this 的值,因為沒什麼意義 」。

沒什麼太大意義的 this
像下面這段程式碼,在一個 function 裡面印出 this 看看會是什麼

function hello(){
  console.log(this)
}

hello()

在這種情況下我會跟你說 this 沒有任何意義,this 的值在瀏覽器底下就會是 window,在 node.js 底下會是 global,如果是在嚴格模式,this 的值就會是 undefined。

  • 在非物件導向的環境下,this 會是預設值,而 this 的預設值會依據環境的不同而有所改變。
    • node.js : global
    • 瀏覽器 : window
    • 嚴格模式 : undefined ( node.js 跟瀏覽器都一樣 )
  • 想要把 this 統一的話,其實可以改成嚴格模式: 'use strict';,這樣 this 就會變成 undefined

更改 this 的值

僅管 this 可能有預設的值,但我們可以透過一些方法來改它。這改的方法也很簡單,一共有三種。

  1. call()
  2. apply()
  3. bind()

☞ 第一種跟第二種方法: call / apply
可以傳參數進去,參數傳什麼,裡面 this 的值就會是什麼。儘管原本已經有 this,也依然會被這種方法給覆蓋掉

call & apply

class Car {
  hello() {
    console.log(this)
  }
}

const myCar = new Car()
myCar.hello() // myCar instance
myCar.hello.call('yaaaa') // yaaaa
myCar.hello.apply('nonono') // nonono

原本 this 的值應該要是 myCar 這個 instance,可是卻被我們在使用 call 時傳進去的參數給覆蓋掉了。

'use strict';

function test(a, b, c) {
  console.log('this: ', this);
  console.log(a, b, c);
}

test(1, 2, 3); // this:  undefined
test.call('I am call', 1, 2, 3); // this: I am call
test.apply('I am apply', [1, 2, 3]); // this: I am apply
  • 第一個輸出:
    • 預設值用嚴格模式下是 undefined
  • 第二個輸出:
    • .call(<this>, <arugemt_1, arugemt_2...>) 改變 this 為字串 I am call
  • 第三個輸出:
    • .apply(<this>, [<arugemt_1, arugemt_2...>]) 改變 this 為字串 I am apply,第二個參數為一個陣列,裡面放參數

所以看得出來 call & apply 的差別,其實就只有參數是不是 array 的形式而已。

☞ 第三種方法,bind : 回傳一個指定 this 的 function
如果只想要固定 this 值、沒有要立刻執行,這時就可以用 bind,用 function bind 會回傳固定 this 的 function 本身。

這樣就不用擔心因為呼叫當下而改變 this,且之後就算用 call or apply 也改變不了 this。

const obj = {
  sayThis: function() {
    console.log(this);
  }
}

const say = obj.sayThis.bind('hello');   //  將 sayThis 函式中的 this 綁定為 hello,並且賦值到 say 這個變數
say();           // => 輸出 [String: 'hello']
say.call('who'); // => 還是輸出 [String: 'hello']

綜合以上兩個重點:

  1. 在物件以外的 this 基本上沒有任何意義,硬要輸出的話會給個預設值
  2. 可以用 call、apply 與 bind 改變 this 的值

怎麼判斷 this 的數值?

this 的值跟作用域跟程式碼的位置在哪裡完全無關,只跟「你如何呼叫」有關

const obj = {
  value: 1,
  hello: function() {
    console.log(this.value)
  }
}

obj.hello() // 1
const hey = obj.hello
hey() // undefined

明明就是同一個函式,但是呼叫的方式不同,this 的值就會不同

判斷 this 的方式:
在呼叫 function 以前是什麼東西,this 就會是什麼。

  • obj.hello() 的 this 就是 obj
  • hey()前面沒有東西,所以 this 就是預設值

以下兩個範例可以給大家練習:

const obj = {
  value: 1,
  hello: function() {
    console.log(this.value)
  },
  inner: {
    value: 2,
    hello: function() {
      console.log(this.value)
    }
  }
}

const obj2 = obj.inner
const hello = obj.inner.hello
obj.inner.hello()
obj2.hello()
hello()

公佈答案:

  1. obj.inner.hello() 的 this 是 obj.inner,所以執行結果是 2
  2. obj2.hello() 的 this 是 obj2,obj2 又等於 obj.inner,所以執行結果也是 2
  3. hello() 的 this 是預設值,因為 window 底下沒有 value,所以執行結果是 undefined
var x = 10
var obj = {
  x: 20,
  fn: function() {
    var test = function() {
      console.log(this.x)
    }
    test()
  }
}

obj.fn()

這題可能大家會以為 this 是 obj,因為感覺 obj 呼叫了 fn() 函式,所以 this 應該是 obj。

但其實有被「呼叫」到真正跟 this 有關的 function 是 test()
所以其實 test() 被呼叫的時候,this 是預設值,也就是 window,而 window.x 得到的結果是 10,10 才是這題的答案。

所以一定要記住判斷 this 最關鍵的原則:要看 this,就看這個函式「怎麽」被呼叫

this 的例外情況

☞例外( 一 ) 事件監聽中的 this
DOM 物件綁定某種事件時,this 會變成綁定的 DOM 元素本身:

document.querySeletor('.btn').addEventListener('click', function(){
    console.log(this); // => btn 這個元素
});

☞例外( 二 )箭頭函示 arrow function 中的 this
之前不是說過 this 跟在哪裡定義無關、而是跟在哪裡呼叫有關嗎?
但有個例外喔,就是箭頭函式。

example1: 一般例子

'use strict';

class Test {
  run() {
    console.log('run: ', this);
    setTimeout(function() {
      console.log('setTimout: ', this);
    }, 1000);
  }
}
const test = new Test(); 
test.run();
// run: Test {}
// setTimout: undefined ( 一秒後 )

以上例子看起來很正常,沒有問題,那如果換成 arrow function 呢? 會發現 setTimout 的 this 變成 Test 了。

example2: 改成箭頭函式 arrow function

'use strict';

class Test {
  run() {
    console.log('run: ', this);
    setTimeout(() => {
      console.log('setTimout: ', this);
    }, 1000);
  }
}
const test = new Test();
test.run();
// run: Test {}
// setTimout: Test {} ( 一秒後 )

this 在 arrow function 中,跟在哪裡定義有關,此時的 this 有點像變數的行為,會依照作用域去抓外部的 this,依據上一層的 run: this 是什麼,setTimout: this 就會是什麼。

example3: 用 bind 改變 this , arrow function 裡面的 this 也會跟著換

要驗證的話,可以把 run function 的 this 固定成一個字串 hello,所以此處的 setTimout: this 也會跟著變成 hello。

class Test {
  run() {
    console.log('run: ', this);
    setTimeout(() => {
      console.log('setTimout: ', this);
    }, 1000);
  }
}
const test = new Test();
const newTest = test.run.bind('hello'); // => 把 this 固定成 hello
newTest();

// run:  hello
// setTimout: hello ( 一秒後 )

this 到底是什麼?

有很多因素要考慮:

  1. 非物件導向底下,要看執行環境有不同預設值:
    • node.js : global
    • 瀏覽器 : window
    • 嚴格模式 : undefined ( node.js 跟瀏覽器都一樣 )
  2. 物件導向中, this 就是 new 出來的 instance
  3. DOM 事件綁定時, this 為綁定的 DOM 物件
  4. 單純在物件底下,this 跟如何呼叫 function 有關
    • 如果是在物件本身上呼叫,this 為物件本身
    • 把 function 抽出來呼叫,this 則為預設值 ( global or window or undefined )
  5. arrow function 中,根據外部的 this 是什麼而決定

如何改變 this:

  1. bind : 將 this 值固定下來,回傳 function
  2. call or apply : 指定 this,傳入參數並執行 function

小測驗

function log() {
  console.log(this);
}

var a = { name: 'a', log: log };
var b = { name: 'b', log: log };

log(); // => global or window
a.log(); // => a 本身
b.log.apply(a); // => a 本身


const newLog = b.log.bind(b); 
newLog.apply(a); // => b 本身

#this







Related Posts

你今天註冊了嗎?用後端好朋友 express 跟 middleware 們做一個簡單的會員註冊系統

你今天註冊了嗎?用後端好朋友 express 跟 middleware 們做一個簡單的會員註冊系統

[MTR04] W2 D2 位元運算和 JS 基礎:變數

[MTR04] W2 D2 位元運算和 JS 基礎:變數

基本屬性注入方法

基本屬性注入方法


Comments