在 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
會直接轉為參考全域物件(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
在上例中,最後一個例子雖然指定外部函式的 this
為 o1
,但事實上,內部函式被呼叫時,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
的決定方式是在於呼叫,而非定義的方式。舉個例子來說,如果你想要自行實現
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