JavaScript Object Oriented Programming: Early and Late Binding

JavaScript

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