Blog
Blog copied to clipboard
JavaScript深入之类数组对象与arguments
类数组对象
所谓的类数组对象:
拥有一个 length 属性和若干索引属性的对象
举个例子:
var array = ['name', 'age', 'sex'];
var arrayLike = {
0: 'name',
1: 'age',
2: 'sex',
length: 3
}
即便如此,为什么叫做类数组对象呢?
那让我们从读写、获取长度、遍历三个方面看看这两个对象。
读写
console.log(array[0]); // name
console.log(arrayLike[0]); // name
array[0] = 'new name';
arrayLike[0] = 'new name';
长度
console.log(array.length); // 3
console.log(arrayLike.length); // 3
遍历
for(var i = 0, len = array.length; i < len; i++) {
……
}
for(var i = 0, len = arrayLike.length; i < len; i++) {
……
}
是不是很像?
那类数组对象可以使用数组的方法吗?比如:
arrayLike.push('4');
然而上述代码会报错: arrayLike.push is not a function
所以终归还是类数组呐……
调用数组方法
如果类数组就是任性的想用数组的方法怎么办呢?
既然无法直接调用,我们可以用 Function.call 间接调用:
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
Array.prototype.join.call(arrayLike, '&'); // name&age&sex
Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
// slice可以做到类数组转数组
Array.prototype.map.call(arrayLike, function(item){
return item.toUpperCase();
});
// ["NAME", "AGE", "SEX"]
类数组转数组
在上面的例子中已经提到了一种类数组转数组的方法,再补充三个:
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
// 1. slice
Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"]
// 2. splice
Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"]
// 3. ES6 Array.from
Array.from(arrayLike); // ["name", "age", "sex"]
// 4. apply
Array.prototype.concat.apply([], arrayLike)
那么为什么会讲到类数组对象呢?以及类数组有什么应用吗?
要说到类数组对象,Arguments 对象就是一个类数组对象。在客户端 JavaScript 中,一些 DOM 方法(document.getElementsByTagName()等)也返回类数组对象。
Arguments对象
接下来重点讲讲 Arguments 对象。
Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。
举个例子:
function foo(name, age, sex) {
console.log(arguments);
}
foo('name', 'age', 'sex')
打印结果如下:
我们可以看到除了类数组的索引属性和length属性之外,还有一个callee属性,接下来我们一个一个介绍。
length属性
Arguments对象的length属性,表示实参的长度,举个例子:
function foo(b, c, d){
console.log("实参的长度为:" + arguments.length)
}
console.log("形参的长度为:" + foo.length)
foo(1)
// 形参的长度为:3
// 实参的长度为:1
callee属性
Arguments 对象的 callee 属性,通过它可以调用函数自身。
讲个闭包经典面试题使用 callee 的解决方法:
var data = [];
for (var i = 0; i < 3; i++) {
(data[i] = function () {
console.log(arguments.callee.i)
}).i = i;
}
data[0]();
data[1]();
data[2]();
// 0
// 1
// 2
接下来讲讲 arguments 对象的几个注意要点:
arguments 和对应参数的绑定
function foo(name, age, sex, hobbit) {
console.log(name, arguments[0]); // name name
// 改变形参
name = 'new name';
console.log(name, arguments[0]); // new name new name
// 改变arguments
arguments[1] = 'new age';
console.log(age, arguments[1]); // new age new age
// 测试未传入的是否会绑定
console.log(sex); // undefined
sex = 'new sex';
console.log(sex, arguments[2]); // new sex undefined
arguments[3] = 'new hobbit';
console.log(hobbit, arguments[3]); // undefined new hobbit
}
foo('name', 'age')
传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享
除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。
传递参数
将参数从一个函数传递到另一个函数
// 使用 apply 将 foo 的参数传递给 bar
function foo() {
bar.apply(this, arguments);
}
function bar(a, b, c) {
console.log(a, b, c);
}
foo(1, 2, 3)
强大的ES6
使用ES6的 ... 运算符,我们可以轻松转成数组。
function func(...arguments) {
console.log(arguments); // [1, 2, 3]
}
func(1, 2, 3);
应用
arguments的应用其实很多,在下个系列,也就是 JavaScript 专题系列中,我们会在 jQuery 的 extend 实现、函数柯里化、递归等场景看见 arguments 的身影。这篇文章就不具体展开了。
如果要总结这些场景的话,暂时能想到的包括:
- 参数不定长
- 函数柯里化
- 递归调用
- 函数重载 ...
欢迎留言回复。
下一篇文章
深入系列
JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog。
JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。
如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。
感觉如果类数组对象的原型指向 Array.prototype 他可以被认为是一个数组了。
毕竟 typeof {} 跟 typeof [] 结果是一样的。
@eczn 哈哈,把原型指向Array.prototype后就可以调用Array.prototype上的方法,行为上确实是跟数组一样,然而Array.isArray和Object.prototype.toString不认呐😂

