JavaScript数组在物理内存中是如何表示的?

69

据我所知,JavaScript数组可以存储混合数据类型,并且可以将任何元素更改为其他类型。解释器如何跟踪每个元素在物理内存中的位置?如果我将一个元素更改为较大的数据类型,如何防止覆盖下一个元素的数据。

我假设数组只存储实际对象的引用,并且当放置在数组中时,基本类型会在后台进行包装。

假设情况是这样的,如果我有一个与基本变量不同的句柄,并更改存储在数组中的值,同步性是否得到维护?

我知道我可能已经回答了自己的问题,但我不确定,也找不到任何相关的信息。


13
JavaScript 的不同实现可能有不同的方法来处理这个问题。 - Brad
你不能修改基本类型。分配一个数组元素永远不会改变其他变量或数组。(如果该数组是另一个数组(或多个其他数组)的元素,则更改将通过其他数组可见,但你永远不会看到 x=3,在某个地方分配给一个数组,然后突然看到 x==4)。 - user2357112
这是您所指的内容吗?http://jsfiddle.net/2xBdx/1 - charlietfl
JS中的数组与其他语言中的数组不同。它们没有那些来自于具有连续内存段和一个简单乘法操作以获取所需内存地址的优化。在JS中,数组实际上是对象-哈希表,如果特定的JS实现具有优化,则取决于其种类。最坏的情况是索引只是哈希键,因此位于索引1处的第二个数组项使用对象中的哈希键“1”进行定位。优点:稀疏数组(非常少的实际值)。缺点:大多数(小)数组(除非在内部进行了优化)。 - Mörre
@Brad的简单评论应该成为这个问题的被采纳答案。 - Engineer
4个回答

85

通常,数组会分配一个固定长度的连续内存块。然而,在Javascript中,数组是带有特殊构造函数和访问器方法的对象类型。

这意味着,像下面这样的语句:

var arr = new Array(100000);

实际上不会分配任何内存!事实上,它只是设置了数组中长度属性的值。当你构造一个数组时,你不需要声明一个大小,因为它们会自动增长。所以,你应该使用这个:

var arr = [];

Javascript中的数组是稀疏的,这意味着数组中并不是所有元素都包含数据。换句话说,只有实际包含数据的元素存在于数组中,这减少了数组使用的内存量。这些值由键而非偏移量定位。它们只是一种方便的方法,不适用于复杂的数值分析。

Javascript中的数组没有类型限制,因此一个元素的值可以是对象、字符串、数字、布尔值、函数或数组。数组和对象之间的主要区别在于长度属性,该属性的值大于数组中最大的整数键。

例如:

您可以创建一个空数组,并在索引0和索引99处添加两个元素。这个数组的长度将是100,但其中的数组元素数量为2。

var arr = [];
arr[0] = 0;
arr[99] = {name: "John"};
console.log(arr.length); // prints 100
arr; // prints something like [0, undefined × 98, Object { name: "John"}]

直接回答您的问题:

Q:我了解到JavaScript数组可以存储混合数据,也可以将数组中的任何元素更改为其他类型。解释器如何跟踪每个元素在物理内存中的位置?如果我将一个元素更改为较大的数据类型,如何防止覆盖下一个元素中的数据?

A:如果您已经阅读了我上面的评论,那么您可能已经知道了。在JavaScript中,数组是Hashtable对象类型,因此解释器不需要跟踪物理内存,更改元素的值也不会影响其他元素,因为它们不是存储在连续的内存块中。

--

Q:我假设数组仅存储对实际对象的引用,并且当将原始值放入数组时,在后台进行了包装。如果这是情况,如果我有一个原始变量的不同句柄,并更改存储在数组中的值,是否保持同步性?

A:不,原始值没有被包装。将分配给数组的原始值更改不会更改数组中的值,因为它们是按值存储的。另一方面,对象是按引用存储的,所以更改对象的值将反映在该数组中。

这里有一个您可以尝试的示例:

var arr = [];
var obj = { name: "John" };
var isBool = true;

