Skip to content

Latest commit

 

History

History
279 lines (202 loc) · 11.9 KB

File metadata and controls

279 lines (202 loc) · 11.9 KB

4.4 Variable-length argument lists

整体看,javascript非常灵活,正是它的灵活性构成了它今天的样子。其中一个灵活性就是函数可以接收任意长度的参数表。本节中我们将看到如下几个例子:

  • 如何给变参函数提供参数
  • 如果实现函数重载
  • 如何使用和理解 length 属性

下面我们先看看如何使用 apply() 来处理变参函数的问题。

4.4.1 Using apply() to supply variable arguments

任何语言的设计者总会忽略一些非常基本的功能函数,js也是如此。比较典型的就是寻找数组中的min和max元素。其中功能最接近的就是Math的min和max函数,但是他们是变参的,只能这样调用

var biggest = Math.max(1,2);
var biggest = Math.max(1,2,3);
var biggest = Math.max(1,2,3,4);
var biggest = Math.max(1,2,3,4,5,6,7,8,9,10,2058);

如果我们现在要处理数组,必须这样写:

var biggest = Math.max(list[0],list[1],list[2]);

这就要求我们必需提前知道数组大小,否则用起来会很麻烦,似乎只有通过循环来解决。不过本着一种忍者精神,我们可以想一想,是否有一种更简单地方法,可以把array作为可变长参数传入呢?

有!那就是 apply() 函数。

回想一下,任何函数都有call和apply方法,包括js的内建函数(我们在“模仿数组”的例子中已经见识了)。下面我们看看如何使用这个能力来实现适于数组的min/max函数。

Listing 4.11 Generic min() and max() functions for arrays

function smallest(array){                         //#1 定义寻找最小
  return Math.min.apply(Math, array);             //#1 数组元素的函数
}                                                

function largest(array){                          //#2 寻找数组最大
  return Math.max.apply(Math, array);             //#2 元素
}                                                 

assert(smallest([0, 1, 2, 3]) == 0,               //#3 测试
       "Located the smallest value.");            //#3
assert(largest([0, 1, 2, 3]) == 3,                //#3
       "Located the largest value.");             //#3

就是这么简单。注意我们把Math设为了函数上下文,其实这里函数上下文可以为任意对象,设成Math只是显得更清晰明了。

4.4.2 Function overloading

在3.3节我们介绍了 arguments 参数,它会隐式的传入任何函数中。下面我们更加“近距离”的观察一下它。

因为它包含了所有传入的参数,所以即使我们只指定了一定数目的参数,我们依然可以获得所有参数值。通过它,我们可以方便的实现函数重载。

DETECTING AND TRAVERSING ARGUMENTS

js的函数重载就定义一个函数,通过监控参数的数目和内容实现不同功能。

下面的代码中,我们将把多个object的属性融合到一个root对象,这在继承中非常有用(我们将在第六章对象原型中详细讨论)

Listing 4.12 Traversing variable-length argument lists

function merge(root){                                   //#1 函数实现
  for (var i = 1; i < arguments.length; i++) {
    for (var key in arguments[i]) {
      root[key] = arguments[i][key];
    }
  }
  return root;
}

var merged = merge(                                     //#2 调用merge
  {name: "Batou"},                                      //#2
  {city: "Niihama"});                                   //#2

assert(merged.name == "Batou",                          //#3 测试
       "The original name is intact.");                 //#3
assert(merged.city == "Niihama",                        //#3
       "And the city has been copied over.");           //#3

其中merge只定义了一个参数,不过我们传入的参数个数则是没有限制的,即使不传参数也可以。函数是否可以正确处理这些参数完全取决于函数的定义本身。

我们只定义了root参数,说明我们只需要给第一个参数名字,其他参数没有名字。

TIP: 通过判断paramname === undefined可以知道该参数是否传入了。

我们通过从索引1往后遍历arguments的方法来获取其他参数。对于每个对象,我们遍历对象属性,并把属性赋值给root。

TIP: for-in会遍历所有对象的属性,并把属性名作为字符串赋值给key

这个功能是不是很灵活?事实上很多库都大量使用了这个技巧实现函数重载,比如jQuery UI,你可以通过dialog创建或操作对话框:

创建对话框如下:

$("#myDialog").dialog({ caption: "This is a dialog" })

操作对话框如下,这个命令就打开了对话框:

$("#myDialog").dialog("open");

dialog函数就是通过检测参数的类型,实现了不同功能。

下面我们再看一个稍微复杂的例子。

SLICING AND DICING AN ARGUMENTS LIST

下面的例子要把第一个参数和后面参数中最大的数相乘,返回乘积结果。尽管实际中这种需求很少,但是对于学习arguments的更多操作技巧有帮助。

初看起来,这个很容易实现,我们的代码可能如下:

Listing 4.13 Slicing the arguments list

function multiMax(multi){
  return multi * Math.max.apply(Math, arguments.slice(1));
}

assert(multiMax(3, 1, 2, 3) == 9, "3*3=9 (First arg, by largest.)");

通过slice(1)获得后面的所有参数,然后通过max.apply获得其中的最大元素。但是当我们执行这段代码的时候,我们却得到了一个异常:

Uncaught TypeError: arguments.slice is not a function(…)

像我们之前指出的,arguments并不是一个真正的array对象,尽管它看起来很像:我们可以通过for循环遍历它,可以访问length属性,但是它却没有array对象的方法。

我们可以手动创建一个array对象,或者为arguments设计一个slice方法,不过最好是继续使用Array的slice方法,我们可以回想4.10例子中方法,如下:

