# THIS 的真實身分

# this 不等於 function

this 代表的是 function 執行時所屬的物件,而不是 function 本身

const foo = function() {
  this.count++;
};

foo.count = 0;

for (var i = 0; i < 5; i++) {
  foo();
}

console.log(foo.count); // 0
console.log(count); // NaN
1
2
3
4
5
6
7
8
9
10
11
12

foo()for 迴圈裡面運行時, this.count++ 始終都是對 window.count 在做遞增的處理,因為這個時候的 this 實際上就是 window

window.count 理論上一開始會是 undefined,在做了五次的 ++ 之後,會得到一個 NaN 的結果,而 foo.count 依然是個 0

# 物件的方法調用

當函式是使用傳統的定義方式時(function 而不是 ()=>),並且只有在 物件下 調用時,this 會指向前一個物件,如果不是,大多情況會指向 window

傳統函式如何定義不會影響 this 的指向,會影響指向的只有 調用方式


在物件內的傳統函式被呼叫時,如果出現 立即函式、回呼函式(callback function),絕大數的情況下這類型都會指向 window

var myName = '全域';

// 主要程式碼
const Ming = {
  myName: '小明',
  fn: function() {
    setTimeout(function() {
      console.log(this.myName);
    }, 0);
  },
};
Ming.fn(); // 全域
1
2
3
4
5
6
7
8
9
10
11
12

# 框架、原型與 this

假設要產生 card 組件和 navbar 組件,我們可以運用到建構函式,其中共用到屬性有 datamethodsrender,而 render 為共用方法,所以將 render 定義為原型的方法。

使用建構函式的目的:

  • 定義物件的結構(方法會先放在 methods 內,此結構與 Vue 是接近的)
  • 將共用的方法使用原型來定義
function Component(obj) {
  const vm = this;

  // 取出 methods 屬性內的物件,定義為各自的方法
  const methods = Object.keys(obj.methods);
  if (methods.length) {
    methods.forEach(function(key) {
      vm[key] = obj.methods[key];
    });
  }
  vm.data = obj.data;
  vm.target = obj.target;
}

// 共用的方法使用原型來定義
Component.prototype.render = function() {
  console.log(`這個是 ${this.data},可以繪製於 ${this.target}`);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

透過上面定義的建構函式來建立組件:






 





const navbar = new Component({
  data: '導覽列的資料',
  target: '導覽列',
  methods: {
    trigger: function() {
      this.render();
    },
  },
});
navbar.trigger(); // 這個是 導覽列的資料,可以繪製於 導覽列
1
2
3
4
5
6
7
8
9
10

# 強制指定 this 的方式

如果透過 ajax function 發送請求,在它的 callback function 中,this 通常會指向 window,而我們可以透過 call()apply()bind() 強制指定 this

# bind()









 



el.addEventListener('click', function(event) {
  console.log(this.textContent);

  // 透過 .bind(this) 來強制指定該 scope 的 this
  $ajax(
    '[URL]',
    function(res) {
      console.log(this.textContent, res);
    }.bind(this)
  );
});
1
2
3
4
5
6
7
8
9
10
11

# call()apply()

基本上 .call() 或是 .apply() 都是去呼叫執行這個 function ,並 將這個 function 的 context 替換成第一個參數帶入的物件。 換句話說,就是強制指定某個物件作為該 function 執行時的 this

.call().apply() 的作用完全一樣,差別只在 傳入參數的方式有所不同





 
 

function func( arg1, arg2, ... ){
  // do something
}

func.call( context, arg1, arg2, ... );
func.apply( context, [ arg1, arg2, ... ]);
1
2
3
4
5
6

# 比較差異

  • bind() - 讓這個 function 在呼叫前先綁定某個物件,使它不管怎麼被呼叫都能有固定的 this
  • call()apply() - 使用在 context 較常變動的場景,依照 呼叫時的需要帶入不同的物件 作為該 function 的 this。在呼叫的當下就立即執行。

# this 與前後文本 (context) 綁定的基本原則

# 預設綁定 (Default Binding)

當 function 是在 普通、未經修飾的情況下被呼叫,也就是當 function 被呼叫的當下如果沒有值或是在 func.call(null)func.call(undefined) 此類的情況下,此時裡面的 this自動指定至全域物件

但若是加上 "use strict" 宣告成嚴格模式後,原本預設將 this 綁定至全域物件的行爲,會轉變成 undefined。

# 隱含式綁定 (Implicit Binding)

即使 function 被宣告的地方是在全域範圍中,只要它成為某個物件的參考屬性 (reference property),在那個 function 被呼叫的當下,該 function 即被那個物件所包含。

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

const obj = {
  a: 2,
  foo: func,
};

func(); // undefined
obj.foo(); // 2
1
2
3
4
5
6
7
8
9
10
11

和「範圍鏈」(Scope Chain) 不同的是,決定 this 的關鍵不在於它屬於哪個物件,而是在於 function 呼叫的時機點,當你透過物件呼叫某個方法 (method) 的時候,此時 this 就是那個物件 (owner object)。

# 顯示綁定 (Explicit Binding)

簡單來說就是透過 bind()call()apply() 這類直接指定 this 的 function 都可被歸類至顯式綁定的類型。

# new 關鍵字綁定

當一個 function 前面帶有 new 被呼叫時,會發生:

  • 會產生一個新的物件 (物件被建構出來)
  • 這個新建構的物件會被設為那個 function 的 this 綁定目標,也就是 this 會指向新建構的物件
  • 除非這個 function 指定回傳 (return) 了他自己的替代物件,否則這個透過 new 產生的物件會被自動回傳

# Cascade

也有人稱為 Fluent Interface。

當每次都是針對同一個物件屬性作運算更新,那麼可以這樣寫:





 





 




 




 



const calNum = function(num) {
  this.num = num;

  this.add = function(newNum) {
    this.num += newNum;
    return this;
  };

  this.sub = function(newNum) {
    this.num -= newNum;
    return this;
  };

  this.multi = function(newNum) {
    this.num *= newNum;
    return this;
  };

  this.division = function(newNum) {
    this.num /= newNum;
    return this;
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

於是,可以在一行內把對 num 的運算搞定:

const count = new calNum(100);

count.add(100).sub(50);
console.log(count.num); // 150
1
2
3
4

# 參考

學好 this 前,先搞清楚 this 做什麼 (opens new window)

What's THIS in JavaScript (opens new window)

函式的 Combo 技: Cascade (opens new window)

Last Updated: 2020/11/12 上午4:23:18