# `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` 會被強制轉成全域物件,而嚴格模式下,將不會強制轉值。