擺脫JavaScript中的綁定局面

大多數開發者不瞭解或不夠關心JavaScript中的綁定,然而,正是這樣一個問題在大多數JavaScript相關的支持渠道中才產生了相當一部分問題。每天開發者被折磨德有數以千計----如果不是數百萬的法---的髮絲落地。但是,只要對這個經常忽略的主體稍加留心,你就會節約時間、精力和耐心,從而轉向更爲強大、有效的腳本。

我們爲什麼要關心綁定?

      幾乎大多數面向對象的程序語言(oop)沒有強制你考慮綁定。也就是說,這些語言並不要求你明確的用一個諸如this或者self之類的參數使接近當前對象的成員(方法和屬性)合格化。如果你在非特定對象上調用其方法,你常常是在當前對象上調用它。當你爲以後的引用傳遞參數時也是一樣:它仍保持在當前對象上。簡言之,對於大多數面向對象的語言(oop),綁定是隱含的,這在java、c#、Ruby、Delphi和c++裏是一樣的,略舉數例。

       PHP和JavaScript要求你明確的聲明你正在接觸的對象,即使是當前對象也是一樣(這也許是我寧願將PHP和JavaScript放到一塊的原因)

       當然,從傳統意義上講,PHP和JavaScript都不是真正的面向對象。就PHP來說,作爲事後諸葛,對象支持增加了,而不是草率行事。即使是在PHP5,函數也不是第一個值,許多oop特色都暗淡無光。JavaScript活力十足,依賴於“原型繼承”,較之於類繼承,它是一個與衆不同的範式。這些差異並不馬上與綁定問題相關,但是,它表明傳統對象相關的語法和行爲對JavaScript設計師沒有多大的重要性。

       在JavaScript中,綁定總是具體的,也容易丟失。因此,使用this的方法在所有情況下並不指向固有的對象,除非你強制它指向一個固有對象。總體來講,JavaScript中的綁定是一個很難的概念,但是它經常被JavaScript程序員忽略和曲解,從而混淆視聽。

讓我們走進它

   設想下面這個看起來無傷大雅的例子,它們實際的行爲好像是多麼的難以預料。
   var john = {
      name: 'John',
      greet: function(person) {
     alert("Hi " + person + ", my name is " + name);
    }
};
john.greet("Mark");
// => "Hi Mark, my name is "

       不可思議的事情發生了,name哪裏去了?在這裏,我們犯了一個綁定假設的錯誤:我們的方法僅僅指向name,JavaScript將在變量有效的幾個層級上去尋找。最終以window結束。當然,我們的window擁有一個name屬性,但它默認的值是空的,因此就沒有name屬性值顯示。

      試試看:

  name = 'Ray'; // 或者具體些: window.name = 'Ray';
  var john = {
    name: 'John',
    greet: function(person) {
    alert("Hi " + person + ", my name is " + name);
  }
};

john.greet("Mark");
// => "Hi Mark, my name is Ray"

       一切正常,但毫無意義。我們需要的是對象name的屬性,並不是window的name屬性。在這裏,明確的綁定是很重要的:

var john = {
  name: 'John',
  greet: function(person) {
    alert("Hi " + person + ", my name is " + this.name);
  }
};

john.greet("Mark");
// => "Hi Mark, my name is John"

      注意到我們是如何在我們的name參數前面加上關鍵字this的:這就是具體綁定,它能真正的運行,抑或不能?

var john = {
  name: 'John',
  greet: function(person) {
    alert("Hi " + person + ", my name is " + this.name);
  }
};

var fx = john.greet;
fx("Mark");
// => "Hi Mark, my name is " (or "Hi Mark, my name »
is Ray" 取決於你在什麼地方調試它)

        也許你對將函數當作地一個值很陌生,所以var fx=john.greet看起來很怪異。這並不是調用greet方法,而是創建一個對它的引用-----如果你願意。可稱爲方法的別名。因此,調用fx方法以調用greet方法結束。但是,我們似乎一下子糊塗了:我們明確的使用了關鍵字this,然而它不用john,給了什麼?

        這就是JavaScript綁定中很重要的問題----有時我稱之爲“綁定丟失”。當你通過一個引用而不是直接通過自己的對象接觸一個方法時 ,他就會發生。這個方法失去了它隱含的綁定,this不再引用它自己的對象,並返回它原來默認的值,在這裏是window(如果window擁有name屬性,它將被採用)。

