---
# System prepended metadata

title: '`this`'
tags: [函式, this, JavaScript 竹白記事本]

---

# `this`
>[竹白記事本](https://chupainotebook.blogspot.com/)
[JavaScript 竹白記事本 目錄](https://hackmd.io/@chupai/rkBjZjh7H)

###### tags: `JavaScript 竹白記事本` `函式` `this`

JavaScript 中的 `this` 關鍵字是最令人困惑的機制之一，真正掌握 `this` 的用法才是算是真正跨過 JS 的門檻。

## `this` 的誤解

`this` 有兩個因為過度解讀字面本身的意義，而造成的誤解。

以下兩者皆為**錯誤解讀**：
1. 自身（Itself）
2. 其作用域（It's Scope）

### 1. 自身

第一個常見的誤解是 `this` 參考到函式本身（the fuction itself）。

```javascript
function foo() {
  console.log(this);
}

foo();  // ?
```
這個 `this` 的值會是什麼？

答案會是全域物件（瀏覽器下是 `Window` 物件、node.js 底下是 `Global` 物件）。

這證明了 `this` 並不會指向函式本身。

### 2. 其作用域

另一個常見的誤解是 `this` 以某種方式參考了函式自身作用域。

考慮一段錯誤示範的程式碼：

這段程式碼嘗試跨作用域，並使用 `this` 隱含地參考一個函式的語彙作用域。想利用 `this` 在 `foo()` 與 `bar()` 的語彙作用建立一個通道，讓 `bar()` 能夠取用在 `foo()` 內層作用域的變數 `a`。
```javascript
function foo() {
  var a = 2;
  this.bar();
}

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

foo();   // ?
```
這個 `this` 的值會是什麼？

結果當然不可能會成功，會回傳 `undefined`。

## `this` 的指向 

說了那麼多，那麼 `this` 到底是什麼？

首先來看看 ECMAScript 標準規範對 `this` 的定義：
> `this` 關鍵字代表的值為目前執行環境的 ThisBinding.

MDN 對 `this` 的定義：
> 在大多數情況，`this` 會因為函式被呼叫的方式而有所不同。


直接說結論：
- `this` 是 JavaScript 的關鍵字之一。
- `this` 是函式執行時，自動生成的物件。
- `this` 的值與函式是在何處被宣告無關。
- `this` 的值完全取決於該函式的被呼叫的方式。
- 在大多數情況下，`this` 代表的就是呼叫函式的物件。


### 1. 一般的函式呼叫

直接呼叫函式的情況下，`this` 會指向全域物件。

一個簡單的範例：
```javascript
var name = 'GlobalName';

function foo() {
  var name = 'Chupai';
  console.log(this.name); 
}

foo();  // "GlobalName"
```

放到立即函式 IIFE ，直接在函式內直接在呼叫另一個函式：
```javascript
var name = 'GlobalName';

(function() {
  function foo() {
    var name = 'Chupai';
    console.log(this.name);
  }
  foo(); // "GlobalName"
})();
```
結果是一樣的。

無論我們把函式宣告放在立即函式內與外，結果還是一樣。
```javascript
var name = 'GlobalName';

function foo() {
  var name = 'Chupai';
  console.log(this.name);
}

(function() {
  foo(); // "GlobalName"
})();
```

閉包 Closure：
```javascript
var name = 'GlobalName';

function foo() {
  var name = 'Chupai';
  return function() {
    console.log(this.name);
  };
}

var myFoo = foo();

myFoo(); // "GlobalName"
```
這樣結果依然相同，並不會因為獨立的作用域改變造成 `this` 的不同。

回呼函式 Callback function：
```javascript
var name = 'GlobalName';

function foo() {
  var name = 'Chupai';
  
  function boo() {
    console.log(this.name);
  }
  
  boo();
}

foo();  // "GlobalName"
```
無論在哪一層， 一般的函式呼叫 `this` 都會指向全域物件。

為什麼 `this` 會指向全域物件，關於這部分會在下方 **[嚴格模式](#嚴格模式)** 說明。

### 2. 物件的方法呼叫

`this` 會指向最後呼叫它的物件。

一個簡單的範例：
```javascript
var name = 'GlobalName';

var obj = {
  name: 'Chupai',
  foo: function() {
    console.log(this.name);
  },
};

obj.foo(); // "Chupai"
```
`obj.foo` 為 `obj` 的方法，因此 `foo` 內的 `this` 會指向 `obj` 物件。

稍微改變一下程式碼：
```javascript
var name = 'GlobalName';

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

var obj = {
  name: 'Chupai',
  foo: foo
};

foo();      // "GlobalName"
obj.foo();  // "Chupai"
```
函式宣告的位置不重要，重要的是呼叫的方法，`obj.foo` 內的 `this` 還是會指向 `obj` 物件。

繼續看下個範例：
```javascript
var name = 'GlobalName';

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

var obj = {
  name: 'Chupai',
  boo: {
    name: 'Wang',
    foo: foo
  }
};

obj.boo.foo(); // "Wang"
```
`this` 會指向最後呼叫它的那個物件，最後呼叫的物件為 `obj` 物件內的 `boo` 物件，所以會指向它。


### 3. 間接參考
接下來看一個容易搞錯的範例，如果將物件內的函式，賦予在一個變數上，並呼叫它：
```javascript
var name = 'GlobalName';

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

var obj = {
  name: 'Chupai',
  foo: foo,
};

var callThisName = obj.foo;

callThisName(); // "GlobalName"
```
`obj.foo` 賦值給變數 `callThisName` 時，`foo` 並沒有被呼叫，也就是函式只是另一個函式的參考。因此 `callThisName()` 就只是一般的函式呼叫。

而當作參數傳遞中的回呼函式，也屬於間接參考：
```javascript
var name = 'GlobalName';

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

var obj = {
  name: 'Chupai',
  foo: foo,
};


function boo(fn) {
  fn();
}

boo(obj.foo); // "GlobalName"
```
參數傳遞只是一種隱含的指定，`boo(obj.foo)` 的參數 `fn` 依舊是 `obj.foo` 的參考，結果如同上一段程式碼。

如果回呼函式給它的那個函式不是本身，而是內建的，結果一樣。
```javascript
var name = 'GlobalName';

var obj = {
  name: 2,
  foo: foo,
};

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

setTimeout(obj.foo, 100); // "GlobalName"
```

## 事件監聽的 `this`

DOM 搭配事件監聽 `addEventListener` 時，監聽函式中的 `this` 會指向的則是該 DOM 物件。
```javascript
var dom = document.querySelector('body');

dom.addEventListener('click', function() {
  console.log(this);  // <body>...</body>
});
```

## 改變 `this` 的指向

會改變 `this` 的指向的情況：
- 使用 `apply`、`call`、`bind` 方法
- `new` 建構一個物件實體
- 使用 ES6 的箭頭函式

### 1.`apply`、`call`、`bind` 方法

在 JavaScript 有三個可以強制指定 `this` 的方式，分別是 `apply`、`call`、`bind` 方法。

#### 1.1 `apply`、`call` 方法

`call()` 與 `apply()` 是能呼叫函式的方法，並且能指定 `this` 值：
```javascript
var obj = {};

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

foo();          // "Window{}"
foo.call(obj);  // Object{}
foo.apply(obj); // Object{}
```
兩者第一個參數都是 `this` 值，也就是要綁定的物件。

而兩者差異只在於後面的參數：
```javascript
var obj = {};

function foo(a, b) {
  console.log(this, a, b);
}

foo.call(obj, 1, 2);    // Object{} 1 2
foo.apply(obj, [1, 2]); // Object{} 1 2
```
- `call()` 跟平常呼叫函式一樣
- `apply()` 需要使用陣列將引數包起來

應用 `call()` 與 `apply()` 解決前面所提到的函式間接參考問題：
```javascript
function foo() {
  console.log(this.name);
}

var obj = {
  name: 2,
};

var boo = function() {
  foo.call(obj);
};

boo(); // 2
setTimeout(boo, 100); // 2
boo.call(window);     // 2
```
建立 `boo` 函式 在內部手動呼叫 `foo.call( obj )`，藉此強制以 `obj` 作為 `this` 的綁定來呼叫 `foo`。不管如何呼叫 `boo` 函式都會以 `obj` 手動調用 `foo`。

此模式被稱為硬綁定（hard binding） 指的是綁定既明確又不會意外變回預設的綁定。

#### 1.2 `bind()`
因為硬綁定模式如此常用，ES5 新增了一個方法，將此模式包裝了起來，`bind()` 會回傳一個新的函式，當被呼叫時，將提供的值設為 `this` 值。

這範例與上段程式碼一模一樣：
```javascript
function foo() {
  console.log(this.name);
}

var obj = {
  name: 2,
};

var boo = foo.bind(obj);

boo(); // 2
setTimeout(boo, 100); // 2
boo.call(window);     // 2
```

後面的參數平常呼叫函式一樣：
```javascript
var obj = {};

function foo(a, b) {
  console.log(this, a, b);
}

const myFoo = foo.bind(obj, 1, 2);

myFoo(); // Object{} 1 2
```


如果使用 [`name`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/name) 屬性查看 `bind()` 所創建的函式，將會在函式的名稱前加上 `"bound "`。
```javascript
var obj = {};

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

const myFoo = foo.bind(obj);

console.log(myFoo.name);  // "bound foo"
```

### 2. `new` 建構一個物件實體
這裡必須注意，JavaScript 的 `new` 運算子看似與其他類別導向語言相同，但實際上跟類別導向的功能性並沒有關聯。對 JavaScript 的 `new` 來說，它並不會連接到類別上，也不會實體化一個類別。

當一個函式前面帶有 `new` 被呼叫時，會發生以下事情：
- 會有一個無中生有的全新物件被建構出來
- 新建構的物件會帶有 `[[ prototype ]]` 連結
- 新建構的物件會被設為那個函式的呼叫的 `this` 繫結
- 除非該函式回傳自己提供的替代物件，否則以這個 `new` 調用的函式呼叫會自動回傳這個新建構物件。

```javascript
function Foo(a) {
  this.a = a;
}

var bar = new Foo(2);
console.log(bar.a); // 2
```
呼叫 `foo()` 時，前面加了 `new`，所以建構出一個新物件，`foo()` 沒有回傳值，所以建立，使新的物件 `bar` 被設為 `foo()` 呼叫的 `this`。

此部分只要了解建構式的 `this` 是指向物件本身即可。

>關於更多[《建構式》](/SJNRww8Lr)會在這裡說明。

### 3. 箭頭函式

ES6 新增的箭頭函式，它本身並沒有 `this`，它會在定義時記住 `this` 值，也就在宣告它的地方的 `this` 是什麼，它的 `this` 就是什麼。

傳統函式的 `this` 是依呼叫的方法而定，因此當你的函式有好幾層時，會遇到一個問題：
```javascript
function foo() {
  function boo() {
    console.log(this.a);
  }
  boo();
}

var obj = {
  a: 2,
  foo: foo
};

obj.foo();  // undefined
```
在函式內的函式會指向全域物件，所以找不到屬性 `a`。

在 ES6 前，解決辦法是利用一個變數儲存 `this` 的值（常見命名 `_this`、`that`、`vm`、`self`）。
```javascript
function foo() {
  var _this = this;
  function boo() {
    console.log(_this.a);
  }
  boo();
}

var obj = {
  a: 2,
  foo: foo
};

obj.foo();  // 2
```

接下來看 ES6 新增的箭頭函式，它的 `this` 始終指向函式定義時的 `this`，而非執行時。
```javascript
function foo() {
  var _this = this;
  var boo = ()=> {
    console.log(this.a);
  }
  boo();
}

var obj = {
  a: 2,
  foo: foo
};

obj.foo();  // 2
```

遇到回呼函式也是一樣：
```javascript
function foo() {
  setTimeout(function() {
    console.log(this.a);
  }, 1000);
}

let obj = {
  a: 2,
  foo: foo,
};

obj.foo();  // undefined 
```
```javascript
function foo() {
  setTimeout(() => {
    console.log(this.a);
  }, 1000);
}

let obj = {
  a: 2,
  foo: foo,
};

obj.foo();  // 2
```

### 3.1 不可使用的情況
箭頭函式中 `this` 是被綁定的，所以套用 `apply`、`call`、`bind` 的方法時是無法修改 `this`。

```javascript
var name = 'GlobalName';

var obj = {
  name: 'Chupai',
};

const foo = () => {
  console.log(this.name);
};

foo.call(obj); // "GlobalName"
foo.apply(obj); // "GlobalName"

var myFoo = foo.bind(obj);
myFoo(); // "GlobalName"
```

箭頭函式也不能用在建構式，會拋出錯誤。
```javascript
const Foo = (a, b) => {
  this.a = a;
  this.b = b;
};

const myFoo = new Foo(1, 2); // Uncaught TypeError: Foo is not a constructor
```

用在監聽 DOM 上一樣會指向全域物件，因為 `this` 是指向所建立的物件上。

```javascript
var e = document.querySelector('body');

var changeDOM = () => {
  console.log(this);  // Window{}
};

e.addEventListener('click', changeDOM);
```

## 嚴格模式

ES5 之後，新增了嚴格模式，在嚴格模式下，一般函式呼叫的 `this` 值都是 `undefined`。
```javascript
'use strict';
function foo() {
  console.log(this);
}

foo(); // undefined
```

`undefined` 與全域物件有什麼關係？

先來一段程式碼：
```javascript
function foo() {
  console.log(this);
}

foo.call(undefined); // Window{}
foo.call(null); // Window{}
```
我們使用 `call()` 將 `this` 值設為 `undefined`，結果卻回傳全域物件。

這是因為 JavaScript 的機制，當 `this` 值為 `undefined` 或 `null` 時，會將 `this` 值強制轉換為一個物件。

在嚴格模式下，刻意將 `undefined` 或 `null` 設為 `this` 值，會回傳正確的 `this` 值。
```javascript
'use strict';
function foo() {
  console.log(this);
}

foo.call(undefined); // undefined
foo.call(null); // null
```

這就是為什麼一般函式呼叫會回傳全域物件的原因。

有的書會用「`this` 永遠指向最後呼叫它的那個物件」來解釋下面這段程式碼：
```javascript
function foo() {
  console.log(this);
}

foo(); // Window{}
window.foo(); // Window{}
```
因為 `foo()` 等同 `window.foo()`，最後呼叫它的物件是全域物件，所以 `this` 指向全域物件。但這其實是不太正確的說法，`this` 值主要還是以函式的呼叫方式為主，`foo()` 的 `this` 值會是 `undefined`，會得到全域物件是因為被強制給值了。

讓我們加上嚴格模式：
```javascript
'use strict';
function foo() {
  console.log(this);
}

foo(); // undefined
window.foo(); // Window{}
```
`window.foo()` 的值還是指向全域物件，因為它是方法呼叫。

## 總結

### 1. 函式的四種呼叫方式

- 作為函式 `func()`，函式的一般呼叫形式
- 作為方法 `obj.func()`，將函式綁定到物件上，以提供物件導向的程式設計方式
- 作為建構式 `new Func()`，用來創建新物件
- 經由函式的 `apply()` 或 `call()` 方法呼叫

### 2. 呼叫函式的方式會影響 `this` 的值

- 直接作為函式來呼叫，通常 `this` 的值為全域物件，嚴格模式下為 `undefined`。
- 函式作為方法來呼叫，`this` 的值為被呼叫函式的所屬物件
- 函式作為建構式來呼叫，`this` 的值為新建立的物件
- 藉由 `apply()` 或 `call()` 呼叫，`this` 的值由第一個參數決定

### 3. 箭頭函式

箭頭函式沒有自己的 `this` 的值，由建立時取得它。

### 4. `bind()`

所有函式都具備 `bind()` 來建立一個新函式，此函式會綁定傳入的引數，除此之外，綁定的函式運作如原始的函式。

### 5. 嚴格模式

當 `this` 值為 `undefined` 或 `null` 會被強制轉成全域物件，而嚴格模式下，將不會強制轉值。
