昨晚睡觉前刷掘金看到一道面试题,由此引发了一系列的拓展与思考。
面试题
不使用loop循环,创建一个长度为100的数组,并且每个元素的值等于它的下标
以下是我的一些解决方案
Array.from(Array(100).keys())
[...Array(100).keys()]
Object.keys(Array(100))
Array.prototype.recursion = function(length) {
if (this.length === length) {
return this;
}
this.push(this.length);
this.recursion(length);
}
arr = []
arr.recursion(100)
Array(100).map(function (val, index) {
return index;
})
当然还有一种比较作死的方法
var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
用了这种方法你也可能被考官打死!但是确实也没用loop循环。
问题与思考
问题
将上述方法挨个在控制台上跑了一遍后发现问题了Object.keys(Array(100))
的结果为[]
,即空数组
map
方法的结果为[undefined × 100]
思考
初步猜测与undefined
有关,进行尝试
arr = [];
arr[10] = 1;
arr[20] = 2;
Object.keys(arr) //["10", "20"];
arr.map(function (val, index) {
return index;
}); // [undefined × 10, 10, undefined × 9, 20]
可以看到显示的是undefined x 10
,这种显示代表的是数组中未初始化的数量,而并非说是10个undefined
的值。
也就是说下面式子并不等价,下文会提到前者是稀疏数组,后者是密集数组。
Array(5) // [undefined × 5]
[undefined, undefined, undefined, undefined, undefined]
也就是说利用Array()
构造函数创建的数组其实是下面这样的,两者之间是等价的。
Array(5)
[,,,,,]
问题很明显了,是因为Object.keys()
与map
跳过了数组中空的部分,换句话说,也就是数组中没有初始化的部分均会被跳过。
发现问题后就很好解决了,只需要利用es6中的fill
对数组初始化就可以了。
Object.keys(Array(100).fill(0))
Array(100).fill(0).map(function (val, index) {
return index;
})
拓展
问题解决后,发现一个问题,以上答案除了递归外均用到了ES6的方法,那么在ES5下怎么解决?查阅相关资料发现了两个词汇稀疏数组
与密集数组
。简要概括大概意思就是:
- 稀疏数组:不连续的数组
- 密集数组:连续的数组
sparse = [] //稀疏sparse[1] = 1sparse[10] = 10dense = [1, 2, 3, 4, 5] //密集
创建密集数组的关键在于我们要对数组中的每个index
对应的value
进行赋值
比如这样
sparse = [] //稀疏
sparse[1] = 1
sparse[10] = 10
dense = [1, 2, 3, 4, 5] //密集
这样创建的数组值为''
,而非空,比较推荐这种。
还可以使用apply
,比如创建长度为5的密集数组Array.apply(null, Array(5))
实际等价为Array(undefined,undefined,undefined,undefined,undefined)
。
如果考虑ES6的话,我们还可以这样创建密集数组
Array.from({length:5})
Array(5).fill(undefined)
虽然数组的值是undefined
,但是却是密集数组,换句话来说就是稀疏与密集数组与数组的值没有任何关系,在于数组中的每个值是否被初始化。
sparse = []
sparse[1] = 1
sparse[10] = 10
for (let index in sparse) {
console.log('sparse:' + 'index=' + index + ' value=' + sparse[index])
}
dense = Array.apply(null, Array(5))
for (let index in dense) {
console.log('dense:' + 'index=' + index + ' value=' + dense[index])
}
sparse[0] === dense[0]
结果为
sparse:index=1 value=1
sparse:index=10 value=10
dense:index=0 value=undefined
dense:index=1 value=undefined
dense:index=2 value=undefined
dense:index=3 value=undefined
dense:index=4 value=undefined
true
可以看到dense
中的value
为undefined
,但是并没有被for...in
忽略,并且sparse[0] === dense[0]
的结果为true
说明两者之间的undefined
并没有什么区别,唯一的区别是sparse
的undefined
是代表空(未初始化),dense
的undefined
是我们赋值的(已初始化)。
换句话来说,虽然我们赋值是undefined
,但是由于我们进行了这步赋值操作,js就认为数组已经初始化了,从而不会被for...in
跳过。
由此可知js中数组相关的方法对于空位(稀疏数组和密集数组)的处理方式是不同,这里在阮老师的ES6中关于数组的空位中找到了分析。
ES5对空位的处理,已经很不一致了,大多数情况下会忽略空位。
- forEach(), filter(), every() 和some()都会跳过空位。
- map()会跳过空位,但会保留这个值
- join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。
ES6则是明确将空位转为undefined
- Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。
- 扩展运算符(…)也会将空位转为undefined。
- copyWithin()会连空位一起拷贝。
- fill()会将空位视为正常的数组位置。
- for…of循环也会遍历空位。
- entries()、keys()、values()、find()和findIndex()会将空位处理成undefined
总之由于对数组的空位的处理规则非常不统一,所以建议避免出现空位。
实际上,typeof Array()
我们可以发现结果是object
,js中的数组就是一个特殊的对象,换句话来说,js中的数组不是传统意义上的数组。
arr = []
arr.length // 0
arr[0] = 1
arr.length // 1
arr[100] = 100
arr.length // 101
arr[100] === arr['100'] // true
arr['a'] = 1
arr.length // 101
通过上述代码我们可以发现,js的数组中的数字索引其实是字符串形式的数字,同时当我们为数组赋值的时候,如果数值索引的index
超过数组原有的长度,数组的长度会自动扩充为index + 1
,同时中间的空隙会自动填充undefined,此时数组会变为稀疏数组。
当index
不为数值的时候,数值依然会被填充进去,但此时length不会发现变化。
数组插值会引起length变化需要满足两个条件
- isNaN(parseInt(index,10)) === false
- index >= length
继续思考,当数组的长度改变的时候,数组中的值是什么情况呢?
arr = [1, 2, 3, 4, 5, 6, 7]
arr['test'] = 1
console.log(arr) // [1, 2, 3, 4, 5, 6, 7, test: 1]
arr.length = 1
console.log(arr) // [1, test: 1]
可以看到,如果改变后的length
如果小于原来的length
,凡是满足index
(包括字符串形式的数字,在数组索引中两者等价)大于等于length
的全部会被删除,即凡是同时满足下列2个条件的都会被删除
- isNaN(parseInt(index,10)) === false
- index >= length
总结
面试题不使用loop循环,创建一个长度为100的数组,并且每个元素的值等于它的下标
答案
Array.from(Array(100).keys())
[...Array(100).keys()]
Object.keys(Array(100).join().split(','))
Object.keys(Array(100).fill(undefined))
Object.keys(Array.apply(null,{length:100}))
Array.prototype.recursion = function(length) {
if (this.length === length) {
return this;
}
this.push(this.length);
this.recursion(length);
}
arr = []
arr.recursion(100)
Array(100).fill(0).map(function (val, index) {
return index;
})
后来仔细想了想,答案中依然存在部分问题,使用了Object.keys()
的结果数组中的值为字符串形式的数字,map
在MDN上Array.prototype.map()的polyfill中的代码来看也是使用了循环。但是出题人的意思应该是指不使用for...in
、for...of
、for
、while
循环吧。
总之我认为最稳妥的答案是以下几个
Array.from(Array(100).keys())
[...Array(100).keys()]
Array.prototype.recursion = function(length) {
if (this.length === length) {
return this;
}
this.push(this.length);
this.recursion(length);
}
arr = []
arr.recursion(100)