認識綁定敏感代碼模式

       綁定敏感代碼模式包括傳遞參數引用,它通過兩種可能的方法發生:要麼將方法當作值分配,要麼將方法當作參數傳遞(當你認真思考時,你發現其實質是一樣的).

思考下面簡單的類定義:

function Person(first, last, age) {
  this.first = first;
  this.last = last;
  this.age = age;
}
Person.prototype = {
  getFullName: function() {
    alert(this.first + ' ' + this.last);
  },
  greet: function(other) {
    alert("Hi " + other.first + ", I'm " + »
    this.first + ".");
  }
};

     試試看:

var elodie = new Person('Elodie', 'Jaubert', 27);
var christophe = new Person('Christophe', »
'Porteneuve', 30);
christophe.greet(elodie);
// => "Hi Elodie, I'm Christophe."


       到目前爲止看起來很好,讓我們在前面添加以下代碼:

function times(n, fx, arg) {
  for (var index = 0; index < n; ++index) {
    fx(arg);
  }
}

times(3, christophe.greet, elodie);
// => Three times "Hi Elodie, I'm undefined."
times(1, elodie.getFullName);
// => "undefined undefined"
  
       啊!我們困惑了,未定義什麼?當我們將greet和getFullName作爲參數傳遞時,它們的this引用指向window對象,該對象沒有first和last屬性,我們因此失去了綁定。

       正如我們所做的一樣,當你笨拙的手寫JavaScript代碼時,你通常會更加意識到這些問題,但是,當你依賴框架處理這些基本問題時,綁定會使你困惑,它只需要你寫出幾行簡單的代碼即可,思考下面基於原型的代碼片斷:

this.items.each(function(item) {
  // Process item
  this.markItemAsProcessed(item);
});

       這些代碼會觸發一個錯誤——聲明markItemAsProcessed方法undefined。爲什麼是那樣?因爲你僅僅傳遞一個引用給一個匿名函數。因此,this在那裏指向window,並不是指向每一個的外部對象。這是一個非常普遍的錯誤,在關於框架問題有機列表中佔了相當一部分份額。


綁定具體化

       因此,我們應該如何固定它?我們使綁定具體化——也就是說,在一個被調用的方法內明確聲明this的指向。現在我們該如何做?JavaScript給了我們兩種選擇:apply和call。

Apply

        每個JavaScript函數都配有一個apply方法,它允許你在特定的綁定中調用那個函數(如果你願意,就是一個具體的this)。它接受兩個參數:綁定的對象,一個傳遞給函數的參數數組,這裏有一個基於我們前面代碼的例子:

var fx = christophe.greet;
fx.apply(christophe, [elodie]);
// => "Hi Elodie, I'm Christophe."

        一個數組的好處在於你調用apply時無須進一步知道它將接受那個參數。你可以寫出世紀的參數列表——構建任何你想要的數組並傳遞給它。在你傳給它之前,你可以接受一個已經存在的參數數組並將它改進成你想要得內容。

Call

        當你想知道傳遞的是那個特定參數時,call將使你如願以償,它接受參數自身,而不是參數數組。

var fx = christophe.greet;
fx.call(christophe, elodie);
// => "Hi Elodie, I'm Christophe."

        但是,call失去了數組的靈活性,一切取決於特定的環境:拋卻它們之間的差異,apply和call擁有同樣的語義和行爲。

       順便提一句,方法並不真正屬於你綁定的對象,只要它使用this時在各方面與綁定的對象(提示:在綁定的對象內成員存在)兼容,我們就能明確。這種靈活性時可能的——因爲JavaScript是在運行時解析成員的動態語言,此特色有時稱爲“晚綁定”。在每一種程序語言中(如Perl, Ruby, Python, PHP),你也將會發現晚綁定,偶爾也包括OLE Automation。

如釋重負

        很高興有一種方法來指定綁定。但問題是,你僅僅只能在引用時段內來指定它,即你不能提前指定它,讓它在合適的時候使其它代碼引用你正常綁定的方法。這是一個大問題,因爲我們傳遞方法參數時,我們欲使其它代碼選擇何時引用。

       因此,我們需要的是永久的綁定一個方法,這樣我們就取得一個限定的方法參數。能達到此目的的唯一途徑是將其原始的方法封裝在另一個之中,,讓它在內部調用apply方法,下面是一次嘗試:

