Skip to content
On this page

10. 二维数组初始化问题

1. 问题阐述

在 JS 中声明并初始化一个二维数组:

js
const arr = [
  [0, 0, 0],
  [0, 0, 0],
  [0, 0, 0]
]
1
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

但是这里有一个数据引用共享的问题,也就是指,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

如果是 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. 解决方案

那么,如何声明一个安全正确的多维数组?总不能一个一个写。

思路

学过 Vue 的都知道,组件的 data 必须是一个函数返回一个对象,就是为了避免组件间共享数据。从这点上,我们可以想想有没有涉及到函数的数组方法。

JS 中涉及到回调函数的数组方法:Array.prototype.mapArray.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

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