JavaScript数组索引是字符串还是整数?

20

我有一个关于JavaScript数组的普遍问题。在JavaScript中,数组索引是作为字符串内部处理的吗?

我曾经在某处读到,因为JavaScript中的数组是对象,所以索引实际上是一个字符串。我对此有点困惑,希望得到任何解释。

5个回答

20

严格地说,所有的属性名都是字符串。这意味着类似于数组的数字属性名与任何其他属性名并没有什么不同。

如果您检查规范中相关部分的第6步,您将发现属性访问器表达式在查找属性之前总是被强制转换为字符串。无论对象是数组实例还是其他类型的对象,这个过程(形式上)都会被遵循。(再次强调,这只是看起来是这样。)

现在,JavaScript运行时在内部实现数组功能的方式是自由的。

编辑 — 我有一个想法,可以玩一下Number.toString以证明发生数字到字符串的转换,但事实证明规范明确描述了该特定类型的转换是通过内部过程进行的,并非通过隐式强制类型转换后调用.toString()(出于性能原因,这可能是一个好事情)。


在这种情况下,整数索引应该被视为字符串处理,因为它是数组的属性,而数组是JS对象的一种特殊类型。 - user3033194
2
@user3033194 对的 - 通过 [ ] 运算符用作属性引用的数字值会被转换为字符串,或者至少规范要求进行转换步骤。你给了我一个想法,所以我会扩展答案。 - Pointy
@Pointy 非常感谢!! - user3033194
1
@GitaarLAB没错——如果你仔细想想,JavaScript中数组的唯一特殊之处就是.length属性发生的有些神奇的事情。 - Pointy
2
@GitaarLAB:现在,在执行 arr[4294967294] = 42; 后,arr.length 正确显示为 4294967295。但是,调用 arr.push(21); 会抛出一个 RangeError: Invalid array length 错误。arr[arr.length] = 21 可以工作,但不会改变 length - Felix Kling
显示剩余11条评论

6

那么正确的做法如下:

> var a = ['a','b','c']
undefined
> a
[ 'a', 'b', 'c' ]
> a[0]
'a'
> a['0']
'a'
> a['4'] = 'e'
'e'
> a[3] = 'd'
'd'
> a
[ 'a', 'b', 'c', 'd', 'e' ]

9
for (var i in a) console.log(typeof i) 显示所有索引的类型都是 'string'。 - RobG
3
是的,但 [ 'a', 'b', 'c' ].map((_, i) => typeof i) 返回 [ 'number', 'number', 'number' ] - dbkaplun
@dbkaplun Array.prototype.map必须将自己的参数返回给回调函数。根据MDN:for...in语句遍历由字符串键入的对象的所有可枚举属性(忽略由符号键入的属性),包括继承的可枚举属性。 - Advait Junnarkar

6

是的,从技术上讲,数组索引是字符串,但正如Flanagan在他的“Definitive guide”中所说:

清楚地区分数组索引和对象属性名是有帮助的。所有索引都是属性名,但只有介于0和232-1之间的整数属性名才是索引。

通常情况下,您不应关心浏览器(或更一般的“脚本主机”)在内部执行什么操作,只要结果符合可预测且(通常/希望)指定的结果即可。实际上,在JavaScript(或ECMAScript 262)的情况下,只描述了需要哪些概念步骤。这(故意)为脚本主机(和浏览器)提供了创造聪明、更小、更快速的方法来实现指定行为的空间。

事实上,现代浏览器在内部使用多种不同的算法来处理不同类型的数组:它们包含什么、大小如何、是否有序、是否可以在(JIT)编译时进行优化,以及它们是否稀疏或密集(是的,经常使用new Array(length_val)而不是[])。

在学习JavaScript时,了解到数组只是一种特殊的对象可能有所帮助。但是它们并不总是像人们期望的那样,比如:
var a=[];
a['4294967295']="I'm not the only one..";
a['4294967296']="Yes you are..";
alert(a);  // === I'm not the only one..

尽管对于不了解的程序员来说,拥有一个带索引的数组并将属性附加到数组对象非常容易且相当透明。
我认为最好的答案来自规范(15.4)本身:

数组对象

数组对象对某些属性名给予特殊处理。当一个属性名P(以字符串形式表示)满足ToString(ToUint32(P))等于P且ToUint32(P)不等于2的32次方减1时,它就是一个数组索引。其属性也称为元素。每个数组对象都有一个长度属性,其值始终为小于2的32次方的非负整数。长度属性的值大于每个属性名为数组索引的属性的名称;无论何时创建或更改数组对象的属性,其他属性都会根据需要进行调整以保持此不变量。具体而言,每当添加一个名称为数组索引的属性时,如果必要,就会更改长度属性,使其比该数组索引的数值多一;每当更改长度属性时,每个名称为数组索引且其值不小于新长度的属性都会自动删除。这个限制仅适用于数组对象的自有属性,并且不受从其原型继承的长度或数组索引属性的影响。

如果以下算法返回true,则称对象O为稀疏对象:

  1. 使用参数“length”调用O的[[Get]]内部方法,将结果赋给len。

  2. 对于范围在0≤i<ToUint32(len)中的每个整数i

    a. 使用参数ToString(i)调用O的[[GetOwnProperty]]内部方法,将结果赋给elem。

    b. 如果elem未定义,则返回true。

  3. 返回false。