function createBoundedWrapper(object, method) {
  return function() {
    return method.apply(object, arguments);
  };
}
        如果你對JavaScript不夠熱心,上面的代碼未免讓你糊塗。其思想在於用一個假定的對象調用createBoundedWrapper方法(情理上屬於所說的對象),該方法生成一個新的函數(我們返回的匿名函數)。

      被調用的函數接受原來的方法並調用apply方法,它傳遞的是:
          1.原始對象的綁定(名叫object的變量)和
          2.在調用期間,作爲參數傳遞的數組。
(每個函數都有一個自動參數變量,它就象我們傳遞給它的所有數組組成的數組)

試試看:

var chrisGreet = createBoundedWrapper(christophe, »
christophe.greet);
chrisGreet(elodie);
// "Hi Elodie, I'm Christophe."

       哈哈,執行了!我們創建了一個基於christophe和它的greet方法的限定方法參數。


JavaScript框架中的綁定

       我們的createBoundedWrapper函數是乾淨的,但有點笨拙,如果你精於JavaScript工作,你可能依賴於框架去解決瀏覽器的兼容性、方便的DOM操作以及壯大JavaScript。因此,讓我們看看一些流行的JavaScript框架是如何處理方法綁定的。

Prototype

       Prototype有很長的備用函數,你僅僅需要這樣做就行:

var chrisGreet = christophe.greet.bind(christophe);
chrisGreet(elodie);

       到目前爲止,太少的人知道綁定也可以讓你做“局部應用”——也就是說,預填充一個或者更多的參數。例如,你有一個toggle專欄狀態的方法:

var coolBehavior = {
  // ...
  toggle: function(enabled) {
    this.enabled = enabled;
    // ...
  },
  // ...
};

      你可以很容易的定義兩種快捷方式——enable和disable,如下:

coolBehavior.enable = coolBehavior.toggle.bind »
(coolBehavior, true);
coolBehavior.disable = coolBehavior.toggle.bind »
(coolBehavior, false);

// And then:
coolBehavior.enable();

        正確使用要注意:有時,如果對綁定沒有興趣,綁定僅用來預填充,有點像下面你看到的代碼:

function times (count, fx) {
  for (var index = 0; index &lt; count; ++index) {
    fx();
  }
}
// ...
var threeTimes = times.bind(null, 3);
// ...
threeTimes(someFunction);

      作爲旁註,在Prototype 1.6中,如果你對預填充感興趣,你更喜歡curry——它保持當前的綁定,集中在參數的預填充上。

var threeTimes = times.curry(3);

Ext JS

     Ext JS庫通過給一個名爲createDelegate添加方法來適應綁定,其語法就像這樣:

method.createDelegate(scope[, argArray] [, appendArgs = false])

       首先,你聲明的額外的參數需要作爲數組提供,而不是在一行,即myMethod.createDelegate(scope, [arg1, arg2]), 而不是 myMethod.createDelegate(scope, arg1, arg2).

       另外一個重要的細微差別是在調用的時候,這些參數將替代你傳遞的參數,而不是局部應用。如果你需要的是後者,你需要傳遞true(它附加參數數組,Prototype則追加到前面)或者作爲第三個參數插入其中(一般來說,用0作爲前導),這裏有一個從API文檔中摘出來的例子:

var fn = scope.func1.createDelegate(scope, [arg1, arg2], true);
fn(a, b, c); // => scope.func1(a, b, c, arg1, arg2);

var fn = scope.func1.createDelegate(scope, [arg1, arg2]);
fn(a, b, c); // => scope.func1(arg1, arg2);

var fn = scope.func1.createDelegate(scope, [arg1, arg2], 1);
fn(a, b, c); // => scope.func1(a, arg1, arg2, b, c);

Dojo

        Dojo工具包也滿足方法綁定,它有一個搞笑的名字——hitch,語法如下:

dojo.hitch(scope, methodOrMethodName[, arg…])

        有意思的是,這些方法要麼直接傳遞,要麼用它的名字。額外的參數(如果有的法)在實際運行期之前傳遞,這裏有一些例子:

var fn = dojo.hitch(scope, func1)
fn(a, b, c); // => scope.func1(a, b, c);

var fn = dojo.hitch(scope, func1, arg1, arg2)
fn(a, b, c); // => scope.func1(arg1, arg2, a, b, c);


Base2

        Dean Edwards 華麗的Base2庫扮演的好似所有JavaScript庫擁有的共同點的角色,在JavaScript運行期間,它忽略了所有煩人的分歧。它承認綁定是需要的,並提供了一個簡單的綁定函數。

