先學完物件導向,學 this 才有意義


Posted by saffran on 2021-02-25

在物件導向裡面,this 的意義在哪

this 原本只是用來在物件導向裡面使用的
在物件導向裡面,this 是用來存取「目前對應到的這個 instance」,不然也沒有其他方法可以去存取目前對應到的 instance 了

this 對應到的就是「目前在呼叫 function 的這個 instance」

在沒有意義的地方呼叫 this,預設值會是什麼?

如果不是在物件導向的環境(也沒有在嚴格模式下),this 的預設值會是一個「global 的東西」,global 會取決於環境而有不同的值,但都會是一個全域的東西

例如:

在 Node.js 執行時,this 的預設值是一個叫做 global 的變數

function test() {
  console.log(this === global); // 在 Node.js 裡面,this 的預設值是一個叫做 global 的變數
}

test(); // true

在瀏覽器執行時,this 的預設值是 window

use strict 開啟嚴格模式

但其實,會有上面這些預設值還滿奇怪的,因為 this 並沒有指向到任何東西,怎麼還會給它這些 global 的值呢?

這時,可以用 "use strict" 來開啟嚴格模式

在嚴格模式下,不管是在 Node.js 或是瀏覽器,this 預設值都會是 undefined,因為 this 本來就不需要任何預設值

Node.js

"use strict"; // 開啟嚴格模式

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

test(); // undefined

瀏覽器

this 放在 function 裡面,this 預設值也是 undefined

"use strict"; // 開啟嚴格模式

function test() {
  var a = 1;
  function inner() {
    console.log(this);
  }
  inner();
}

test(); // undefined

用 DOM + 瀏覽器的事件

如果是「用 DOM + 瀏覽器的事件」,this 的預設值就是「你去實際做操作的那個東西」

例如以下的 EventListener:
我 click 到哪一個按鈕,this 就會指向到那個按鈕

//用 DOM + 瀏覽器的事件
document.querySelector("btn").addEventListener("click", function () {
  console.log(this); // 我 click 到哪一個按鈕,this 就會指向到那個按鈕
});

如果不是在物件導向的環境,也不是「用 DOM + 瀏覽器的事件」,基本上 this 都是沒有任何意義的(undefined)

另外兩種呼叫 function 的方法:call 與 apply

call() 來呼叫 function

call() 裡面傳入的第一個參數,就會是 function 裡面的 this

"use strict"; // 開啟嚴格模式

function test() {
  console.log(this); // 123
}

test.call(123);

apply() 來呼叫 function

apply() 裡面傳入的第一個參數,就會是 function 裡面的 this

"use strict"; // 開啟嚴格模式

function test() {
  console.log(this); // [ 1 ]
}

test.apply([1]);

call 跟 apply 的差別

call 跟 apply 的差別就只有一個,那就是:

  • call() 可以傳多個參數,都是用逗號隔開
  • apply() 的參數只能有兩個,第二個參數會是一個 array,用 array 把我要傳的參數包起來

call 跟 apply 共同的重點就是:

call()apply() 的第一個參數,都可以去改變 function 裡面 this 的值。傳入什麼值,this 就會是什麼值

call

對於 call 來說,可以傳多個參數,都是用逗號隔開

  • 第一個參數 "hello" 就會是 this 的值
  • 然後 1, 2, 3 就會依序對應到參數 a, b, c
"use strict"; // 開啟嚴格模式

function test(a, b, c) {
  console.log(this); // [String: 'hello']
  console.log(a, b, c); // 1 2 3
}

test.call("hello", 1, 2, 3);

apply

對於 apply 來說,參數只能有兩個

  • 第一個參數是 this 的值
  • 第二個參數會是一個 array,用 array 把我要傳的參數包起來(用 array 的方式來表示參數)
"use strict"; // 開啟嚴格模式

function test(a, b, c) {
  console.log(this); // hello
  console.log(a, b, c); // 1 2 3
}

test.apply("hello", [1, 2, 3]); // 第二個參數會是一個 array,用 array 把我要傳的參數包起來

用另一種角度來看 this 的值

this 的值跟「寫在程式碼的哪裡」無關,只跟「function 是怎麼被呼叫的」有關

範例一:this 的值會是「obj 本身」

"use strict"; // 開啟嚴格模式

const obj = {
  a: 123,
  test: function () {
    console.log(this); // { a: 123, test: [Function: test] }
  },
};

obj.test();

範例二:this 的值都會是 inner 這個物件

「直接呼叫 function」跟「用 call 呼叫」,this 的值都會是 inner 這個物件

"use strict"; // 開啟嚴格模式

const obj = {
  a: 123,
  inner: {
    test: function () {
      console.log(this); //
    },
  },
};

obj.inner.test(); // { test: [Function: test] }
obj.inner.test.call(obj.inner); // { test: [Function: test] }

範例三:this 的值會是 undefined

執行 func() 會印出 this 的值是 undefined

"use strict"; // 開啟嚴格模式

const obj = {
  a: 123,
  inner: {
    test: function () {
      console.log(this); // undefined
    },
  },
};

const func = obj.inner.test;
func();

原因為:
func() 可以看成是 func.call(undefined)

因為在 func.call() 前面沒有其他東西了,所以第一個參數就只能是 undefined

"use strict"; // 開啟嚴格模式

const obj = {
  a: 123,
  inner: {
    test: function () {
      console.log(this); // undefined
    },
  },
};

