# 第10期 浅析类数组对象

# 什么是类数组对象

简单来说,和数组类似,拥有length属性,可以通过索引来访问或设置里面的元素,但是不能使用数组的方法。

看个例子:

arr[0];
// => "dazhi"

这里的arr一定是一个数组吗?不一定,也可能是一个对象。

let arr = {
    0: 'dazhi'
}
console.log(arr[0]); // dazhi

再来看个例子:

let arr = ['name', 'age', 'job'];

// 创建一个类数组对象
let arrLike = {
    0: 'name',
    1: 'age',
    2: 'job',
    length: 3
}

注意:这边arrLike必须加上length属性,不然它就是一个普通对象而已。

为什么叫做类数组对象呢?我们从读写、获取长度、遍历这三个方面来看看这两个对象。

# 读写

console.log(arr[0]); // name
console.log(arrLike[0]); // name

arr[0] = 'new name';
arrLike[0] = 'new name';

console.log(arr[0]); // new name
console.log(arrLike[0]); // new name

# 获取长度

console.log(arr.length); // 3
console.log(arrLike.length); // 3

# 遍历

for (let i = 0;i < arr.length; i++){
    console.log(arr[i]);
}

// name 
// age 
// job

for (let i = 0;i < arrLike.length; i++){
    console.log(arrLike[i]);
}
// name 
// age 
// job

有没有很像?当我们使用使用数组的方法呢?继续往下看:

arr.push('gender');

arrLike.push('gender');  // 报错

原形毕露了,终归是类数组。

那如果类数组就想用数组的方法呢?可以把类数组转换为数组。

# 转换为数组

# Array.prototype.slice.call()

var arrLike2 = Array.prototype.slice.call(arrLike);
arrLike2.push('gender');
console.log(arrLike2[3]); // gender

# Array.from()

var arrLike3 = Array.from(arrLike);
arrLike3.push('gender');
console.log(arrLike3[3]); // gender

# Array.prototype.splice.call()

var arrLike4 = Array.prototype.splice.call(arrLike, 0);

arrLike4.push('gender');

console.log(arrLike4[3]); // gender

# 注意

如果length值和实际元素不相等呢?

let arrLike = {
    0: 'name',
    1: 'age',
    length: 3
}
console.log(Array.from(arrLike)); // ["name", "age", undefined]

可以看到,如果length值大于实际元素的数量,不足的将用undefined填充。

如果反过来呢?

let arrLike = {
    0: 'name',
    1: 'age',
    length: 1
}
console.log(Array.from(arrLike)); // ["name"]

最终只保留了一个元素。可见,length值是决定最终生成数组的长度的,多余的去掉,不足的用undefined填充。

那如果我们索引不从0和1开始,可以吗?

let arrLike = {
    2: 'name',
    3: 'age',
    length: 2
}
console.log(Array.from(arrLike));  // [undefined, undefined]

可见,0和1是有用的,会影响到最终的填充索引。

# 类数组对象的应用

说了这么多,类数组能用来做什么?

还记得arguments对象吗?它就是一个类数组对象。我们看下MDN上对其的描述:

# Arguments对象

arguments对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments对象在函数中引用函数的参数。arguments对象不是一个Array。它类似于Array,但除了length属性和索引元素之外没有任何Array属性。例如,它没有pop方法。但它可以被转换为一个真正的Array

MDN地址:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/arguments

看个例子:

function foo(a, b, c) {
    console.log(arguments);
}

foo(1, 2, 3);

控制台看下打印结果:

我们可以通过arguments对象在函数内部引用传递进来的参数:

function foo(a, b, c) {
    console.log(arguments[0]);
    console.log(arguments[1]);
    console.log(arguments[2]);
}

foo(1, 2, 3);
// 1
// 2
// 3

# 1.length属性

argumentslength属性代表的是实参的个数,因为我们调用函数的时候,有时候并不是所有参数都需要传入,看下面的代码:

