# JavaScript - this 是誰、指向哪裡,以及 call、apply、bind ## this 是什麼 - `this` 是 JavaScript 的一個關鍵字 - `this` 是 function 執行時,自動生成的一個內部物件 - 隨著 function 執行場合的不同,`this` 所指向的值,也會有所不同 - `this` 與 function 在何處被宣告完全無關,而是取決於 function 被呼叫的方式 - 在大多數的情況下, `this` 代表的就是呼叫 function 的物件 (owner Object of the function) - 當 function 是某個 object 的 method,`this` 指的就是上層物件 ## this 的指向(綁定規則) ### 默認綁定 Default Binding 當 function 被呼叫的當下如果沒有值或在 `func.call(null)` 、`func.call(undefined)` 這類的情況下,此時 function 裡的 `this` 會自動綁定至**全域物件**: 在嚴格模式下是 `undefined` ;非嚴格模式底下就是**全域物件**: - 瀏覽器底下是 `window` - node.js 底下是 `global` ### 隱式綁定 Implicit Binding 即使在 global scope 宣告了 function,只要它成為某個 object 的參考屬性 (reference property),在那個 function **被呼叫的當下**,該 function 即被那個物件所包含。Function 可以作為某個 object 的 method 調用,這時 `this` 指的就是這個上層物件。 #### 透過 object 呼叫 method 時, `this` 就是那個物件 (owner object) ```javascript // 在 global 宣告了 `test` 函式 function test() { console.log(this.x); } var obj = {}; obj.x = 1; obj.m = test; // `test` 函式被傳址為 obj 物件的屬性(賦值但沒有被呼叫) // 在 global 透過 `obj` 物件呼叫 `m` method obj.m(); // 1 test(); // undefined ``` 上面這個例子中, - `obj.m()` 輸出 `1`:`test` 被作為物件 `obj.m` 的參考屬性 (reference property, function is passed by reference),雖然是在最外層呼叫了 `obj.m()`,但這邊的 `this` 指向的是 function 的上層,也就是 `obj`。 - `test()` 輸出 `undefined`:當 `test()` 在 global scope 被調用, `this` 指向的是 `window`,而 `windon` 並沒有 `x` 這個變數所以是 `undefined` 再看看另一個例子: ```javascript var obj = { a:10, b:{ fn:function(){ console.log(this.a); //undefined } } } obj.b.fn(); ``` 上述程式碼中, `this` 的上一層是 `b` 物件,但 `b` 內部沒有 `a`,所以是 `undefined`。 #### 將 function 賦值給全域變數再呼叫,`this` 則是全域物件 ```javascript var obj = { a:10, b:{ x:12, fn:function(){ console.log(this.x); //undefined console.log(this); //window } } } var j = obj.b.fn; j(); ``` 這邊可能會誤以為 `fn` 的上一層是 `b` ,這樣 `x` 應該是 `12` 、`this` 應該是 `b` 才對呀,怎麼會是 `undefined` 跟 `window` 呢? 雖然 `fn` 是 `b` 的 method,但是 **`fn` 賦值給 `j` 的時候並沒有執行**,所以此時 `this` 的對象仍是全域變數所在的 `window`。 當我們宣告全域變數 `var j = obj.b.fn` 時,實際上 `j` 是 `window.j`,而執行 `j()` 的時候等同於執行 `window.j()` 。於是此時的 `this` 是 `window` ,而 `this.x` 是 `undefined`。 **決定 `this` 的關鍵不在於它屬於哪個物件,而是在於 function「呼叫的時機點」**。 :::info - 當你**透過物件呼叫某個方法 (method)** 的時候,此時 `this` 就是那個物件 (owner object) - 當你將 function 先賦值給全域變數,再呼叫全域變數執行 function,此時 `this` 指向的則是全域物件。 ::: ### 顯示綁定 Explicit Binding 透過 `apply()` / `call()` / `bind()` 的 function methods,改變 function 的 `this`。 這些 methods 的第一個參數就是改變後調用這個 function 的對象,這時 `this` 指的就是第一個參數。 ```javascript var x = 0; function test() {  console.log(this.x); } var obj = {}; obj.x = 1; obj.m = test; obj.m() // 1 obj.m.apply(obj) // 1 ``` #### `call` 跟 `apply` ```javascript 'use strict'; // 嚴格模式 function hello(a, b){ console.log(this, a, b) } // 直接呼叫 function hello(1, 2) // undefined 1 2 // call hello.call(this 的值, 1, 2) // this的值 1 2 // apply - 要傳進去的參數是 array hello.apply(this 的值, [1, 2]) // **undefined** 1 2 ``` - `.call()` 傳入參數的方式是由「逗點」隔開 - `.apply()` 則是傳入整個陣列作為參數 - 第一個參數就是 `this` 的值:第一個參數傳什麼,裡面 this 的值就會是什麼。儘管原本已經有 this,也依然會被這種方法給覆蓋掉 #### `Bind` ```javascript 'use strict'; function hello() { console.log(this) } const myHello = hello.bind('my') myHello() // my ``` `bind` 會回傳一個新的 function,在這邊我們把 hello 這個 function 用 `my` 來綁定,所以最後呼叫 myHello() 時會輸出 `my`。 一但 `bind` 了以後值就不會改變: ```javascript 'use strict'; function hello() { console.log(this) } const myHello = hello.bind('my') myHello.call('call') // my,即使用 call 將 this 修改為 'call' 仍不會改變 ``` ### bind, call, apply 的差異 - `bind()` 讓 function 在被呼叫前先綁定某個物件,使它不管怎麼被呼叫都能有固定的 `this`。`bind()` 尤其常用在像是 callback function 這種類型的場景,可以想像成是先綁定 `this`,然後讓 function 在需要時才被呼叫 - 而 `.call()` 與 `.apply()` 則是使用在 context 較常變動的場景,依照呼叫時的需要帶入不同的物件作為該 function 的 `this`。在呼叫的當下就立即執行。 在「非嚴格模式」底下,無論是用 call、apply 還是 bind,你傳進去的如果是 primitive 都會被轉成 object,舉例來說: ```javascript function hello() { console.log(this) } hello.call(123) // [Number: 123] const myHello = hello.bind('my') myHello() // [String: 'my'] ``` ### this 綁定的優先順序 當「隱含式綁定」與「顯式綁定」衝突時,此時 this 會以「顯式綁定」為主 ## 總結 我覺得這篇[「What's THIS in JavaScript ? [下]」](https://kuro.tw/posts/2017/10/20/What-is-THIS-in-JavaScript-%E4%B8%8B/)總結得很好,可以直接透過這樣的順序來辨別出 `this` 到底是誰。 > 綜合上述介紹,我們可以簡單總結出一個結論: - 這個 function 的呼叫,是透過 `new` 進行的嗎? 如果是,那 `this` 就是被建構出來的物件。 - 這個 function 是以 `.call()` 或 `.apply()` 的方式呼叫的嗎? 或是 function 透過 `.bind()` 指定? 如果是,那 `this` 就是被指定的物件。 - 這個 function 被呼叫時,是否存在於某個物件? 如果是,那 `this` 就是那個物件。 - 如果沒有滿足以上條件,則此 function 裡的 `this` 就一定是全域物件: `window` 或是 `global`,在嚴格模式下則是 `undefined`。 > 而決定 `this` 是誰的關鍵: - function 可以透過 `.bind()` 來指定 this 是誰。 - 當 function 透過 `call()` 或 `apply()` 來呼叫時, `this` 會指向第一個參數,且會立即被執行。 - callback function 內的 `this` 會指向呼叫 callback function 的物件。 - ES6 箭頭函數內建 `.bind()` 特性,此時 `this` 無法複寫。 ## Ref - [What's THIS in JavaScript ?](https://kuro.tw/posts/2017/10/12/What-is-THIS-in-JavaScript-%E4%B8%8A/) - [面试官:谈谈this对象的理解](https://github.com/febobo/web-interview/issues/62)