Day 16:this 關鍵字 (2)

函數執行環境下 (Function Context) (續)

2. 簡易呼叫 (Simple Call)

this 物件:

  • 一般模式下:Global 物件。

  • 嚴謹模式下:undefined

簡易呼叫指的是函數被單獨呼叫,前面沒有帶任何呼叫物件的情境。

例如這樣的語法:

myFunc();

當函數被單獨呼叫,無論呼叫地點在全域環境還是函數環境內,此時執行這段程式的預設綁定擁有者是 Global 物件

需要注意的是,這是指一般模式下的行為,一定會設定一個綁定物件,因為程式碼沒有指定呼叫物件,Global 物件就被推派出來當預設綁定者。

但基於安全性上的考量,嚴謹模式下不會作呼叫物件的強制給予,也就是說不會預設綁定 Global 物件當呼叫物件,程式碼沒指定就當沒有,因此 this 會是 undefined

節錄 W3Schools 的原文:

  • When used alone, the owner is the Global object.

  • The Global object (the owner of the function) is the default binding.

  • Strict mode does not allow default binding.

根據函數被呼叫的地點不同,有幾種情境。

2.1. 全域環境 (Global Context) 下定義函數 & 呼叫函數

一般模式下,this 會綁定 Global 物件,在 HTML 環境裡就是 window 物件:

function f1(){
  return this;
}

console.log( f1() ); // `window`

在嚴格模式下,this 不會作預設綁定,會是 undefined

"use strict";

function f1(){
  return this;
}

console.log( f1() ); // undefined

但其實對簡易呼叫來說,在哪裡被定義和呼叫都不重要,再看下一個例子會更明白。

2.2. 內部函數 (Inner Functions)

內部函數是指在 A 函數內定義一個 B 函數,然後在 A 函數裡呼叫 B。

例如以下例子 (一般模式):

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log('Output 1: ', this.x);
        var foo = function(){ console.log('Output 2: ', this.x); }
        foo();
    }
};

obj.f();

執行結果:

Output 1:  20
Output 2:  10

這個例子是在 obj.f() 內再定義一個內部函數 foo() ,進行內部呼叫。

如前面所說,foo() 沒有指定呼叫物件,this 就是 Global 物件,因此 foo() 所印出的 this.x 值會是全域變數的 x,而非 objx (Output 2)。

如果這裡想讓 foo() 可以取到obj.x,可以使用一個變數去儲存執行 obj.f() 時的 this 物件。

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log('Output 1: ', this.x);
        var me = this;   // Use a variable to store the `this` object
        var foo = function(){ console.log('Output 2: ', me.x); }  // Access `obj.x` via `me.x`  
        foo();
    }
};

obj.f();

執行結果:

Output 1:  20
Output 2:  20

3. HTML 事件處理 (HTML Event Handlers)

this 物件:接受該事件的 HTML 元素 (HTML Element)。

在 HTML 元件的事件 Callback 裡,this 就是該事件的 HTML 元素。

例如下面例子,onclick 裡的 this,指的就是 <button> 元素本身。

<button onclick="console.log(this); this.style.display='none';">
     Click to Remove Me!
</button>

4. 顯性函數綁定之 bind() 篇 (Explicit Function Binding for bind())

this 物件:新函數物件被指定的綁定物件,也就是 Function.prototype.bind() 的第一個參數。

ES5 導入了 Function.prototype.bind,可以為一個函數建立一個繼承該函數 prototype 的新函數物件,但綁定一個固定的擁有者。

換句話說,無論新的函數物件怎麼被呼叫,函數內的 this 都會是當初綁定的那個擁有者物件

此外,透過 Function.prototype.bind 的綁定,一般模式或嚴謹模式是一樣的結果。

4.1. 一般模式下的範例

例如下面的例子:

var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var firstName = "One", lastName = "Jar";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());           // "One Jar"
console.log(introIronMan());          // "Tony Stark"
console.log(introCaptainAmerica());   // "Steven Rogers"

上面例子在全域環境 (Global Context) 定義了函數 getFullName()

  • 透過簡單呼叫去執行 getFullName(),也是上面 2.1 節所舉的情境。

    • 在一般模式下會預設綁定 Global 物件作為 this 值。

    • 因此函數內會找到全域變數 firstNamelastName,因而印出 "One Jar"

  • 使用 getFullName().bind(),分別產生了兩個新的函數物件,繼承了 getFullName() 的 prototype,但各自綁定了固定的擁有者物件。

    • introIronMan 函數物件綁定了擁有者物件 { firstName: "Tony", lastName : "Stark" }

    • introCaptainAmerica 函數物件綁定了擁有者物件 { firstName: "Steven", lastName : "Rogers" }

  • 一樣透過簡單呼叫的形式去呼叫 introIronMan()introCaptainAmerica(),函數內的 this 值會是各自當初綁定的物件 (而非 Global 物件)。

4.2. 嚴謹模式下有同樣的行為

"use strict";

var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var firstName = "One", lastName = "Jar";
var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introCaptainAmerica = getFullName.bind( { firstName: "Steven", lastName : "Rogers" } );

console.log(getFullName());          // TypeError: Cannot read property 'firstName' of undefined
console.log(introIronMan());         // "Tony Stark"
console.log(introCaptainAmerica());  // "Steven Rogers"
  • 上面執行 getFullName() 時,因為是嚴謹模式,this 不再預設綁定 Global 物件,因此是 undefined (2.1 節情境)。

  • introIronMan()introCaptainAmerica() 不受影響。

4.3. Binding 就像山盟海誓,只有第一次有效

需要注意的是,對一個函數物件來說,只有第一次的 Binding 動作有效

透過 Function.prototype.bind 所建立的新函數物件 A,如果企圖用 Function.prototype.bind 再去建立一個新函數物件 B 並綁定新擁有者,因為函數物件 B 繼承了 A 的 prototype,包含當初的綁定者,因此無論是否給予新的綁定對象都沒有用。

第二次綁定的當下並不會拋錯,只是沒有效果,即使在嚴謹模式下也不會 (感覺這也是嚴謹模式的遺珠)。

"use strict";

var getFullName = function() {
    return this.firstName + " " + this.lastName;
}

var introIronMan = getFullName.bind( { firstName: "Tony", lastName : "Stark" } );
var introClone = introIronMan.bind();
var introSpiderMan = introIronMan.bind( { firstName: "Peter", lastName : "Parker"} );

console.log(introClone());        // "Tony Stark"
console.log(introSpiderMan());    // "Tony Stark"

References

Last updated