function foo(a, b, c) {
    console.log('实参的个数:' + arguments.length); // 1
}

foo(1);

console.log('形参的个数为:' + foo.length); // 3

# 2.auguments和对应参数的绑定

function foo(a, b, c, d) {
    // "use strict";
    console.log(a, arguments[0]); // 1 1
    
    // 改变形参
    a = 11;
    
    console.log(a, arguments[0]); // 11 11
    
    // 改变arguments
    arguments[1] = 22;
    
    console.log(b, arguments[1]); // 22 22
    
    // 未传入的参数
    console.log(c); // undefined
    
    c = 3;
    
    console.log(c, arguments[2]); // 3 undefined
    
    arguments[3] = 4;
    console.log(d, arguments[3]); // undefined 4
}

foo(1, 2);

总结:

  1. 有传入的参数,实参和arguments的值会共享,没有传入的参数,不会共享
  2. 在严格模式下,无论参数有没有传入,实参和arguments的值都不会共享

# 3.传递参数

将参数从一个函数传递到另一个函数。

function foo() {
    bar.apply(this, arguments);
}

function bar(a, b, c) {
    console.log(a, b, c);
}

foo(1, 2, 3);

// 1 2 3

# 4.arguments转为数组

arguments除了可以用上面的几个转为数组的方法,还可以使用...展开运算符。

function bar(a, b) {
  // 用...把argumens转为数组
  console.log([...arguments]); // [1, 2]
}

bar(1, 2);

但是如果应用到普通的类数组对象呢?

let arrLike5 = {
    a: 1,
    b: 2,
    length: 2
}

console.log([...arrLike5]);  // 报错 Uncaught TypeError: arrLike5 is not iterable

报错的意思是:arrLike5不是可迭代的,也就证实了arguments除了是类数组对象,还是一个可迭代对象,而我们自定义的对象并不具备可迭代功能,所以不能使用展开运算符。

因为我们自定义的类数组对象不具备可迭代功能,所以也没办法使用for...of来遍历:

let arrLike = {
  0: 'name',
  1: 'age',
  2: 'job',
  length: 3
}

for (let arrItem of arrLike) {
  console.log(arrItem);
}

// 同样会报错 Uncaught TypeError: arrLike is not iterable

那么forEachfor...in呢?

forEach是数组的方法,自然也没办法使用。

来看下for...in

let arrLike = {
  0: 'name',
  1: 'age',
  2: 'job',
  length: 3
}

for (let index in arrLike) {
  console.log(index);
}

// 0
// 1
// 2
// length

for...in是遍历对象的可枚举属性,会把length也遍历出来。

所以只有for循环可以正确遍历类数组对象。

另外,arguments的应用其实还有很多,这里就不继续展开了,大家有兴趣可以自己再去找资料学习一下,比如:

  1. 参数不定长
  2. 函数柯里化
  3. 递归调用
  4. 函数重载
  5. ...

# 其他类数组对象

我们在页面上随便写几个p标签:

<p></p>
<p></p>
<p></p>

然后用document.getElementsByTagName()获取:

var ps = document.getElementsByTagName('p');
console.log(ps);

可以看到,里面也有length属性,但是它并不是一个数组,我们可以来检测一下:

// 类数组对象不能使用数组的方法
ps.push('a');  // 报错:Uncaught TypeError: ps.push is not a function

console.log(Object.prototype.toString.call(ps)); // [object HTMLCollection]

console.log(Object.prototype.toString.call([])); // [object Array]

好了,本文就先到这里了。

# 最后

感谢您的阅读,希望对你有所帮助。由于本人水平有限,如果文中有描述不当的地方,烦请指正,非常感谢。

# 关注

欢迎大家关注我的公众号前端帮帮忙,一起交流学习,共同进步!

参考:

https://github.com/mqyqingfeng/Blog/issues/14

上次更新: 1/22/2020, 12:07:03 AM