为什么引入实参对象arguments
在JS开发中,每一个函数都对应一个实参对象,称为arguments。这个对象引用的目的是为了解决如下问题:
当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。
因为JS函数定义与调用极其灵活,参数个数是不确定的,而且系统也不会作自动检测。这为开发带来灵活性的同时也带来相当的麻烦。下文将结合实际开发中使用到arguments时经常遇到的几个“麻烦”进行讨论,并给出对应的解决方案。
在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。
根据上面的分析,不难得出这样的结论:可以让一个函数轻松地操作任意数目的实参。例如下面的例子,返回任意个数的一组数据中的最大值。
function maxValue(/*...*/){ var max=Number.NEGATIVE_INFINITY; for(var i= 0,n=arguments.length;i< n;i++){ if(arguments[i]>max) max=arguments[i]; } return max; } var largestData=maxValue(3,342,3,45454,999,-2929,999); console.log("larget is: "+largestData);
专家建议:arguments[]对象最适合的应用场景是在这样一类函数中,这类函数包含固定个数的命名和必需参数,以及随后个数不定的可选实参。
作为一个类数组对象,arguments包含了所有函数调用时的实参信息。但是,如果在嵌套函数中也需要外层函数的arguments信息时容易出现问题。请参考下面的开码片段。
var it=List("JavaScript","Java","C#","C++","Objective-C","C","Swift","Python","Visual Basic"); it.next(); it.next(); function List(){ var start= 0,n=arguments.length; return{ hasNext:function(){ return start<n; } , next:function(){ if(start>=n){ throw new Error("End of iteration!"); } console.log( arguments[start++]); //wrong usage } }; }
undefined
undefined
细究起来,问题就出在迭代器next方法中最后一句,原意图是在此调用外层函数的实参对象arguments,但正是在此出现错误―这时的arguments是本层函数对应的实参对象(元素个数为0),根本就不同于外层函数的实参对象arguments,所以才有上面的输出结果。理解了这一点,上面的代码便可以轻松地修改为如下形式:
var it=List("JavaScript","Java","C#","C++","Objective-C","C","Swift","Python","Visual Basic"); it.next(); it.next(); function List(){ var start= 0, n=arguments.length, outerA=arguments; return{ hasNext:function(){ return start<n; } , next:function(){ if(start>=n){ throw new Error("End of iteration!"); } console.log( outerA[start++]); //correct usage } }; }
JavaScript
Java
总结:上面的技巧在实际开发中也经常使用到,应当熟练掌握。
arguments是一个实参对象。尽管每个实参对象都包含以数字为索引的一组元素以及length属性,但它们的确不是真正的数组。
实参对象包含一个非同寻常的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参的别名,形参名称可以认为是相同变量的不同命名。通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值。
下面这个例子清楚地说明了这一点:
function f(x){ console.log(x);//输出实参的初始值 arguments[0]=null; console.log(x);//输出"null" } var v=100; f(v); console.log(v);
输出结果如下:
100 //输出实�⒌某跏贾�
null
100//因为JS函数参数是传值方式调用的
在这个例子中,arguments[0]和x指代同一个值,修改其中一个的值会影响到另一个。
既然arguments对象是函数参数的别名,而不是函数参数的副本,那么有意或者无意地修改arguments对象会冒着使函数命名参数失去意义的危险。因此,在ECMAScript5中移除了实参对象的上述特殊特性,即函数参数不支持对其arguments对象取别名。请参考下面的代码:
function strictTest(val){ 'use strict'; arguments[0]="modified"; //不再支持这种修改,但是没有显式给出错误提示 console.log( val===arguments[0]); } function nonstrictTest(val){ arguments[0]="modified"; console.log(val===arguments[0]); } strictTest("original"); nonstrictTest("original");
false
true
在严格模式下还有一点(和非严格模式下相比的)不同,在非严格模式中,函数里的arguments仅仅是一个标识符,在严格模式中,它变成了一个保留字。严格模式中的函数无法使用arguments作为形参名或局部变量名,也不能给arguments赋值。
但是,实际开发中往往需要从函数内部修改传入的参数,特别是在不定参数的情况下。此时想到的一个办法是:尽早地复制实参对象arguments中数据到一个真正的数组中。为此,可以使用如下编码技巧:
var args=Array.prototype.slice.call(arguments);
var args=[].slice.call(arguments);
var args=[].slice.call(arguments,N);
上述技巧在实际开发及JS库中广泛使用。
callee和caller属性除了数组元素,实参对象还定义了callee和caller属性。在ECMAScript5严格模式中,对这两个属性的读写操作都会产生一个类型错误。而在非严格模式下,ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller是非标准的,但大多数测览器都实现了这个属性,它指代调用当前正在执行的函数的函数。
严格模式下,参考下面代码:
function f(){ 'use strict'; return f.caller; } f();
上面代码运行时出现运行时错误,信息如下:
TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
把上面函数内部的f.caller修改为f.callee时,如下:
function f(){ 'use strict'; return f.callee; } console.log(f());
undefined
专家建议:避免使用arguments对象的callee和caller属性,除非有特殊的针对性需要并明确由此带来的后果。