JavaScript 語言核心(11)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 函式,並分別設定為 p1p2toString 來參考,透過 p1 呼叫時,toString 就像是 p1 的方法(Method),透過 p2 呼叫時,toString 就像是 p2 的方法。
在上例中,toString 函式中使用了 this,在呼叫函式時,每個函式都會有個 this,然而 this 參考至哪個物件,其實依呼叫方式而有所不同。以上例而言,透過 p1 呼叫時,toString 中的 this 會參考至 p1 所參考的物件,也因此顯示 p1 物件的 nameage 值,透過 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) 的呼叫方式,所以雖然 toStringp1 的特性,但 call 指定 this 是參考至 p2,結果當然也是傳回 p2name
在用物件實字建立物件時,也可以直接指定函式作為特性。例如:
var o = {
    name : 'Justin',
    toString : function() {
        return this.name;
    }
};

console.log(o.toString()); // Justin
如果呼叫函式時,無法透過 . 運算、callapply 等方法確定 this 的對象,如果不是嚴格模式,那麼 this 會直接轉為參考全域物件(Global object)。
全域物件是 JavaScript 執行時期全域可見的物件,在不同的環境中想要取得全域物件,會透過不同的名稱,像是 Node.js 中可以使用 global,瀏覽器中可以透過 window 或在全域範圍使用 this,Rhino(或 JDK8 的 Nashorn)可以在全域範圍使用 this 取得。
因此,如果你想統一全域物件的變數名稱,例如統一使用 global,可以透過類似以下的方式:
var global = global || (function() {
    return this;
})();
類似地,非嚴格模式下,當一個內部函式直接被呼叫時,無法確定 this 對象時,this 也是轉為參考全域物件。例如:
function func() {
    function inner() {
        return this;
    }
    return inner();
}

console.log(func() === global);    // true

var o1 = {func : func};

console.log(o1.func() === o1);         // false
console.log(o1.func() === global);     // true
console.log(func.call(o1) === global); // true
在上例中,最後一個例子雖然指定外部函式的 thiso1,但事實上,內部函式被呼叫時,this 仍是參考至全域物件。可以對照以下這個例子的結果:
function func() {
    function inner() {
        return this;
    }
    this.inner = inner;
    return this.inner();
}

console.log(func() === global);            // true

var o1 = {func : func};

console.log(o1.func() === o1);                // true
console.log(o1.func.call(global) === global); // true
console.log(func.call(o1) === global);        // false
然而,在無法確立 this 參考對象下,直接令其轉為參考全域物件,會造成判斷的複雜度,因此在嚴格模式下,this 無法確認對象下,就會是 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 的決定方式是在於呼叫,而非定義的方式。
舉個例子來說,如果你想要自行實現 ArrayforEach 方法,則可以如下:
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