Appearance
10. 二维数组初始化问题
1. 问题阐述
在 JS 中声明并初始化一个二维数组:
js
const arr = [
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]
]
1
2
3
4
5
2
3
4
5
更多时候我们会使用数组的 fill
方法:
js
const arr = Array(3).fill(Array(3).fill(0))
// 生成的结果:
[[0, 0, 0],
[0, 0, 0],
[0, 0, 0]]
1
2
3
4
5
2
3
4
5
但是这里有一个数据引用共享的问题,也就是指,fill 内层的数组被外层的 fill 方法复用了,三个子数组指向同一个引用地址!试着打印以下结果:
js
arr[0] === arr[1] // true
arr[0] === arr[2] // true
arr[0][0] = 1
console.log(arr)
[[1, 0, 0],
[1, 0, 0],
[1, 0, 0]]
1
2
3
4
5
6
7
2
3
4
5
6
7
如果是 fill 里面直接写一个数组,结果也是一样的,子数组的引用地址相同:
js
const a = Array(3).fill([1, 2, 3])
1
解释
导致这个问题的原因是 JS 的函数参数是 按值传递,当使用对象作为参数,则是拷贝了一份对象的应用,将这个引用副本值传入函数。fill 函数基于性能问题,直接将子数组赋值了。因为如果不是直接赋值,则要使用递归方法一一传入数组元素。可以这样说,fill 设计之初就没有考虑过使用在多维数组上。
Array.prototype.fill()
的原理实现:
js
Array.prototype._fill = function (source) {
for (let i = 0; i< this.length; i++) {
// 只做第一层赋值,传递的是 source 的引用地址的拷贝值
this[i] = source
}
return this
}
1
2
3
4
5
6
7
2
3
4
5
6
7
2. 解决方案
那么,如何声明一个安全正确的多维数组?总不能一个一个写。
思路
学过 Vue 的都知道,组件的 data 必须是一个函数返回一个对象,就是为了避免组件间共享数据。从这点上,我们可以想想有没有涉及到函数的数组方法。
JS 中涉及到回调函数的数组方法:Array.prototype.map
、Array.from
等。
map 方法
js
const a = Array(3).fill().map(() => Array(3).fill(0))
console.log(a)
// [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
a[0][0] = 10
console.log(a)
// [ [ 10, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
1
2
3
4
5
6
2
3
4
5
6
Array.from 方法
js
const a = Array.from({ length: 3 }, () => Array(3).fill(0))
console.log(a)
// [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
a[0][0] = 10
console.log(a)
// [ [ 10, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
1
2
3
4
5
6
2
3
4
5
6