Listing 4.14 Slicing the arguments list—successfully this time

function multiMax(multi){
  return multi * Math.max.apply(Math,
    Array.prototype.slice.call(arguments, 1));           //#1 使用call把arguments当做Array对待
}

assert(multiMax(3, 1, 2, 3) == 9,
       "3*3=9 (First arg, by largest.)");

通过上面的学习,我们对arguments有了更深了解。下面我们就来看看几个实现函数重载的技巧。

FUNCTION OVERLOADING APPROACHES

通过判断参数的数量和种类实现重载一般通过if-else分支实现。对于简单函数这比较方便,不过一旦函数复杂了,这种方案就非常臃肿。本节剩下的部分,我们会介绍一种技巧,利用它我们可以创建很多同名但是参数个数不同的函数,可以把他们写成独立的匿名函数而不是冗长的if-then-else-if块。

该技巧的核心,同时也是我们将首先学习的,是函数的一个几乎无人知道的属性,length。

THE FUNCTION’S LENGTH PROPERTY

函数有一个不为人知的属性,叫length,等于函数声明时使用的形参个数。注意它和arguments的length不同。如下所示:

function makeNinja(name){}
function makeSamurai(name, rank){}
assert(makeNinja.length == 1, "Only expecting a single argument");
assert(makeSamurai.length == 2, "Two arguments expected");

所以,给定一个函数,我们可以通过这2个length获知2个信息:

  • 通过函数的length属性,我们能知道形参有几个
  • 通过arguments的length属性,我们能知道实际有多少参数传入
OVERLOADING FUNCTIONS BY ARGUMENT COUNT

如上所述,我们可以通过判断arguments的个数实现函数重载:

var ninja = {
  whatever: function() {
       switch (arguments.length) {
         case 0:
           /* do something */
           break;
         case 1:
           /* do something else */
           break;
         case 2:
           /* do yet something else */
           break;
       //and so on ...
     }
  }
}

这种方法是不是看起来不像“忍者”作风?下面我们来探索一下其他方案,比如我们可以这样创建函数:

var ninja = {};
addMethod(ninja,'whatever',function(){ /* do something */ });
addMethod(ninja,'whatever',function(a){ /* do something else */ });
addMethod(ninja,'whatever',function(a,b){ /* yet something else */ });

下面重点来了,addMethod的实现如下:

Listing 4.15 A method-overloading function

function addMethod(object, name, fn) {
  var old = object[name];                        //#1 保存旧函数,如何没有合适的用old
  object[name] = function(){
    if (fn.length == arguments.length)           //#2 判断形参和实参
      return fn.apply(this, arguments)           //#2 个数是否一致
    else if (typeof old == 'function')           //#3 如果没有匹配的
      return old.apply(this, arguments);         //#3 使用old
  };
}

下面再来看看我们的例子:

var ninja = {};
addMethod(ninja,'whatever',function(){ /* do something */ });
addMethod(ninja,'whatever',function(a){ /* do something else */ });
addMethod(ninja,'whatever',function(a,b){ /* yet something else */ });

首先,我们创建了一个空对象,第一次调用addMethod时,它先创建了一个匿名函数,并赋值给whatever属性,当无参数调用whatever时,会调用fn。注意,每次调用addMethod都会创建一个闭包,其中fn和old就是闭包中的2个变量(我们将在下一章详细讲述闭包)。

第二次调用addMethod时,它会先保存上一个已经创建的函数old,然后再创建一个匿名函数赋值给whatever,当调用whatever时,如果形参个数为1,就会调用此时的fn,如果不是,则会尝试old,即第一次创建的匿名函数。

第三次调用addMethod和第二次类似,如果形参个数为2,则会调用此时的fn,否则会执行第二次创建的匿名函数old。在old中,如果参数个数不是1,则尝试在第一次中生成的匿名函数。

整个过程就像剥洋葱,先检查最外层的函数是不是符合要求,否则就一层层向里检查。其实和if-else逻辑很像。

下面我们就来测试一下我们的addMethod函数

Listing 4.16 Testing the addMethod() function

var ninjas = {                                                    //#1 测试对象
 values: ["Dean Edwards", "Sam Stephenson", "Alex Russell"]
};

addMethod(ninjas, "find", function(){                             //#2 增加无参数函数
  return this.values;
});

addMethod(ninjas, "find", function(name){                         //#3 接受一个参数的函数
  var ret = [];
  for (var i = 0; i < this.values.length; i++)
    if (this.values[i].indexOf(name) == 0)
      ret.push(this.values[i]);
  return ret;
});

addMethod(ninjas, "find", function(first, last){                 //#4 接受2个参数的函数
  var ret = [];
  for (var i = 0; i < this.values.length; i++)
    if (this.values[i] == (first + " " + last))
      ret.push(this.values[i]);
  return ret;
});

assert(ninjas.find().length == 3,                                //#5 测试
       "Found all ninjas");
assert(ninjas.find("Sam").length == 1,
       "Found ninja by first name");
assert(ninjas.find("Dean", "Edwards").length == 1,
       "Found ninja by first and last name");
assert(ninjas.find("Alex", "Russell", "Jr") == null,
       "Found nothing");

这个方案非常漂亮,我们并没有把这些匿名函数保存在典型数据结构中,它们只是闭包中的引用。当然它也有一些不足

  • 只能区分参数个数,不能区分参数种类、名字等。而我们经常需要这种区分。
  • 函数调用的性能有些损失。在追求高性能的应用中可能不适合。

这一节讲了一等公民“函数”的很多功能,下面我们讲讲如何分辨函数。