为什么引入实参对象arguments
在JS开发中,每一个函数都对应一个实参对象,称为arguments。这个对象引用的目的是为了解决如下问题:
当调用函数的时候传入的实参个数超过函数定义时的形参个数时,没有办法直接获得未命名值的引用。
因为JS函数定义与调用极其灵活,参数个数是不确定的,而且系统也不会作自动检测。这为开发带来灵活性的同时也带来相当的麻烦。下文将结合实际开发中使用到arguments时经常遇到的几个“麻烦”进行讨论,并给出对应的解决方案。
在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样可以通过数字下标就能访问传入函数的实参值,而不用非要通过名字来得到实参。
根据上面的分析,不难得出这样的结论:可以让一个函数轻松地操作任意数目的实参。例如下面的例子,返回任意个数的一组数据中的最大值。
1
2
3
4
5
6
7
8
9
|
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包含了所有函数调用时的实参信息。但是,如果在嵌套函数中也需要外层函数的arguments信息时容易出现问题。请参考下面的开码片段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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,所以才有上面的输出结果。理解了这一点,上面的代码便可以轻松地修改为如下形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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的技巧
arguments是一个实参对象。尽管每个实参对象都包含以数字为索引的一组元素以及length属性,但它们的确不是真正的数组。
实参对象包含一个非同寻常的特性。在非严格模式下,当一个函数包含若干形参,实参对象的数组元素是函数形参所对应实参的别名,形参名称可以认为是相同变量的不同命名。通过实参名字来修改实参值的话,通过arguments[]数组也可以获取到更改后的值。
下面这个例子清楚地说明了这一点:
1
2
3
4
5
6
7
8
|
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对象取别名。请参考下面的代码:
1
2
3
4
5
6
7
8
9
10
11
|
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中数据到一个真正的数组中。为此,可以使用如下编码技巧:
1
|
var
args=Array.prototype.slice.call(arguments);
|
1
|
var
args=[].slice.call(arguments);
|
1
|
var
args=[].slice.call(arguments,N);
|
上述技巧在实际开发及JS库中广泛使用。
关于arguments对象的callee和caller属性
callee和caller属性除了数组元素,实参对象还定义了callee和caller属性。在ECMAScript5严格模式中,对这两个属性的读写操作都会产生一个类型错误。而在非严格模式下,ECMAScript标准规范规定callee属性指代当前正在执行的函数。caller是非标准的,但大多数测览器都实现了这个属性,它指代调用当前正在执行的函数的函数。
严格模式下,参考下面代码:
1
2
3
4
5
|
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时,如下:
1
2
3
4
5
|
function
f(){
'use strict'
;
return
f.callee;
}
console.log(f());
|
undefined
专家建议:避免使用arguments对象的callee和caller属性,除非有特殊的针对性需要并明确由此带来的后果。
本文转自朱先忠老师51CTO博客,原文链接: http://blog.51cto.com/zhuxianzhong/1657417,如需转载请自行联系原作者