为什么数组允许字符串作为 JavaScript 中的索引?

Ngu*_*ong 2 javascript arrays specifications object

正如我们已经知道的,数组和对象之间的区别之一是:

“如果你想提供特定的键,唯一的选择是一个对象。如果你不关心键,那就是一个数组”(在这里阅读更多)

此外,根据MDN 的文档

数组不能使用字符串作为元素索引(如在关联数组中),但必须使用整数

然而,令我惊讶的是:

> var array = ["hello"]; // Key is numeric index
> array["hi"] = "weird"; // Key is string
content structure looks like:  ["hello", hi: "weird"]
Run Code Online (Sandbox Code Playgroud)

数组的内容结构看起来很奇怪。更重要的是,当我检查它返回的数组类型时true

Array.isArray(array) // true
Run Code Online (Sandbox Code Playgroud)

问题:

  1. 为什么会有这种行为?这似乎不一致,对吧?
  2. 在幕后实际存储的数据结构是什么:作为数组或诸如对象、哈希表、链表之类的东西?
  3. 这种行为是否取决于特定的 JavaScript 引擎(V8spidermonkey等)?
  4. 我应该在普通对象上使用这样的数组(键都是数字索引和字符串)吗?

Mar*_*eed 10

JavaScript 中的数组不是单独的类型,而是 object 的子类(Array实际上是 class 的实例)。该阵列(和列表)语义层次上的行为上面,你在JavaScript中的每个对象自动获得。Javascript 对象实际上是关联数组抽象数据类型的实例,也称为字典、映射、表等,因此可以具有任意命名的属性。

您引用的 MDN 文档的其余部分很重要:

使用括号表示法(或点表示法)通过非整数设置或访问不会从数组列表本身设置或检索元素,但会设置或访问与该数组的对象属性集合关联的变量。数组的对象属性和数组元素列表是分开的,数组的遍历和变异操作不能应用于这些命名属性。

可以肯定的是,您始终可以在数组上设置任意属性,因为它是一个对象。执行此操作时,根据实现的不同,在控制台中显示数组时可能会得到看起来很奇怪的结果。但是,如果您使用标准机制(for...of循环或.forEach方法)遍历数组,则不包括这些属性:

> let array = ["hello"] 
> array["hi"] = "weird"
> for (const item of array) { console.log(item) }
hello
> array.forEach( (item) => console.log(item) )
hello
Run Code Online (Sandbox Code Playgroud)

MDN 声明“数组的对象属性和数组元素列表是分开的”有点夸张;数组的索引元素只是普通对象的属性,其键碰巧是数字。因此,如果您使用..迭代所有属性,则数字属性将与其他属性一起显示。但正如有据可查的那样,..忽略数组的特性,如果您正在寻找类似数组的行为,则不应使用它。这是MDN另一个页面,讨论了这个:forinforinArray

数组迭代和 for...in

注意:for...in 不应用于迭代索引顺序很重要的数组。数组索引只是具有整数名称的可枚举属性,在其他方面与一般对象属性相同。不能保证 for...in 会以任何特定顺序返回索引。for...in 循环语句将返回所有可枚举属性,包括具有非整数名称的属性和继承的属性。

由于迭代顺序取决于实现,因此对数组进行迭代可能不会以一致的顺序访问元素。因此,在迭代访问顺序很重要的数组时,最好使用带有数字索引的 for 循环(或 Array.prototype.forEach() 或 for...of 循环)。

所以回答你的问题:

  1. 为什么会有这种行为?这似乎不一致,对吧?

“为什么”的问题总是很难回答,但从根本上说,这种情况下的答案似乎是你所看到的不是有意的行为。就我们所知,Brandon Eich 和随后的 Javascript 设计者并没有着手制作一种也允许非数字键的数组类型;相反,他们选择不使数组类型在所有。在Javascript中,仅存在五种类型:booleannumberstringundefined,和object(其中包括null)。Array 没有成功——它不是一个单独的类型,而只是一个对象类。事实上,您可以从核心语言中删除数组并完全在 Javascript 本身中实现它们(尽管您会失去[...]文字语法的便利性)。

所以这就是为什么 - Javascript 数组不是它们自己的类型,而只是一个对象类;Javascript 中的所有对象都是关联数组;因此,您可以将数组用作关联数组。

  1. 在幕后实际存储的数据结构是什么:作为数组或诸如对象、哈希表、链表之类的东西?

ECMAScript 规范要求对象作为关联数组运行,但没有规定实现;您可以假设它们是哈希表而不会失去任何一般性。但是由于 Array 类通常是核心语言实现的一部分而不是纯 Javascript 运行时代码,因此我不会惊讶地发现它包含了通用对象属性处理代码之外的特殊优化,以更有效地处理数字索引值. 只要语义相同,就没有关系。

  1. 这种行为是否取决于特定的 Javascript 引擎(V8、SpiderMonkey 等)?

不同的引擎可能会更改 Array 值的显示/序列化方式,尤其是此类字符串表示形式是否包含非数字属性。但是能够存储任意键/值对的潜在行为是语言设计的自然副作用,并且应该在所有符合 ECMAScript 规范的实现中通用。

  1. 我应该在普通对象上使用这样的数组(键都是数字索引和字符串)吗?

好吧,再一次,数组一个普通对象。但是为了让那些阅读您的代码的人获得最大的可读性和最小的惊喜,我不建议使用相同的对象作为常规数组和关联数组。您可以实现一个具有类似数组行为的新​​类,甚至可以从Array原型继承,但通常最好将这两种数据结构类型分开。

  • @NguyễnVănPhong 实际上,这是一个小错误 - 它还应该检查 `key < this.length`。但是是的。是为了说明一个观点。自定义实现中唯一真正缺少的是数组文字语法“[1, 2, 3]”。除此之外,其他一切都是标准的对象行为。如果需要的话,可以很容易地重新创建数组方法。不过,它们不需要 - 所有这些都是故意通用的,因此它们可以应用于类似数组的对象(具有正整数作为键和“length”属性的对象)。 (3认同)
  • “*尽管你会失去 for...of 循环的便利*”并非如此。`for..of` 本质上获取对象的迭代器,因此如果您实现“假数组”以拥有仅提供正整数索引值的迭代器,您将获得与 `for..of 相同的行为` 在普通数组上。因为这基本上就是发生的情况,“for (const item of Something)”将迭代“something[Symbol.iterator]()”,而对于作为“arr.values()”迭代器别名的数组。 (2认同)