this 是什麼?


在 JavaScript 中,函式是物件,是 Function 的實例,可以在變數間任意指定,可以傳給函式的參數參考,當然,要新增為物件的特性也是可以的。例如:

function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

var p1 = {
    name     : 'Justin', 
    age      : 35,
    toString : toString
};

var p2 = {
    name     : 'momor', 
    age      : 32,
    toString : toString
};

console.log(p1.toString());  // [Justin,35] 
console.log(p2.toString());  // [momor,32] 

在上例中定義了一個 toString 函式,並分別設定為 p1 與 p2 的 toString 來參考,透過 p1 呼叫時,toString 就像是 p1 的方法(Method),透過 p2 呼叫時,toString 就像是 p2 的方法。

在上例中,toString 函式中使用了 this,在呼叫函式時,每個函式都會有個 this,然而 this 參考至哪個物件,其實依呼叫方式而有所不同。以上例而言,透過 p1 呼叫時,toString 中的 this 會參考至 p1 所參考的物件,也因此顯示 p1 物件的 name 與 age 值,透過 p2 呼叫時,toString 中的 this 則會參考至 p2 所參考的物件。

如果呼叫函式時是透過物件與點運算子的方式呼叫,則 this 會參考至點運算子左邊的物件。

在 JavaScript 中,函式是 Function 的實例,Function 都會有個 call 方法,可以讓你決定 this 的參考對象。舉例來說,你可以如下呼叫:

function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

var p1 = {
    name : 'Justin', 
    age  : 35,
};

var p2 = {
    name     : 'momor', 
    age      : 32,
};

console.log(toString.call(p1));  // [Justin,35] 
console.log(toString.call(p2));  // [momor,32] 

這次並沒有將 toString 指定為物件的特性,而是直接使用 call 方法來呼叫函式, call 方法的第一個參數就是用來指定函式中的 this 所參考的物件。如果函式原本具有參數,則可接續在第一個參數之後。例如:

function add(num1, num2) {
    return this.num + num1 + num2;
}

var o = {num : 10};

console.log(add.call(o, 20, 30)); // 60

Function 也有個 apply 方法,作用與 call 方法相同,也可讓你在第一個參數指定 this 所參考的對象,不過 apply 方法指定後續引數時,必須將引數收集為一個陣列,如果你有一組引數,必須在多次呼叫時共用,就可以使用 apply 方法。例如:

function add(num1, num2) {
    return this.num + num1 + num2;
}

var o1 = {num : 10};
var o2 = {num : 100};
var args = [20, 30];

console.log(add.apply(o1, args)); // 60
console.log(add.apply(o2, args)); // 150

所以,this 實際參考的對象,是以呼叫方式而定,而不是它是否附屬在哪個物件而定。例如就算函式是附屬在函式上的某個特性,也可以這麼改變 this 所參考的對象:

function toString() {
    return this.name;
}

var p1 = {
    name     : 'Justin', 
    toString : toString
};

var p2 = {
    name     : 'momor', 
    toString : toString
};

console.log(p1.toString());        // Justin
console.log(p2.toString());        // momor
console.log(p1.toString.call(p2)); // momor

在最後一個測試中,是以 p1.toString.call(p2) 的呼叫方式,所以雖然 toString 是 p1 的特性,但 call 指定 this 是參考至 p2,結果當然也是傳回 p2 的 name。

在用物件實字建立物件時,也可以直接指定函式作為特性。例如:

var o = {
    name : 'Justin',
    toString : function() {
        return this.name;
    }
};

console.log(o.toString()); // Justin

如果呼叫函式時,無法透過 . 運算、call、apply 等方法確定 this 的對象,在嚴格模式下 this 會是 undefined。

全域物件是 JavaScript 執行時期全域可見的物件,在不同的環境中想要取得全域物件,會透過不同的名稱,像是 Node.js 中可以使用 global,瀏覽器中可以透過 window 或在全域範圍使用 this

因此,如果你想統一全域物件的變數名稱,例如統一使用 global,可以透過類似以下的方式:

var global = global || (function() {
    return this;
})();

嚴格模式下,當一個內部函式直接被呼叫時,無法確定 this 對象時,this 會是 undefined。

function func() {
    function inner() {
        return this;
    }
    return inner();
}

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

底下也會是 undefined:

'use strict'

 (function() {
     return this;
 })();  // undefined

在嚴格模式下,如果真的想取得全域物件,可以透過兩個方式,第一個是直接建立 Function 實例:

var global = global || Function('return this')();

第二個方式是間接參考 eval 函式:

var get = eval;
var global = global || get('this');

詳情可以參考 How to get the global object in JavaScript?,如果不想多個 get 名稱,那也可以寫為:

var global = global || (0, eval)('this');

這個有趣的語法在於,逗號運算子會從左而右運算每個運算元,然後傳回最後一個運算元,可參考 MDN:Comma operator 的說明。

在 JavaScript 執行過程中,搞清楚 this 是誰有時非常重要,this 的決定方式是在於呼叫,而非定義的方式。

舉個例子來說,如果你想要自行實現 Array 的 forEach 方法,則可以如下:

var obj = {
    '0' : 100,
    '1' : 200,
    '2' : 300,
    length : 3,
    forEach : function(callback) {
        for(var i = 0; i < this.length; i++) {
            callback(this[i]);
        }
    }
};

obj.forEach(function(elem) {
    console.log(elem);
});

在上例中,由於呼叫 forEach 時,obj 參考的物件就是 this 所參考的物件,因而可以取得 length 等特性,函式是物件,所以自然可以丟給 forEach 作為引數。

在 ECMAScript 5 中,函式實例有個 bind 方法,執行結果傳回一個新函式,這個新函式的 this 綁定對象固定為你呼叫 bind 時指定的物件。例如:

function forEach(callback) {
    for(var i = 0; i < this.length; i++) {
        callback(this[i]);
    }
}

var obj1 = {
    '0' : 100,
    '1' : 200,
    '2' : 300,
    length : 3,
};

var f1 = forEach.bind(obj1);

f1(function(elem) {
    console.log(elem);  // 100 200 300
});

var obj2 = {
    '0' : 10,
    '1' : 20,
    '2' : 30,
    length : 3,
    forEach : f1
};

obj2.forEach(function(elem) {
    console.log(elem);  // 100 200 300
});

在上面這個例子中,即使後來透過 obj2.forEach(...),bind 傳回的函式,this 都是綁定為 obj1 參考的對象。

bind 可以指定引數,如果給的引數不全,傳回的函式之後只要補上不全的引數就可以了。例如,這可以用來達到一些語言內建的部份函式(Partial function)效果:

function plus(a, b) {
    return a + b;
}

var addTwo = plus.bind(undefined, 2);
console.log(addTwo(10));    // 12
console.log(addTwo(5));     // 7