借用方法

有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。得益于函数的call()apply()方法,可以通过借用方法模式实现它。在本书中,你其实已经见过这种模式了,甚至在本章extendDeep()的实现中也有用到。

在JavaScript中函数也是对象,它们有一些有趣的方法,比如call()apply()。这两个方法的唯一区别是后者接受一个参数数组以传入正在调用的方法,而前者只接受一个一个的参数。你可以使用这两个方法来从已有的对象中借用方法:

  1. // call()示例
  2. notmyobj.doStuff.call(myobj, param1, p2, p3);
  3. // apply()示例
  4. notmyobj.doStuff.apply(myobj, [param1, p2, p3]);

在这个例子中有一个对象myobj,而且notmyobj有一个用得着的方法叫doStuff()。你可以简单地临时借用doStuff()方法,而不用处理继承然后得到一堆myobj中无关的方法。

你传一个对象和任意的参数,这个被借用的方法会将this绑定到你传递的对象上。简单地说,你的对象会临时假装成另一个对象以使用它的方法。这就像实际上获得了继承但又免除了“继承税”(译注:指不需要的属性和方法)。

例:从数组借用

这种模式的一种常见用法是从数组借用方法。

数组有很多很有用但是一些“类数组”对象(如arguments)不具备的方法。所以arguments可以借用数组的方法,比如slice()。这是一个例子:

  1. function f() {
  2. var args = [].slice.call(arguments, 1, 3);
  3. return args;
  4. }
  5. // 示例
  6. f(1, 2, 3, 4, 5, 6); // returns [2,3]

在这个例子中,有一个空数组被创建了,因为要借用它的方法。也可以使用一种看起来代码更长的方法来做,那就是直接从数组的原型中借用方法,使用Array.prototype.slice.call(...)。这种方法代码更长一些,但是不用创建一个空数组。

借用并绑定

当借用方法的时候,不管是通过call()/apply()还是通过简单的赋值,方法中的this指向的对象都是基于调用的表达式来决定的。但是有时候最好的使用方式是将this的值锁定或者提前绑定到一个指定的对象上。

我们来看一个例子。这是一个对象one,它有一个say()方法:

  1. var one = {
  2. name: "object",
  3. say: function (greet) {
  4. return greet + ", " + this.name;
  5. }
  6. };
  7. // 测试
  8. one.say('hi'); // "hi, object"

现在另一个对象two没有say()方法,但是它可以从one借用:

  1. var two = {
  2. name: "another object"
  3. };
  4. one.say.apply(two, ['hello']); // "hello, another object"

在这个例子中,say()方法中的this指向了twothis.name是“another object”。但是如果在某些场景下你将函数赋值给了全局变量或者是将这个函数作为回调,会发生什么?在客户端编程中有非常多的事件和回调,所以这种情况经常发生:

  1. // 赋值给变量,this会指向全局对象
  2. var say = one.say;
  3. say('hoho'); // "hoho, undefined"
  4. // 作为回调
  5. var yetanother = {
  6. name: "Yet another object",
  7. method: function (callback) {
  8. return callback('Hola');
  9. }
  10. };
  11. yetanother.method(one.say); // "Holla, undefined"

在这两种情况中say()中的this都指向了全局对象,所以代码并不像我们想象的那样正常工作。要修复(绑定)一个方法的对象,我们可以用一个简单的函数,像这样:

  1. function bind(o, m) {
  2. return function () {
  3. return m.apply(o, [].slice.call(arguments));
  4. };
  5. }

这个bind()函数接受一个对象o和一个方法m,然后把它们绑定在一起,再返回另一个函数。返回的函数通过闭包可以访问到om,也就是说,即使在bind()返回之后,内层的函数仍然可以访问到om,而om会始终指向原来的对象和方法。让我们用bind()来创建一个新函数:

  1. var twosay = bind(two, one.say);
  2. twosay('yo'); // "yo, another object"

正如你看到的,尽管twosay()是作为一个全局函数被创建的,但this并没有指向全局对象,而是指向了通过bind()传入的对象two。不论如何调用twosay()this将始终指向two

绑定是奢侈的,你需要付出的代价是一个额外的闭包。

Function.prototype.bind()

ECMAScript5在Function.prototype中添加了一个方法叫bind(),使用时和apply()/call()一样简单。所以你可以这样写:

  1. var newFunc = obj.someFunc.bind(myobj, 1, 2, 3);

这意味着将someFunc()myobj绑定了,并且还传入了someFunc()的前三个参数。这也是一个在第4章讨论过的部分应用的例子。

让我们来看一下当你的程序跑在低于ES5的环境中时如何实现Function.prototype.bind()

  1. if (typeof Function.prototype.bind === "undefined") {
  2. Function.prototype.bind = function (thisArg) {
  3. var fn = this,
  4. slice = Array.prototype.slice,
  5. args = slice.call(arguments, 1);
  6. return function () {
  7. return fn.apply(thisArg, args.concat(slice.call(arguments)));
  8. };
  9. };
  10. }

这个实现可能看起来有点熟悉,它使用了部分应用,将传入bind()的参数串起来(除了第一个参数),然后在被调用时传给bind()返回的新函数。这是用法示例:

  1. var twosay2 = one.say.bind(two);
  2. twosay2('Bonjour'); // "Bonjour, another object"

在这个例子中,除了绑定的对象外,我们没有传任何参数给bind()。下一个例子中,我们来传一个用于部分应用的参数:

  1. var twosay3 = one.say.bind(two, 'Enchanté');
  2. twosay3(); // "Enchanté, another object"