JavaScript Object Oriented Programming: Early and Late Binding

JavaScript 在呼叫的時候設定 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 的差異

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 方法

References


這篇文章的原始位置在這裡-JavaScript Object Oriented Programming - Early and Late Binding

由於部落格搬遷至此,因此在這裡放了一份,以便閱讀;部份文章片段也做了些許修改,以期提供更好的內容。
標籤: javascript
comments powered by Disqus