# This
## this 是什麼
this 是 JavaScript 的一個關鍵字,函式執行時自動生成的一個內部物件,用來呼叫自身擁有的屬性或值。
## 基本觀念
* 每個執行環境都有屬於自己的 this 關鍵字
* this 與函式如何宣告沒有關聯,僅與 **呼叫方法** 有關
* 嚴格模式下,簡易呼叫會有很大的改變
### This 的用途
可以略過函式的定義方式,依據 **執行的方式** 取用特定物件。
## 影響 This 的調用方式
* ⭐ 作為物件方法 (最常運用 this 的方法)
* 簡易呼叫 (絕大多數的呼叫方式)
⭐ **盡量避免使用簡易呼叫裡的 this**
* bind、apply、call 方法
* new
* DOM 事件處理器
* 箭頭函式 (ES6)
## 透過物件的方法調用 ⭐
:::info
* this 與函式如何宣告沒有關聯,僅與 **呼叫方法** 有關
* 物件的方法調用時,僅需關注 **是在哪一個物件** 下呼叫
:::
在以下範例中,定義一個 `family` 物件,裡面分別有 `myName` 和 `callName` 屬性,`callName` 則是調用外層的 `callName` 函式。
而無論函式是如何定義的,只看是在哪個物件下呼叫 this,因此 `callName` 裡面的 this 就會指向 `family` 物件。
```javascript=
var myName = 'ming'
function callName() {
console.log(this.myName) // 小明家
}
var family = {
myName: '小明家',
callName: callName,
Ming: {
myName: '大明家',
callName: callName, // 大明家
}
}
family.callName()
family.Ming.callName()
```
接下來稍微修改上面的範例,把 `family` 物件內的 `callName` 改成匿名函式,在外層宣告新的變數,並把物件內的 `callName` 取出來,最後直接執行 `callName()`。
雖然在範例中,物件內的 `callName` 被重新定義,但 **實際執行的環境是在全域下**,因此取得的 this 就會是全域,而 `this.myName` 的結果會是 `ming`。
```javascript=
var myName = 'ming'
var family = {
myName: '小明家',
callName: function() {
console.log(this.myName) // 'ming'
}
}
var callName = family.callName
callName()
```
### This 與靜態作用域 :zap:
this 的調用方式經常和靜態作用域搞混,this 取決 **是在哪一個物件** 下被呼叫,而函式建立時就已經確定作用域,直接查找 `myName` 並不會因為呼叫位置而改變結果。
```javascript=
var myName = 'ming'
function callName() {
console.log(this.myName, myName)
// 小明家, ming
}
var family = {
myName: '小明家',
callName: callName
}
family.callName()
```
## 透過簡易呼叫調用 Simple Call
:::danger
盡量避免使用簡易呼叫裡的 this,它的本質其實是 undefined
:::
### 1. 直接呼叫
最簡單判斷是否為 simple call 的方式,就是判斷函式是否被直接呼叫,如以下範例就是基本的 simple call。
```javascript=
var scope = 'global'
function callName() {
console.log(this.scope) // global
}
callName();
```
### 2. 立即函式 IIFE
立即函式會在定義後會被立刻執行,本身也屬於 simple call。
以下範例程式碼在立即函式內又有定義另外一個函式 `callSomeone`,定義完後也是立刻執行,所以也屬於 simple call。
```javascript=
var scope = 'global'
(function() {
console.log(this.scope) // global
function callSomeone() {
console.log(this.scope) // global
}
callSomeone()
})()
```
我們所知道的全域變數都是掛在 window 這個全域物件下,雖然 simple call 會調用全域 this,但不代表呼叫函式的時候是直接執行 window 底下的函式。
如果把立即函式內的 `callSomeone` 函式改成 `window.callSomeone()`,此時就會發生 <font color="red">`window.callSomeone is not a function`</font> 的錯誤。
### 3. 閉包
閉包是在函式內再定義另一個函式,並取用外層變數。
在執行閉包函式之前通常會先宣告一個新的變數,並將外層函式賦予這個新的變數,最後再執行函式。
所以實際上在執行的環境是在全域底下,因此 this 就會指向全域。
```javascript=
var scope = 'global'
function storMoney() {
var money = 100
return function(price) {
money = money + price
console.log(this.scope, money)
}
}
var totalMoney = storMoney()
totalMoney(100) // global, 200
```
### 4. Callback
Callback 是將 `A函式` 以參數的形式傳入 `B函式`,並在 `B函式` 內執行,此時函式會以 simple call 的形式在 `B函式` 內直接執行,所以 this 也會指向全域。
```javascript=
var scope = 'global'
function fnA() {
var scope = 'local'
console.log(this.scope) // global
}
function fnB(fn) {
return fn()
}
fnB(fnA)
```
<!--
```javascript=
var scope = 'global'
function fnA(fn) {
var obj = {
scope: 'local',
fn
}
obj.fn()
}
fnA(function() {
console.log(this)
})
``` -->
#### forEach
Callback 除了自己寫的函式外,也有許多內建的 callback 函式,這邊以 `forEach` 來說明:
`forEach` 裡面要帶入一段 callback 函式,所以會取到全域變數,裡面帶入的參數 `i` 分別是陣列裡面的值。
```javascript=
var scope = 'global'
var arr = [1, 2, 3]
arr.forEach(function(i) {
console.log(this.scope, i)
})
// global 1
// global 2
// global 3
```
#### setTimeout
以下範例程式碼中,雖然 `setTimeout` 在物件底下的 `callName` 裡,但他還是一個 callback 函式,所以還是屬於 simple call,也就會指向全域變數。
如果想要取用物件裡的區域變數,可以在 `setTimeout` 函式外層把 this 賦予一個新的變數 `vm`,此時就會把 this 重新指向到 `callName` 函式,接著因為 **範圍鍊** 的關係會向外尋找 `scope` 變數,結果就會是 `local`。
```javascript=
var scope = 'global'
var obj = {
scope: 'local',
callName: function() {
var vm = this
setTimeout(function() {
console.log(this.scope, vm.scope)
// global, local
}, 1000)
}
}
obj.callName()
```
## call、apply、bind 方法
call、apply、bind 是三種會影響 this 指向的方法,在 **[嚴格模式](#嚴格模式)** 和非嚴格模式下會有些許不同。
### call
```
fun.call(thisArg[, arg1[, arg2[, ...]]])
```
在 MDN 文件中提到,`call()` 方法是用來呼叫函式,並且給予特定的 this 參數。
1. 在使用 `call()` 方法呼叫函式時,帶入的第一個參數就會是指定的 this
2. 在 **一般模式下**,`null`、`undefined` 將會被替換成全域變數,而原生型別的值將會以建構式的方式封裝
#### 一般模式
```javascript=
var scope = 'global'
var obj = {
scope: 'local'
}
function fn(para1, para2) {
console.log(this, para1, para2)
}
fn.call(1, 2) // Number {1} 2 undefined
fn.call(obj, 1, 2) // {scope: "local"} 1 2
fn.call(undefined, 1, 2) // window 1 2
fn.call(null, 1, 2) // window 1 2
```
#### 嚴格模式
```javascript=
function fnStrict(para1, para2) {
'use strict'
console.log(this, para1, para2)
}
fnStrict.call(1, 2) // 1 2 undefined
fnStrict.call(obj, 1, 2) // {scope: "local"} 1 2
fnStrict.call(undefined, 1, 2) // undefined 1 2
fnStrict.call(null, 1, 2) // null 1 2
```
### apply
`apply()` 在使用上和 `call()` 幾乎一樣,差別在於 `call()` 可以傳入一連串參數,而 `apply()` 只能傳入一組陣列作為參數。
```
fun.apply(thisArg, [argsArray])
```
### bind
```
fun.bind(thisArg[, arg1[, arg2[, ...]]])
```
在前面提到的 `call()` 和 `apply()` 都可以直接呼叫來調用函式,而 `bind()` 則需要另外宣告變數才會執行。
另外需要特別注意的是,`bind()` **在宣告變數後會綁定參數,之後就不能再修改**。
能夠改變傳入參數的情況只有兩種:
1. 宣告變數時只有部分寫入
2. 重新宣告變數
```javascript=
var scope = 'global'
var obj = {
scope: 'local'
}
function fn(para1, para2) {
console.log(this, para1, para2)
}
var newFn = fn.bind(obj, 1, 2)
newFn(3, 4) // {scope: "local"} 1 2
```
#### 部分寫入
```javascript=
var newFn = fn.bind(obj, 3)
newFn(4) // {scope: "local"} 3 4
```
#### 重新宣告
```javascript=
var newFn = fn.bind(obj, 1, 2)
newFn() // {scope: "local"} 1 2
var newFn = fn.bind(obj, 3, 4)
newFn() // {scope: "local"} 3 4
```
## 嚴格模式
* 加入 `'use strict'`
* 不會影響不支援嚴格模式的瀏覽器
* 可依據執行環境設定 `'use strict'`,可以加在全域也可以是特定函式內
* 透過拋出錯誤的方式消除一些安靜錯誤 (防止小錯誤)
* 禁止使用一些有可能被未來版本 ECMAScript 定義的語法
舉例來說,原本在非嚴格模式下,如果直接對變數賦值時並不會跳出錯誤,但是在嚴格模式下,就會要求一定要先宣告變數。
```javascript=
(function() {
a = '小明' // Pass
})();
(function() {
'use strict'
b = '大明' // b is not defined
})();
```
因此我們用嚴格模式的方式來查看當函式為 simple call 時,this 分別的指向。當在非嚴格模式下,this 指向全域 window,但在嚴格模式下卻會回傳 undefined。
這是因為在一般模式下,概念就如同使用 `call()` 這個方法,只是並不會傳入 this 這個參數,因此 **this 這個參數是使用 undefined 來替代**,在進入嚴格模式時,undefined 就會維持原本的 undefined,這也是為什麼要盡量避免使用 simple call 的 this 的原因。
```javascript=
(function() {
console.log(this) // window
})();
(function() {
'use strict'
console.log(this) // undefined
})();
```
## 常見陷阱題 :zap:
### 改變 callback 函式 this 指向
大部分 callback 函式的 this 都會指向全域,但還是有特例。
例如以下程式碼中,在外部函式帶入一個 callback 函式,並查看 this 指向。
雖然是 callback 函式,但實際上執行的地方是在 `fn` 函式內以物件的方式調用,因此 this 會指向父層 `obj` 物件。
```javascript=
var scope = 'global'
function fn(callback) {
const obj = {
scope: 'local',
callback
}
obj.callback() // 實際調用的地方
}
fn(function() {
console.log(this); // obj
})
```
### bind 使用情境
這題的關鍵在於,目前不是在嚴格模式的開發環境下,如果帶入 null 或 undefined 會指向全域,而其他參數則會依序綁定,因此 `this.scope` 會回傳 global。
```javascript=
var scope = 'global'
var obj = {
scope: 'local',
fn: function(a, b, c) {
return console.log(this.scope, a, b, c)
}
}
var fnA = obj.fn
var fnB = fnA.bind(null, 0)
fnB(1, 2)
// global 0 1 2
```
### This 的觀念
```javascript=
var scope = 'global'
var obj = {
scope: 'local',
fn: function() {
return this.value
}
}
console.log(obj.fn()) // local
```
#### 表達式
這兩段程式碼要表達的是 `()` 立刻執行裡面的程式片段,主要觀念在於 **裡面是一段表達式**,而表達式要回傳一個結果,這個結果就是 `obj.fn`,也就是直接執行`obj.fn`,因此會得到 global 的結果。
```javascript=
console.log((obj.fn = obj.fn)()) // global
```
```javascript=
console.log((false || obj.fn)()) // global
```
### Callback
#### 1. parseInt
```javascript=
var arr = [1, 2, 3].map(parseInt)
console.log(arr) // 1, NaN, NaN
```
這一題的關鍵在於 **parseInt**,通常 parseInt 只會用到一個參數,但這裡用到第二個參數,第一個是表達式,第二個則是進位數。
而 map 的 callback 函式會帶入三個參數,元素、索引、陣列。
因為只會用到兩個,所以陣列會被忽略,但是第二個索引卻會被帶入 parseInt,因此展開後程式會變成這樣:
當進位數為 0 的時候會以預設為主(在 JavaScript 通常是十進位),因此結果會是 NaN。
```javascript=
var arr = [1, 2, 3].map(function(item, i) {
return parseInt(item, i)
// parseInt(1, 0)
// parseInt(2, 1)
// parseInt(3, 2)
})
```
#### 2. 回傳觀念
在這段程式碼中,執行流程如下:
1. `function c` => `console.log('Casper')`
2. `function b` => 將 `function c` 以參數的方式帶入 b 並執行
3. `function a` => 執行完 b 但並沒有回傳結果,因此 a 參數為 undefined
4. undefined => `a is not a function`
```javascript=
function a(a) {
a()
}
function b(b) {
b()
}
function c(c) {
console.log('Casper')
}
a(b(c)) // Casper / a is not a function
```
---
## 參考資料
* [JavaScript 核心篇](https://www.hexschool.com/courses/js-core.html)
* [parseInt()](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/parseInt)