Featured image of post 二维数组应该怎么赋初值

二维数组应该怎么赋初值

V8引擎是怎么实现不定长数组的

接前文:《js 的按值访问和按引用访问》

问题背景:写代码的时候意外地发现了一个小 bug,在对二维数组赋初值的时候遇到了一些问题,仔细思考之后,对 js 按值访问和按引用访问有了更深刻的理解。

如何正确构建一个二维数组?

我想要构建一个二维数组,并在构建完成后对其中的特定元素进行修改,很自然地写出了这样的代码:

1
2
3
4
// setArray.js
let arr = [[]];
arr[1][1] = 1;
console.log(arr[1][1]);

结果当然是毫无疑问地报错了

1
2
3
4
% node setArray.js
arr[10][10] = 1;
            ^
TypeError: Cannot set properties of undefined (setting '10')

提示不能给没有定义的数组元素复制,这很自然,因为按照arr = [[]]来定义变量,事实上只对arr[0]进行了定义,你可以给arr[0][1]赋值,但如果对arr[1][0]进行赋值的话就会进行报错。而undefined[0]显然是没有意义的。虽然 js 中的数组是**_不定长的_**(实际上不能这么说,下文会对其进行解释),你可以先声明arr = [],然后对arr[10]进行赋值,不会报错,但对于二维数组而言,就不是这么简单了。

接下来我用 ES6 中的Array对象来新建数组:

1
let arr = new Array(10).fill(0);

这行代码可以生成一个长度为 10,所有元素都为 0 的数组,我用它来改写一下上面的代码:

1
2
let arr = new Array(10).fill(new Array(10).fill(0));
console.log(arr);

使用 Array 的嵌套,成功生成了一个 10×10,所有元素值都为 0 的二维数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

接下来我对这个数组进行赋值操作:

1
arr[5][5] = 1;

输出的数组却把整列的值都改变了,即产生了arr[*][5] = 1的效果!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
]

很反直觉,对吗?但其实有道理可循,之所以会出现这种情况,是因为整个过程实际上只创建了两个数组,上述代码的效果和下面的代码是一模一样的:

1
2
3
4
5
6
let a = [];
b = new Array(10).fill(0);
for (let i = 0; i < 10; i++) a[i] = b;
// Array().fill()实际上就是把fill()内的值复制n次
a[5][5] = 1;
// a = [b, b, b, b, b, b, b, b, b, b]

我们知道,数组本质上也是一个对象,而对象是按引用访问的,也就是说,复制一个对象并不是复制其本身,而是复制它的指针。那么对任意一个对象的修改,都会体现到全局的变化。针对上述代码,对数组"a"内任意一个元素"b"的修改,都会导致整个"a"中每一个"b"的变化。

那么,解决这个问题的办法就是让数组"a"中每一个元素都指向不同的新数组,即:

1
2
3
4
5
6
let arr = [];
for (let i = 0; i < 10; i++) {
  arr[i] = new Array(10).fill(0);
}
arr[5][5] = 1;
console.log(arr);

这样就只改变了arr[5][5]一个值。


关于 js 中数组不定长的实现

参考文章:

《探究 JS V8 引擎下的“数组”底层实现》

《js 数组底层实现》

js 作为一门弱类型的动态脚本语言,和 C、C++、Java 等静态编译型语言有着比较大的不同。在后者中,数组在声明时就需要提前确定好了长度。这是由于在此类静态语言中,在编译时就把每一个变量的类型和所占用的内存确定下来了,而 js 中的变量实际上只有在使用时才会确定其类型和占用内存。因此,在静态语言中常常会出现数组越界的情况,而 js 中的数组似乎无穷无尽,可以随意操作。让我们观察下面的代码:

1
2
console.log(Array.__proto__.__proto__.constructor);
// Object()

可以看出,Array函数的原型链指向的是Object。事实上,几乎“所有JavaScript中的对象都是位于原型链顶端的Object的实例”(FunctionObject的原型链指向十分令人费解,我也是丈二和尚摸不着头脑一知半解),换句话说,js 中的数组根本就不是传统意义上的数组,而是顶着数组名字的一个对象!

js 中的数组,底层就是一个键值对的Map,V8 引擎规定:“js 数组在声明的时候如果元素类型不一致时,js 内部就自动将数组装换为慢数组,在数组中空元素的个数大于 1024 时也会自动将数组转换为慢数组”。这里涉及到两个新的概念,快数组慢数组

其中,快数组是一种线性的存储方式。新创建的空数组。js 默认的存储方式是快数组,快数组长度是可变的,可以根据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现(采用 C++实现)。

慢数组则是基于HashTable实现的,以数字为键值的哈希(散列)表,使用的是不连续的内存,没有了内存连续的限制,能够动态的分配内存,就代表着可以更方便的增加元素,删除元素,但是在查询方面的性能要低于快数组。

两者的区别在于,慢数组采用以时间换空间,不必申请连续的空间,节省了内存,但需要付出效率变差的代价;而快数组采用以空间换时间的方式,申请了大块连续内存,提高效率。

Built with Hugo
主题 StackJimmy 设计