# `this`
>[竹白記事本](https://chupai.github.io/),學習紀錄。
###### 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` 會被強制轉成全域物件,而嚴格模式下,將不會強制轉值。