233 很接近 但是还是有所区别
Array.prototype.concat.apply([], arguments) 这个是不是写错了,不应该是arguments
@stoneyallen 十分感谢指出,确实是写错了。o( ̄▽ ̄)d
谢谢楼主的分享!我把您的文章里的 demo 全敲了一遍,有两个地方不太明白,还请指教! md格式的不太会用写的有点丑陋,还请见谅
callee 属性 解决闭包经典面试题的那个例子,虽然跑通了,但不明白是什么意思?
这是什么写法,不太懂??
(data[i] = function () { console.log(arguments.callee.i) }).i = i;
传递参数里面,demo 没有跑通
`
function foo(){
bar.apply(this,arguments);
// 这句的意思是把 bar的参数 传递给 foo 吗? 如果是的话,下面会打印出 3 ,
console.log(arguments.callee.length); // 0
}
function bar(a,b,c){
console.log(arguments); // []
console.log(arguments.callee.length); // 3
}
foo()
bar()
`
还有楼主应该在补充讲一下,arguments还有一个属性 caller 指向 调用当前函数的函数的引用
哈哈,那我把我的回复再回复一遍哈,如果以后有相同的问题,大家也都可以看到~
关于第一个问题,写个简单例子:
var fun1 = function(){}
fun1.test = 'test';
console.log(fun1.test)
函数也是一种对象,我们可以通过这种方式给函数添加一个自定义的属性。 这个解决方式就是给 data[i] 这个函数添加一个自定义属性,这个属性值就是正确的 i 值。
关于第二个问题,是把foo的参数传递给bar,可以看这个跑通的例子:
function foo() { bar.apply(this, arguments); }
function bar(a, b, c) { console.log(a, b, c) }
foo(1, 2, 3)
关于caller,直接截图MDN哈:

