JavaScript Object Oriented Programming: Early and Late Binding
10 Feb 2016JavaScript 在呼叫的時候設定 this 的值,而這個 this 的值有可能不是我們預期的結果。下面有幾個範例。
this 的值並非預期的狀況
以下都會由 Menu 這個建構子作為範例。
function Menu(elem) {
//...
}
var menu = new Menu(document.createElement('div'));
setTimeout
this 是參考到 Window,而非 menu。這是因為 setTimeout 永遠是在 window context 下執行。
function Menu(elem) {
setTimeout(function() {
console.log(this); // Window
}, 500);
}
var menu = new Menu(document.createElement('div'));
onclick
event handler 永遠是將 this 設定給傳入的物件(例如:在此是 DOM Element「elem」),而非 menu。但 menu 才是我們預期的結果。
function Menu(elem) {
elem.onclick = function() {
console.log(this); // elem, not Menu
};
}
var menu = new Menu(document.createElement('div'));
Private Method / Local Function
private method 或 local function 內的 this 若沒有特別指定,則值是 Window。
function Menu(elem) {
function privateMethod() {
console.log(this);
}
privateMethod(); //Window
}
var menu = new Menu(document.createElement('div'));
我們需要一些解法來解決這個問題 - 能在 function 內存取到 menu 物件。解法有三種 - 使用 var self = this
做綁定、Early binding 和 Late binding。
Binding with var self = this
在進入 setTimeout 之前,我們先將正確的 this 儲存在區域變數 self 裡面,避免 this 進入 setTimeout 後變成 Window。然後我們便可在任何地方使用 self 了。除了 setTimeout,event handle 和 private method / local function 也可以使用此解法。
function Menu(elem) {
var self = this;
setTimeout(function() {
console.log(self); // Menu
});
}
new Menu(document.createElement('div'));
Early Binding
我們可以使用 bind 這樣的 helper function - bind 接受參數(函式 func 和 this)並回傳一個閉包 - 將 this 替換成此 func 的內容狀態。這等於是強制替換 this 的值。
function bind(func, fixThis) {
return function() {
return func.apply(fixThis, arguments);
};
}
function Menu(elem) {
elem.onclick = bind(function() {
console.log(this); // Menu
}, this);
}
new Menu(document.createElement('div'));
Function.prototype.bind
其實我們不用那麼辛苦 - 自己寫一個 bind 這樣的 helper function,直接使用原生的 Function.prototype.bind 即可!如果舊瀏覽器不支援,我們也可以像以下這樣擴充它。
Function.prototype.bind =
Function.prototype.bind ||
function(fixThis) {
var func = this;
return function() {
return func.apply(fixThis, arguments);
};
};
因此,setTimeout、event handle 和 private method / local function 也可以使用此解法了。
setTimeout
function Menu(elem) {
setTimeout(
function() {
console.log(this); // Menu
}.bind(this),
1000,
);
}
new Menu(document.createElement('div'));
onclick
function Menu(elem) {
elem.onclick = function() {
console.log(this); //Menu
}.bind(this);
}
new Menu(document.createElement('div'));
Private Method / Local Function
function Menu(elen) {
var privateMethod = function() {
console.log(this);
}.bind(this);
privateMethod();
}
new Menu(document.createElement('div'));
比較 bind 和 var self = this
的差異
- bind 不需要包在 closure(閉包)之內。bind 綁定是即時且永久的。bind 勝!
- bind 必須要在每個要使用的 funciton 加上去。
var self = this
勝! var self = this
必須額外加上去。bind 勝!
Late Binding
呼叫的時候才做綁定的動作。
Early Binding 的問題
function bind(func, fixThis) {
// using custom bind for simplicity
return function() {
return func.apply(fixThis, arguments);
};
}
function Menu(elem) {
this.sayHi = function() {
console.log('Menu');
};
elem.onclick = bind(this.sayHi, this);
}
function SuperMenu(elem) {
Menu.apply(this, arguments);
this.sayHi = function() {
console.log('SuperMenu');
};
}
new SuperMenu(document.body);
底下的程式碼預期印出 「SuperMenu」,但其實印出的是「Menu」。這是因為 onclick 這個 event handler 使用的 this.sayHi
的 this 是指 Menu,而非 SuperMenu。
使用 Late Binding
修正一下即可。
function bindLate(funcName, fixThis) {
// instead of bind
return function() {
return fixThis[funcName].apply(fixThis, arguments);
};
}
function Menu(elem) {
this.sayHi = function() {
console.log('Menu');
};
elem.onclick = bindLate('sayHi', this);
}
function SuperMenu(elem) {
Menu.apply(this, arguments);
this.sayHi = function() {
console.log('SuperMenu');
};
}
new SuperMenu(document.body);
總結三種 binding 方法
- 將 this 存在區域變數中(
var self = this
)。 - 使用 bind helper funciton 或原生的
Function.prototype.bind
。 - 使用 late binding。
References
這篇文章的原始位置在這裡-JavaScript Object Oriented Programming - Early and Late Binding
由於部落格搬遷至此,因此在這裡放了一份,以便閱讀;部份文章片段也做了些許修改,以期提供更好的內容。