有效地,ECMAScript 262规范确保了JavaScript程序员无论是获取/设置arr['42']还是arr[42],都能获得明确的数组引用,最高可达32位无符号整数。
主要区别在于自动更新array.length、array.push和其他数组语法糖,如array.concat等。虽然JavaScript也允许人们循环遍历已设置为对象的属性,但我们无法读取我们已经设置了多少个属性(除非进行循环)。而且,据我所知,现代浏览器(特别是Chrome)对真正的小整数数组(预初始化)非常快速。
另请参见例如this相关问题。
编辑:根据@Felix Kling的测试(来自他上面的评论):
在执行arr[4294967294] = 42;之后,arr.length正确显示为4294967295。但是调用arr.push(21)会抛出一个RangeError: Invalid array length异常。使用arr[arr.length] = 21可以正常工作,但不会改变数组长度。
这种(可预测且有意的)行为的解释应该在此答案之后清晰明了。
编辑2:
现在,有人发表了以下评论:

for (var i in a) console.log(typeof i) shows 'string' for all indexes.

由于for in是JavaScript中的(无序必须补充)属性迭代器,很明显它返回一个字符串(如果它没有这样做,我会非常困惑)。
来自MDN的说明:

for..in循环不应用于需要保持索引顺序的数组。

数组索引只是具有整数名称的可枚举属性,与普通对象属性完全相同。没有保证for...in将以任何特定顺序返回索引,并且它将返回所有可枚举属性,包括那些具有非整数名称和那些被继承的属性。

由于迭代顺序取决于实现方式,因此迭代数组可能无法按一致的顺序访问元素。因此,在迭代访问顺序重要的数组时,最好使用带有数字索引的for循环(或Array.forEach或for...of循环)。

那么我们学到了什么?如果顺序对我们很重要(通常在数组中是如此),那么我们就需要JavaScript中这种奇怪的数组,并且拥有“长度”对于按数字顺序循环而言相当有用。

现在想想替代方案:给你的对象一个id/顺序,但然后你又需要为每个下一个id/顺序(属性)再次循环遍历你的对象...

编辑3:

有人给出了以下回答:

var a = ['a','b','c'];
a['4'] = 'e';
a[3] = 'd';
alert(a); // returns a,b,c,d,e

现在使用我回答中的解释:发生的事情是 '4' 可以强制转换为整数 4,并且在范围 [0, 4294967295] 内,因此它变成一个有效的数组 index,也称为 element。由于变量 a 是一个数组([]),所以数组元素 4 被添加为数组元素,而不是属性(如果变量 a 是一个对象({}),那么就会发生这种情况)。
进一步说明数组和对象之间的区别的示例:
var a = ['a','b','c'];
a['prop']='d';
alert(a);

看看它如何返回a,b,c,没有看到'd'。

编辑4:

你评论说:"在这种情况下,整数索引应该被处理为字符串,因为它是数组的属性,而数组是JavaScript对象的一种特殊类型。" 从术语上讲,这是错误的,因为:(表示)整数索引(介于[0,4294967295]之间)创建数组索引元素;而不是属性

最好这样说:实际整数和表示整数的字符串(都介于[0,4294967295]之间)都是有效的数组索引(并且在概念上应该被视为整数),并且创建/更改数组元素(只有当您执行arr.join()arr.concat()时才会返回“东西”/值)。

除此之外,所有的操作都会创建/改变一个属性(并且在概念上应该被视为字符串)。 浏览器实际上所做的事情通常不应该引起您的兴趣,需要注意的是,编写更简单、更清晰的代码可以让浏览器更好地识别:“哦,让我们在底层优化为一个真正的数组”。


不,我并不是唯一这么说的人:来自Axel Rauschmayer博士的博客(http://www.2ality.com/2012/12/arrays.html):“JavaScript中的数组索引实际上是字符串。自然地,引擎在内部执行优化,使其不成立。但这是规范定义它们的方式”,“假装数组索引是数字。这通常是在内部发生的,并且ECMAScript正在向这个方向发展。”有效地,ECMAScript 262规范只确保用户获得/设置'9'或9时具有明确的数组引用,最高可达32位无符号数。 - GitaarLAB

3

让我们来看看:

[1]["0"] === 1 // true

哦,但这并不是确定的,因为运行时可能会将"0"强制转换为+"0"+"0" === 0

[1][false] === undefined // true

现在,+false === 0,因此运行时不会将该值强制转换为数字。
var arr = [];
arr.false = "foobar";
arr[false] === "foobar" // true

实际上,运行时将该值强制转换为字符串。是的,这是一个哈希表查找(外部)。


这对我来说是全新的。我曾经认为JS数组索引就像其他语言中的数组索引。 - user3033194
请记住,内部运行时很可能将数组表示为传统数组以提高性能。但对于用户来说,数组只是一个对象。 - Witiko
[...] 就像传统数组一样,但您可能会遇到大量空缺或非常大的索引,因此在所有情况下,使用映射可能更好(索引还可以是负小数,如"-3.3")。但是,通过这些测试,Firefox似乎不会将除整数键元素之外的元素显示为数组的一部分。有趣... - Alexis Wilke

2

在JavaScript中,有两种类型的数组:标准数组和关联数组(或具有属性的对象)

  • [ ] - 标准数组 - 仅限基于0的整数索引
  • { } - 关联数组 - JavaScript对象,其中键可以是任何字符串

因此...

var arr = [ 0, 1, 2, 3 ];

标准数组是指只能使用整数作为索引的数组。当您使用 arr["something"] 时,由于 something(即用作索引的内容)不是整数,因此您实际上是在将属性定义给 arr 对象(在 JavaScript 中,所有内容都是对象)。但您没有向标准数组添加元素。


5
JavaScript对象在很多方面都像“关联数组”,但它们并不完全相同,规范从未使用过这个术语。 - Pointy
1
我刚刚调整了对那个术语的使用。 - rfornal
3
将数组描述为对象类型可能更准确,而不是反过来。 - RobG

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