解释的很详细!!我再补充点
类数组检测
function isArrayLike(o) {
if (o && // o is not null, undefined, etc.
typeof o === 'object' && // o is an object
isFinite(o.length) && // o.length is a finite number
o.length >= 0 && // o.length is non-negative
o.length===Math.floor(o.length) && // o.length is an integer
o.length < 4294967296) // o.length < 2^32
return true; // Then o is array-like
else
return false; // Otherwise it is not
}
arguments
如图可以看出
- arguments的长度只与实参的个数有关,与形参定义的个数没有直接关系。
- arguments 有一个Symbol(Symbol.iterator)属性这个表示该对象是可迭代的
思考
- 字符串可以像类数组一样操作是因为js自动包装成String对象的原因,String对象照上面检测函数也是类数组对象。不过因为本身值不能被改变,所以给指定下标赋值不会改变。
@gnipbao 非常感谢补充!o( ̄▽ ̄)d
这个类数组对象的判断方法应该是来自《JavaScript权威指南》吧,很多库比如 underscore 和 jQuery 中也有对数组和类数组对象的判断,比如 jQuery 的实现:
function isArrayLike(obj) {
// obj必须有length属性
var length = !!obj && "length" in obj && obj.length;
var typeRes = type(obj);
// 排除掉函数和Window对象
if (typeRes === "function" || isWindow(obj)) {
return false;
}
return typeRes === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
underscore 的实现:
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var isArrayLike = function(collection) {
var length = collection.length;
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; var isArrayLike = function(collection) { var length = collection.length; return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; }; 这种判断方式 array 也会返回 true
@dongliang1993 确实是这样的, jQuery 和 underscore 的 isArrayLike 都是既判断数组又判断类数组对象的~
这样的话,感觉 _.each 函数就有点问题了,
_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context)
var i, length
if (isArrayLike(obj)) { // const obj = {a: 1, length: 1} 会直接进入到下面的循环,变成了obj[0],obj[1]
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj)
}
} else {
const keys = _.keys(obj)
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj) // (value, key, obj)
}
}
return obj
};
其实应该是要用 for in 来遍历吧?
@dongliang1993 没有问题呐,类数组对象是可以使用 for 循环遍历的呐~
@mqyqingfeng 我的意思是,如果是 obj = { name: 'xiaoming', length: 1 } 这样的类数组对象,isArrayLike 判断为 true,然后进入相应的迭代器,用 for 循环是 iteratee(obj[i], i, obj) 这样的,可是 i 是 0, 1, 2...这样的数字,那 obj[0],obj[1] 都是 undefined呀,可是 obj 明明是有 'name' 这个属性的。不知道大佬有没有看明白我的意思。。。
@dongliang1993 确实会出现这样的问题, { name: 'xiaoming', length: 1 } 可以通过 underscore 的 isArrayLike 验证,但是在 each 函数中,obj[0] 为 undefined。关键还是在于这个对象并不是一个严格意义上的类数组对象,isArrayLike 可以校验出我们开发中会用到的 arguments 对象,满足我们的开发需求,但是对于我们故意创造出的对象,确实也会漏掉~
@dongliang1993 如果用 for in 遍历类数组对象的话,length 和 自定义的一些属性也会被遍历到,也会导致问题吧~
好像说在函数中传递arguments给任何参数,将导致Chrome和Node中使用的V8引擎跳过对其的优化,这也将使性能相当慢。 请问博主知道其中的原因么?
忘了在哪里看到的了
@huangmxsysu 这个是来自 blueBird 的 wiki,https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#3-managing-arguments,以前也查过这个问题,之所以降低性能,是因为:
Leaking the arguments object kills optimization because it forces V8 to instantiate the arguments as a Javascript object instead of optimizing them into stack variables.
当时想不明白的是为什么 [].slice.call(arguments) 依然会导致性能损失,现在想想,可能是因为将 this 指向 arguments,所以依然保持了对 arguments 的引用吧
其实本篇应该添加 leaking arguments 的部分,告诉大家不要乱用 arguments 😂
噢好像是因为这个原因哈! 是啊,昨天看到你数组去重那篇中有个_.union函数,就想着应该讲arguments转换一下,类似这样
function union() {
//最好能把arguments转换一下
var args = new Array(arguments.length);
for(var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return unique(flatten(args, true, true));
}
flatten那篇
@huangmxsysu 我并不确定直接传递 arguments 会不会导致性能损失,因为在 wiki 里也只是讲了以下三种情况会导致 leaking arguments:
function leaksArguments1() {
return arguments;
}
function leaksArguments2() {
var args = [].slice.call(arguments);
}
function leaksArguments3() {
var a = arguments;
return function() {
return a;
};
}
而且也讲到了:
STRICTLY fn.apply(y, arguments) is ok, nothing else is, e.g. .slice. Function#apply is special.
我简单做了个试验:
function otherFunc(a, b) {
"use strict";
return a + b;
}
function withArguments() {
"use strict";
var a = arguments;
return otherFunc.apply(null, arguments);
}
function withLeakingArguments() {
"use strict";
var a = arguments;
return otherFunc.apply(null, [].slice.call(arguments));
}
function withCopy() {
"use strict";
var a = [];
var i, len = arguments.length;
for (i = 1; i < len; i += 1) {
a[i - 1] = arguments[i];
}
return otherFunc.apply(null, a);
}
console.time('with arguments')
for (var i = 0; i < 100000; i++) {
withArguments(1, 2)
}
console.timeEnd('with arguments')
console.time('with leaking arguments')
for (var i = 0; i < 100000; i++) {
withLeakingArguments(1, 2)
}
console.timeEnd('with leaking arguments')
console.time('with copy')
for (var i = 0; i < 100000; i++) {
withCopy(1, 2)
}
console.timeEnd('with copy')
一次试验的结果为:

当然这个测试可能并不准确,并不能确定就是 leaking arguments 导致的性能损失,不过我想直接使用 arguments 的话,底层应该会有优化
@mqyqingfeng
let elements = document.getElementsByClassName('box');
Array.prototype.splice.call(elements, 0);
// Uncaught TypeError: Cannot assign to read only property 'length' of object '#<HTMLCollection>'
// slice方法可以
@veedrin 感谢指出哈~ 确实没有注意到这个问题。
如果是普通的类数组对象,使用 splice 是可以转换的,比如:
var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 }
Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"]
然而对于 HTMLCollection,length 属性为只读,splice 底层还是会修改 length 的长度,这才导致了报错。
@mqyqingfeng 为什么你们判断是不是类数组,isArrayLike 函数需要这么多判断,直接查看它的 是否为 [object object] 和 length 存在就行了吗?难道在哪些情况下会有问题吗?这个我还没遇到过哦,请指教指教!
@jxZhangLi 哈哈,都可以啦,因为对于类数组对象的判断,其实可以很宽,也可以很严格,比如说判断 length 属性存在,更严格的话,可以判断 length 属性值必须是数字,再严格的话,可以判断 length 属性值必须大于 0,再再严格的话,可以判断 length -1 属性值必须存在,看 API 的设计者想严格到什么程度啦,在满足当前业务的情况下,即使设计的很宽松,也是可以的,但是作为一个库的设计者的话,还是应该设计的更加严格一点~