Skip to main content

Command Palette

Search for a command to run...

JavaScript - this 是誰、指向哪裡,以及 call、apply、bind

Updated
3 min readView as Markdown
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)

// 在 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() 輸出 1test 被作為物件 obj.m 的參考屬性 (reference property, function is passed by reference),雖然是在最外層呼叫了 obj.m(),但這邊的 this 指向的是 function 的上層,也就是 obj

  • test() 輸出 undefined:當 test() 在 global scope 被調用, this 指向的是 window,而 windon 並沒有 x 這個變數所以是 undefined

再看看另一個例子:

var obj = {
    a:10,
    b:{
        fn:function(){
            console.log(this.a); //undefined 
        }
    }
}
obj.b.fn();

上述程式碼中, this 的上一層是 b 物件,但 b 內部沒有 a,所以是 undefined

將 function 賦值給全域變數再呼叫,this 則是全域物件

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 應該是 12this 應該是 b 才對呀,怎麼會是 undefinedwindow 呢?

雖然 fnb 的 method,但是 fn 賦值給 j 的時候並沒有執行,所以此時 this 的對象仍是全域變數所在的 window

當我們宣告全域變數 var j = obj.b.fn 時,實際上 jwindow.j,而執行 j() 的時候等同於執行 window.j() 。於是此時的 thiswindow ,而 this.xundefined

決定 this 的關鍵不在於它屬於哪個物件,而是在於 function「呼叫的時機點」。

  • 當你透過物件呼叫某個方法 (method) 的時候,此時 this 就是那個物件 (owner object)

  • 當你將 function 先賦值給全域變數,再呼叫全域變數執行 function,此時 this 指向的則是全域物件。

顯示綁定 Explicit Binding

透過 apply() / call() / bind() 的 function methods,改變 function 的 this

這些 methods 的第一個參數就是改變後調用這個 function 的對象,這時 this 指的就是第一個參數。

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

callapply

'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

'use strict';
function hello() {
  console.log(this)
}

const myHello = hello.bind('my')
myHello() // my

bind 會回傳一個新的 function,在這邊我們把 hello 這個 function 用 my 來綁定,所以最後呼叫 myHello() 時會輸出 my

一但 bind 了以後值就不會改變:

'use strict';
function hello() {
  console.log(this)
}

const myHello = hello.bind('my')
myHello.call('call') // my,即使用 call 將 this 修改為 'call' 仍不會改變

bind, call, apply 的差異

  • bind() 讓 function 在被呼叫前先綁定某個物件,使它不管怎麼被呼叫都能有固定的 thisbind() 尤其常用在像是 callback function 這種類型的場景,可以想像成是先綁定 this,然後讓 function 在需要時才被呼叫

  • .call().apply() 則是使用在 context 較常變動的場景,依照呼叫時的需要帶入不同的物件作為該 function 的 this。在呼叫的當下就立即執行。

在「非嚴格模式」底下,無論是用 call、apply 還是 bind,你傳進去的如果是 primitive 都會被轉成 object,舉例來說:

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 ? [下]」總結得很好,可以直接透過這樣的順序來辨別出 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

More from this blog

TypeScript - 型別斷言(Type Assertion)

型別斷言是 TypScript 的一種機制,允許我們手動指定一個明確的型別,提供 TypeScript 編譯器無法自行推斷的額外型別資訊。 我們可以透過這兩種寫法來指定型別斷言: 語法 尖括號 <>(angle-bracket)語法 語法:<type>value - 在需要斷言的變數前加上 <Type> 即可 const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas") as 語法 語法:value as...

Oct 15, 20232 min read

JavaScript - Scope 作用域 & Closure 閉包

Scope 作用域 留言 什麼是作用域 「作用域就是一個變數的生存範圍,一旦出了這個範圍,就無法存取到這個變數」。 當我們把變數 a 宣告在 function 中,function 之外的地方都無法取用這個變數: // 把變數宣告在 function 中 function test1() { var a = "hello" } console.log(a) // Uncaught ReferenceError: a is not defined 但當我們把變數 b 宣告在全域,fu...

Oct 3, 20236 min read
JavaScript - Scope 作用域 & Closure 閉包

JavaScript - Hoisting 提升

Hoisting 是怎麼發生的? 變數和函數的宣告會在編譯階段就被放入記憶體,但實際位置和程式碼中完全一樣。 從這段 MDN 對於 hoisting 的說明大概可以了解到,Javascript 在執行程式碼之前會先進行編譯,而在編譯的過程中會將變數宣告以及函式宣告提升 (hoist) 到該 scope 的頂端,但需注意這邊並非實際改動程式碼的位置。 JS 在運作時是分成「編譯」和「執行」兩個步驟。而 hoisting 是發生在編譯的階段。 JS 在編譯的階段會將變數及函式的宣告處理好(h...

Oct 3, 20233 min read
JavaScript - Hoisting 提升

Yachu's Dev Memo

18 posts