arr.push(obj);
arr[1] = isBool;

console.log(arr[0]); // print obj.name
console.log(arr[1]); // print true

obj.age = 40;        // add age to obj
isBool = false;      // change value for isBool

console.log(arr[0]); // value here will contain age
console.log(arr[1]); // value here will still be true

还需要注意的是,当你以以下两种方式初始化数组时,它们的行为是不同的:

var arr = new Array(100);
console.log(arr.length);        // prints 100
console.log(arr);               // prints []

var arr2 = new Array(100, 200);
console.log(arr2.length);       // prints 2
console.log(arr2);              // prints [100, 200]
如果您想将Javascript数组用作连续的内存块,可以考虑使用TypedArray。TypedArray允许您将一块内存分配为字节数组,并更高效地访问原始二进制数据。
通过阅读ECMA-262规范(版本5.1),您可以了解更多有关Javascript的复杂性。

25
很好的回答,但我认为值得注意的是,在这里有很多关于实现定义的手脚。例如,在SpiderMonkey(早期Mozilla JS实现的骨干)中,数组被实现为jsval的稠密C数组,直到你对它们进行某些操作时,引擎才能在你下面将它们切换为对象。 - TML
你说得对。谢谢你提出来。我正想着那个问题,想要编辑答案,但可能要等到明天早上了。 - Nick
这个 jsperf 似乎与在 JS 中预分配数组不可能/有益的想法相矛盾:https://jsperf.com/array-allocation-push-vs-new-array-size-assignment - Ryan Biwer
这是否意味着JavaScript中的数组没有缓存局部性? - gaurav5430
太好了。如果有人知道如何将TypedArray作为V8的外部内存来使用,请告诉我。 - Jonathan

11

以下是一些值得思考的内容。我制作了一个jsperf测试简单数组优化,其中一些JavaScript引擎实现了这种优化。

测试案例创建两个包含一百万个元素的数组。 a 数组仅包含数字;而 b 数组包含相同的数字,除了第一个元素是一个对象:

var a = [ 0 ], b = [ { valueOf: function() { return 0; } } ];
for( var i = 1;  i < 1000000;  ++i ) {
    a[i] = b[i] = i;
}

在数组b的第一个元素中的对象的valueOf属性返回0,因此计算与第一个数组相同。

然后,这两个测试只是对两个数组的所有值求和。

快速数组:

var x = 0;
for( var i = 0;  i < 1000000;  ++i ) {
    x += a[i];
}

缓慢的数组:

var x = 0;
for( var i = 0;  i < 1000000;  ++i ) {
    x += b[i];
}

从jsperf的测试结果可以看出,在Chrome中,数值数组大约快了5倍,在Firefox中,数值数组大约快了10倍,在IE中快了大约2倍。

这并没有直接揭示数组使用的内部结构,但它非常清楚地表明两者之间存在相当大的差异。


你在开玩笑吗?慢的数组要添加1000000次带有数字的对象!这就是区别所在!你应该在jsperf循环中设置var i = 1。 - yangguang1029
2
@yangguang1029 不,它不会。它只在第一次加法中将对象转换为数字。 - Jonas Wilms

6

被接受的答案说

[数组构造]不会分配任何内存!

这并不一定是真的。将数组存储为无序键值对(如对象)非常低效,因为每次查找都需要搜索。即使条目已排序,它仍会分配比所需更多的内存。因此,许多引擎以几种不同的方式表示数组,以节省内存并优化性能。这也可以在Michael Geary的示例中看到,仅包含数字的数组得到了优化( V8的详细信息)。有时,引擎可能会决定为新创建的数组分配一些空插槽,如果底层表示具有固定大小且难以轻松缩放(例如,数组列表)。


0
当将一个值分配给数组范围之外的正索引时,数组会将该值存储在指定的索引处,并且在它之前的所有其他索引都为空。数组的长度也会更改为“index + 1”。
当使用负值时,数组将元素存储为键值对,其中负索引是键,要插入的元素是值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接