const func = obj.inner.test;
func() 可以看成是 func.call(undefined);

小技巧:把 function call 轉成用 call() 的形式來呼叫,就可以很清楚知道 this 的值會是什麼了

call() 這個形式來呼叫 function,就可以很簡便的得知 this 的值

例如:

  • func() 因為在 func 前面沒有任何東西,所以 this 的預設值就是 undefined
  • obj.inner.test() 因為在 test() 前面是 obj.inner,所以 this 的預設值就是 obj.inner
func() 可以看成是 func.call(undefined); // this 的預設值就是 undefined
obj.inner.test() 可以看成是 obj.inner.test.call(obj.inner) // this 的預設值就是 obj.inner
"use strict"; // 開啟嚴格模式

const obj = {
  a: 123,
  inner: {
    test: function () {
      console.log(this); // undefined
    },
  },
};

const func = obj.inner.test;
func() 可以看成是 func.call(undefined);
obj.inner.test() 可以看成是 obj.inner.test.call(obj.inner)

只要掌握幾個重點,this 的部分就沒問題了!

  1. 在跟物件無關的地方去 log this,會依據「是否為嚴格模式」以及「執行環境不同」而有不同的預設值(有可能是 window 或是 undefined
  2. 在物件導向裡面去 log thisthis 就會是「自己這個 instance」
  3. 呼叫一個物件裡面的 this,可以把 function call 轉換成 call() 的形式,call() 的第一個參數是什麼,this 的預設值就會是什麼

this 的練習題:

下面各會印出什麼值呢?

"use strict";

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

var a = { a: 1, log: log };
var b = { a: 2, log: log };

log(); // undefined
a.log(); // { a: 1, log: [Function: log] }

b.log.apply(a); // { a: 1, log: [Function: log] }
  • 第 10 行 log() 就是在一個沒有意義的地方呼叫 this,在嚴格模式下,this 的值就會是 undefined
  • 第 11 行 a.log()this 的值就會是 a
  • 第 12 行 b.log.apply(a),在 apply() 裡面傳入的第一個參數就會是 this 的值,所以就是 a

bind 強制綁定 this 的值

下面範例中,用不同的方式去呼叫 function,this 就會有不同的值:

  • 第 10 行 obj.test()this 的值是 obj
  • 第 12 行 func()this 的值是 undefined
"use strict";

const obj = {
  a: 1,
  test: function () {
    console.log(this);
  },
};

obj.test(); // this 的值是 obj
const func = obj.test;
func(); // this 的值是 undefined

但我想要做的是:不管用什麼方式呼叫 function,this 的值都是一樣的

要做到這件事情,就要用 bindthis 的值給強制綁定住,範例如下:

bind() 小括號裡面的東西當作 this 放到 obj.test

綁定完成之後,我就不用擔心因為呼叫的方式不同而有不同的 this 值了

就算是用 bindTest.call(123) 來呼叫 function,this 的值依然會是 hello

bind 跟「call, apply」不同的地方在於:

  • bind 幫我把 this 綁定完之後,會回傳一個 function,所以 bindTest 會是一個 function
  • call, apply」是幫我指定完 this 的值之後,會直接呼叫 function
"use strict";

const obj = {
  a: 1,
  test: function () {
    console.log(this);
  },
};

const bindTest = obj.test.bind("hello");

bindTest(); // this 的值會是 hello
bindTest.call(123); // 就算用 call 來呼叫,this 的值依然會是 hello

arrow function 的 this

先看這個沒有箭頭函式的範例:

class Test {
  run() {
    console.log("run this: ", this); // this 的值會是 t 這個 instance
    setTimeout(function () {
      console.log(this); // this 的值會是 window
    }, 100);
  }
}

const t = new Test();
t.run();

把上面的程式碼貼到瀏覽器的 console 去執行:

  • 第 3 行的 this 就會是 t 這個 instance
  • 第 5 行的 this 就會是 window

為什麼第 5 行的 this 會是 window 呢?

因為過了 100 毫秒之後執行 setTimeout 裡面的 function,就是在一個沒有意義的地方呼叫 this,所以在寬鬆模式下,瀏覽器的 this 預設值就會是 window(如果是在嚴格模式下,瀏覽器的 this 預設值就會是 undefined)

箭頭函式的 this

現在,我把 setTimeout 裡面的 function 改成箭頭函式,結果印出來的 this 跟上一層的 this 是一樣的(都是 t 這個 instance)

class Test {
  run() {
    console.log("run this: ", this); // this 的值會是 t 這個 instance
    setTimeout(() => {
      console.log(this); // this 的值會是 t 這個 instance
    }, 100);
  }
}

const t = new Test();
t.run();

原因為:
這是箭頭函式的特性(是一個特例)
箭頭函式裡面的 this,跟我怎麼呼叫沒有關係,而是跟「定義在程式碼的哪裡」有關(概念跟 scope 比較像)

上一層定義好的 this 是什麼,在箭頭函式裡面的 this 就會是什麼
箭頭函式會去用「一開始被定義好的 this 的值」

因為是在 run 裡面定義了這個箭頭函式,所以在箭頭函式裡面的 this 就會是「在 run 裡面我可以存取的到的這個 this


#javascript







Related Posts

Ternary 三元運算子

Ternary 三元運算子

程式基礎

程式基礎

[ week 4 ] HTTP協定

[ week 4 ] HTTP協定


Comments