base2.bind(method, scope[, arg]);

      注意:作用域對象在第二位,並不是第一個。除此之外,其語法與Prototype的bind或者Dojo的hitch嚴格等同。

var fn = base2.bind(func1, scope)
fn(a, b, c); // => scope.func1(a, b, c);

var fn = base2.bind(func1, scope, arg1, arg2)
fn(a, b, c); // => scope.func1(arg1, arg2, a, b, c);


jQuery

         jQuery庫不提供如此的綁定機制,該庫的哲學是偏袒閉包勝過綁定,當用戶需要傳遞一條稱之爲“實例成員”的代碼時,它會強迫它們去經歷一些苦衷(也就是說,手動的方式聯合語義上的閉包和Apply或者call,與其它庫在內部實現不同)。

你應該適時地綁定

         現在我們已經瀏覽到了綁定的細節,我們的公平的說,有些時候綁定有點矯枉過正了。具體來說,較之於顯著的性能回報,代碼模式中的綁定可以被語義上的閉包代替(如果你不明白什麼是閉包,不要驚慌。)

        這裏有一種模式:一些代碼的方法依賴於一個匿名函數,該函數通過傳遞參數工作。那些匿名函數需要接觸到環境中方法的this關鍵字。例如,假設一分鐘,我們在一個數組裏迭代每一項,再次思考下面的代碼:

// ...
  processItems: function() {
    this.items.each(function(item) {
      // Process item…
      this.markItemAsProcessed(item);
    });
  },
  // ...

        這裏的問題是:擁有實際處理過程的匿名函數是作爲變量傳遞到每一個迭代對象的。因此,它失去了當前的綁定。當它嘗試去調用this.markItemAsProcessed()時,因爲window沒有此方法,故而失敗。

        多數開發者非常迅速的用綁定來固定。如用Prototype,他們添加了如下的方法:

// ...
  processItems: function() {
    this.items.each(function(item) {
      // Process item
      this.markItemAsProcessed(item);
    }.bind(this));
  },
  // ...

        注意:加上去的稱之爲綁定。但是,此代碼看起來不是一個好主意。我們看到,實現這種“限定參數”要求我們將原始的方法封裝在一個匿名函數之內。這意味着調用界定方法參數會導致兩個方法的調用:我們的匿名包和原始的方法。如果對任何語言有一件事是正確的法,那就是call方法的代價是昂貴的。
  
       在這種情況下,我們就要使用原始的、我們在同一代碼位置定義的預期的this關鍵字,並調用這個有待完善的函數(我們將其作爲一個參數傳遞給每個對象的匿名函數)。我們簡單在局部變量中存儲這個正常的this參數,然後再迭代函數內部使用它。

// ...
  processItems: function() {
    var that = this;
    this.items.each(function(item) {
      // Process item
      that.markItemAsProcessed(item);
    });
  },
  // ..

        瞧!沒有綁定,這些代碼使用了一個稱之爲“語義閉包”的語言特色。簡而言之,閉包 能使代碼在A處使用環境A中定義的標識符。在這兒,我們的匿名函數使用環境中函數的變量——我的processItems方法。無論如何,這樣一個閉包在JavaScript執行期間維持。因此,使用它也無須付出額外的代價。即使有,我也很有自信的認爲,其成本遠遠低於每一個交替循環 調用額外函數的成本。

        小心你的閉包:有時候閉包提供更簡單、更短小、更好的方法(正因爲如此,我認爲爲什麼“jQuery”讓用戶以手動的方式來處理綁定,這樣能使他們對每一種情況能做出最佳選擇)。同時,閉包也有自身的一些問題——使用不當,他們會導致某些瀏覽器內存泄漏,這裏我推薦的用法是相當安全的。


外賣點(我實在不知道如何翻譯)

摘要:

  . 任何使用的成員在相關的對象內必須合格化,即使是this。
  . 任何形式的函數引用(作爲值分配,作爲參數傳遞)失去原有函數的綁定。
  . JavaScript提供了兩個等價的方法明確函數調用是的綁定:Apply和call.
  . 創建一個“界定方法參數”需要一個匿名封裝函數和調用成本。在特定情況下,利用閉包也許使更好的選擇。

現在,在這篇文章的幫助下,你將對綁定沒有什麼疑慮了吧 !

原文地址:http://www.alistapart.com/articles/getoutbindingsituations
譯文地址:http://blog.myspace.cn/130773114 ... 8/14/402009066.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章