摆脱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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值