我刚才碰到了一个问题,arguments接受的实参是一个列表,得到的是一个像数组一样的东西,于是我想实现无限参数求和,在遍历数组求和时,使用了forEach遍历,却报错了,这是为什么,代码如下:
sum(1,2,3,4,5);
function sum () {
var total = 0;
arguments.forEach(v =>{
total += v;
})
return total()
}
执行代码后出现报错如下:
那么arguments既然得到一个类似于数组的东西,使用forEach方法为什么会报错呢?最后在百度查了半天,实际上,它并不是一个真正的数组,而是一个类数组或者叫伪数组,也就是一个类似于数组的东西。
下面我们一起看看什么是伪数组呢?
1.我们先来看看用于接受实参的方法 arguments , 执行代码如下:
function fu() {
console.log(arguments);
}
fu(1,2,3,4,5,)
控制台打印出来如下:
这里可以看到,Arguments显示的也有方括号 [1,2,3,4,5…] ,但是后面多了一些其他方法;也有length属性,但没有数组的push,pop等那些方法,像数组又不是数组。
2.大家也可以去打印看看其他比如getElementsByClassName 得到的:
<template>
<div></div>
<div></div>
<div></div>
</template>
<script>
var oDivs = document.getElementsByTagName('div')
console.log(oDivs)
</script>
控制台打印出来如下:
这里可以看到,HTMLCollection显示的也有方括号 [1,2,3,4,5…] ,但是后面多了一些其他方法;也有length属性,但同样没有数组的push,pop等那些方法,像数组又不是数组。
所以简单总结一下,伪数组具有以下特点:
1、拥有length属性,可以获取长度;
2、拥有角标索引,可以通过length属性遍历获取所有值。
3、但是不可以使用数组的内置方法。
那么接下来就得研究下为什么伪数组不能使用数组的内置方法和属性呢?
首先从上图标注的 [[prototype]]可以看到arguments伪数组的原型指向的是Object对象再来打印真数组,
可以看到,真数组的proto指向的是Array数组
同样 从上图也可以看出 [[prototype]]可以看到通过标签名获取页面元素拿到的伪数组的原型指向的是HTMLCollection对象
至此,我们也可以看出伪数组的原型不一样,其实就是不同的对象,而数组的元素是Array,他们和真数组的本质不一样,这也是伪数组为什么不能使用数组的属性和方法的根本原因,也就是说如果没有想使用数组的方法比如forEach,pop等方法,必须把他们转化为真数组才能使用数组的各种方法。
接下来就是研究如何将伪数组怎样转成真数组:
以下就是代码表演阶段:
方法一:遍历:创建一个空数组,循环遍历伪数组,将遍历出的数据逐一放在空数组中
伪数组:
var ali = document.getElementsByTagName('li');
console.log(ali); // [li, li, li, li]
// ali.push("hello"); // TypeError: ali.push is not a function
...........................................................................................分割线
转为真数组:
var arr = []; // 先创建空数组
for(var i=0;i<ali.length;i++){
// 循环遍历伪数组
arr[i] = ali[i]; // 取出伪数组的数据,逐个放在真数组中
}
arr.push("hello");
console.log(arr); // [li, li, li, li, "hello"]
方法二:通过调用Array的slice的截取方法。[].slice.call()
const divs = document.querySelectorAll('div');
console.log(divs instanceof Array);
const arr = [].slice.call(divs);
console.log(arr instanceof Array);
// false
// true
想知道怎么通过slice方法实现转换就需要知道数组的slice方法内部实现原理。下面我简单的模拟一下slice方法。
function slice(start,end){
const startIndex = start || 0;
const endIndex = end || this.length;
let tempArr = [];
for(let i = startIndex;i < endIndex;i++){
tempArr.push(this[i]);
}
return tempArr;
}
里面的this就是能实现伪数组转换成真数组的关键,通过call()方法将里面的this绑定到伪数组对象上,再通过循环将伪数组的每一项push进一个真数组再返回这个真数组。
方法三:使用slice方法:利用Array原型对象的slice方法,配合apply,将slice中的this指向伪数组。Array.prototype.slice.call()
第三种也是通过调用Array的slice的截取方法,下面总结处有第二种和第三种的区别分析。
伪数组:
var ali = document.getElementsByTagName('li');
console.log(ali); // [li, li, li, li]
// ali.push("hello"); // TypeError: ali.push is not a function
..........................................................................................分割线
转为真数组:
var arr = Array.prototype.slice.apply(ali);
arr.push("hello");
console.log(arr); // [li, li, li, li, "hello"]
方法四:利用ES6提供的Array的from方法 /此方法存在兼容性问题。ES6 Array.form()
伪数组:
var ali = document.getElementsByTagName('li');
console.log(ali); // [li, li, li, li]
// ali.push("hello"); // TypeError: ali.push is not a function
........................................................................................分割线
转换为真数组:
var arr = Array.from(ali);
arr.push("hello");
console.log(arr); // [li, li, li, li, "hello"]
方法五:利用ES6提供的展开运算符(…)/此方法存在兼容性问题
伪数组:
var ali = document.getElementsByTagName('li');
console.log(ali); // [li, li, li, li]
// ali.push("hello"); // TypeError: ali.push is not a function
...........................................................................................分割线
转换为真数组:
var arr = [...ali];
arr.push("hello");
console.log(arr); // [li, li, li, li, "hello"]
方法六:利用原型的复制:将伪数组的proto复制为Array的prototype。但是这种方法有局限性
- 手动创建具有索引和长度的对象,作为伪数组
var obj = {
0:"a",
1:"b",
2:"c",
length:3
}
console.log(obj); // {0: "a", 1: "b", 2: "c", length: 3}
// obj.push(); // TypeError: obj.push is not a function
obj.__proto__ = Array.prototype;
console.log(obj); // ["a", "b", "c"]
obj.push("hello");
console.log(obj); // ["a", "b", "c", "hello"]
- arguments也适用
function fn(){
var arg = arguments;
console.log(arg); // ["a", "b", ...]
// arg.push("hello"); // TypeError: arg.push is not a function
arg.__proto__ = Array.prototype;
arg.push("hello");
console.log(arg); // ["a", "b", "hello", ...]
}
fn("a","b");
- 选择器返回的元素集合不适用,因为就算将元素集合的原型改成了数组原型(如图5),但元素集合本身是只读的,依然不能修改
var ali = document.getElementsByTagName('li');
console.log(ali); // [li, li, li, li]
// ali.push("hello"); // TypeError: ali.push is not a function
ali.__proto__ = Array.prototype;
// ali.push("hello"); // Index property setter is not supported
但是不是意味着,没有修改到原数组的方法就可以使用呢(注意上图,没有length属性)
ali.forEach(val => {
console.log(val);
});
// 会发现浏览器没执行,原因是此时ali缺少length或length为0
console.log(ali.length); // 0
需要提前获取ali的length,在修改原型之后再手动设置
var ali = document.getElementsByTagName('li');
console.log(ali); // [li, li, li, li]
// ali.push("hello"); // TypeError: ali.push is not a function
var len = ali.length; // 获取初始length
ali.__proto__ = Array.prototype;
ali.length = len; // 设置给修改原型之后的数组对象
// ali.push("hello"); // Index property setter is not supported
ali.forEach(val => {
console.log(val); // 遍历打印数组中的值
});
console.log(ali.length); // 4
小提示:选择器返回的元素数组,使用复制原型方法,还需要手动设置length属性,且不能使用会改变原数组的方法。
总结:
- [].slice.call()与Array.prototype.slice.call()的区别就在于通过Array.prototype找效率更高。
- 通过[]去调用slice方法需要通过原型链去找slice方法。
- 解构赋值手动创建的伪数组对象时, 需要添加iterator接口。
以上是小编对伪数组转真数组的理解及解决方法,有些方法是查询的,如文中有错误的,欢迎大佬留言指正…