接前文:《js 的按值访问和按引用访问》
问题背景:写代码的时候意外地发现了一个小 bug,在对二维数组赋初值的时候遇到了一些问题,仔细思考之后,对 js 按值访问和按引用访问有了更深刻的理解。
如何正确构建一个二维数组?
我想要构建一个二维数组,并在构建完成后对其中的特定元素进行修改,很自然地写出了这样的代码:
|
|
结果当然是毫无疑问地报错了
|
|
提示不能给没有定义的数组元素复制,这很自然,因为按照arr = [[]]
来定义变量,事实上只对arr[0]
进行了定义,你可以给arr[0][1]
赋值,但如果对arr[1][0]
进行赋值的话就会进行报错。而undefined[0]
显然是没有意义的。虽然 js 中的数组是**_不定长的_**(实际上不能这么说,下文会对其进行解释),你可以先声明arr = []
,然后对arr[10]
进行赋值,不会报错,但对于二维数组而言,就不是这么简单了。
接下来我用 ES6 中的Array
对象来新建数组:
|
|
这行代码可以生成一个长度为 10,所有元素都为 0 的数组,我用它来改写一下上面的代码:
|
|
使用 Array 的嵌套,成功生成了一个 10×10,所有元素值都为 0 的二维数组
|
|
接下来我对这个数组进行赋值操作:
|
|
输出的数组却把整列的值都改变了,即产生了arr[*][5] = 1
的效果!
|
|
很反直觉,对吗?但其实有道理可循,之所以会出现这种情况,是因为整个过程实际上只创建了两个
数组,上述代码的效果和下面的代码是一模一样的:
|
|
我们知道,数组本质上也是一个对象,而对象是按引用访问的,也就是说,复制一个对象并不是复制其本身,而是复制它的指针。那么对任意一个对象的修改,都会体现到全局的变化。针对上述代码,对数组"a"
内任意一个元素"b"
的修改,都会导致整个"a"
中每一个"b"
的变化。
那么,解决这个问题的办法就是让数组"a"
中每一个元素都指向不同的新数组,即:
|
|
这样就只改变了arr[5][5]
一个值。
关于 js 中数组不定长的实现
参考文章:
js 作为一门弱类型的动态脚本语言,和 C、C++、Java 等静态编译型语言有着比较大的不同。在后者中,数组在声明时就需要提前确定好了长度。这是由于在此类静态语言中,在编译时就把每一个变量的类型和所占用的内存确定下来了,而 js 中的变量实际上只有在使用时才会确定其类型和占用内存。因此,在静态语言中常常会出现数组越界的情况,而 js 中的数组似乎无穷无尽,可以随意操作。让我们观察下面的代码:
|
|
可以看出,Array
函数的原型链指向的是Object
。事实上,几乎“所有JavaScript
中的对象都是位于原型链顶端的Object
的实例”(Function
和Object
的原型链指向十分令人费解,我也是丈二和尚摸不着头脑一知半解),换句话说,js 中的数组根本就不是传统意义上的数组,而是顶着数组名字的一个对象!
js 中的数组,底层就是一个键值对的Map
,V8 引擎规定:“js 数组在声明的时候如果元素类型不一致时,js 内部就自动将数组装换为慢数组,在数组中空元素的个数大于 1024 时也会自动将数组转换为慢数组”。这里涉及到两个新的概念,快数组
和慢数组
。
其中,快数组
是一种线性的存储方式。新创建的空数组。js 默认的存储方式是快数组,快数组长度是可变的,可以根据元素的增加和删除来动态调整存储空间大小,内部是通过扩容和收缩机制实现(采用 C++实现)。
而慢数组
则是基于HashTable
实现的,以数字为键值的哈希(散列)表,使用的是不连续的内存,没有了内存连续的限制,能够动态的分配内存,就代表着可以更方便的增加元素,删除元素,但是在查询方面的性能要低于快数组。
两者的区别在于,
慢数组
采用以时间换空间,不必申请连续的空间,节省了内存,但需要付出效率变差的代价;而快数组
采用以空间换时间的方式,申请了大块连续内存,提高效率。