JavaScript数组中的负数索引是否应计入数组长度?

95

在 JavaScript 中,我可以这样定义一个数组:

var arr = [1,2,3];

我也可以做

arr[-1] = 4;

现在如果我这样做

arr = undefined;

我还失去了对arr[-1]的值的引用。

所以在我的逻辑中,arr[-1]似乎也是arr的一部分。

但是当我执行以下操作时(没有将arr设置为未定义):

arr.length;

它返回 3 而不是 4

所以我的观点是如果数组可以使用负索引,这些负索引也应该是它们的长度的一部分**。我不知道我是否错了或者是否漏掉了关于数组的某些概念。


15
你还可以写 arr[1.5] = 1,这也不会影响数组的长度。语言规范已经非常明确地阐述了什么会影响数组的长度。可能你不喜欢它,但你必须接受它。要么就设计自己的竞争性语言,并说服人们转用它。 - Raymond Chen
2
@DigvijayYadav:在JavaScript中,可以将它视为一个缺陷或特性,就像许多其他事物一样。这是因为数组的行为有点像对象,你也可以使用字符串var a = []; a ['foo'] ='baz',但这并不意味着你应该这样做;显然违反了所有约定。 - elclanrs
3
朋友们,这里的主要观点是数组是对象,它们没有区别。这就是这种行为的原因,即使你不喜欢也完全是故意的。等你学到所有数字都用浮点数表示,即使是整数,再回来看JavaScript,你会觉得这门语言很有趣... - Tony R
2
您可以在规范中找到定义数组元素设置算法的详细信息:http://es5.github.com/#x15.4.5.1(特别是第4步),并且“数组索引”在此处定义:http://es5.github.com/#x15.4。 - Felix Kling
我猜你想这样做是因为你想用Python作类比。 - Ali Saberi
显示剩余6条评论
8个回答

123

对我来说,逻辑上看起来arr[-1]也是arr的一部分。

是的,但不是你想象中的那样。

你可以为一个数组分配任意属性(就像JavaScript中的任何其他对象一样),这就是当你在-1处“索引”数组并赋值时所做的操作。由于这不是数组的成员,而只是任意属性,因此你不应该期望length考虑该属性。

换句话说,以下代码执行相同的操作:

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

​arr.cookies = 4;

alert(arr.length) // 3;

40
好的,这句话的意思是负数索引实际上并不像正数索引那样起作用。 - me_digvijay
4
没问题,它们只是数组上的任意属性。 - Andrew Whitaker
7
那就不要使用数组,使用一个普通的对象。但这样会失去内置的“length”属性。 - Andrew Whitaker
2
@Digvijay:控制台仅显示具有正整数名称的属性,因为(我猜)它只是从0到.length进行普通的for循环。使用console.dir(arr)查看完整的对象。此外,使用方括号访问数组不是数组的特征,而是对象的固有特征(var obj = {foo:'bar'}; obj.fooobj['foo'])。 - Felix Kling
@DigvijayYadav:那只是控制台的表示,它不会显示数组对象上的非数字属性。它仍然存在。 - Bergi
显示剩余5条评论

27

length属性将返回一个数字,该数字比已分配的“索引”中最高的一个大1,其中数组“索引”是大于或等于零的整数。请注意,JS允许“稀疏”数组:

var someArray = [];
someArray[10] = "whatever";
console.log(someArray.length); // "11"

当然,如果没有元素,则length0。请注意,如果使用delete删除最高元素,则length不会更新。

但是数组是对象,因此您可以分配具有其他任意属性名称(包括负数或分数)的属性:

someArray[-1] = "A property";
someArray[3.1415] = "Vaguely Pi";
someArray["test"] = "Whatever";
请注意,即使您提供一个数字,如-1,JS在幕后也会将属性名称转换为字符串。(正整数索引也是一样的。)
数组方法(如.pop().slice()等)仅适用于零或更高的整数"索引",而不适用于其他属性,因此length 在这一点上保持一致。

谢谢你的回答,帮了我很多。我也尝试了使用负索引的splice方法,我发现如果我使用-1,它会到达数组的最后一个元素,对于-2则到达倒数第二个元素,以此类推。 - me_digvijay
+1. 长度并不告诉你有多少元素或属性。它只是最高索引加1。例如,给定 var a = new Array(9)a 的长度为9,但没有任何元素。 - RobG
1
@DigvijayYadav - 是的,Array.slice()方法应该可以做到。String.slice()方法也是同样的道理。 - nnnnnn

11
请注意,当您使用位置(或0)索引时,值将放置在数组中:
var array = [];

array[0] = "Foo";
array[1] = "Bar";

// Result: ["Foo", "Bar"]
// Length: 2

当您添加非索引值(而不是0-9+)时,情况并非如此:

var array = [];

array[0]  = "Foo";
array[1]  = "Bar";
array[-1] = "Fizzbuzz"; // Not a proper array index - kill it

// Result: ["Foo", "Bar"]
// Length: 2

只有当您遵守规则时,才会将值放入数组中。如果不遵守规则,则不会被接受。但是,它们仍然可以被放在数组对象本身中,这也是JavaScript中的几乎所有内容的情况。即使 ["Foo", "Bar"] 是我们数组中唯一的值,我们仍然可以访问 "Fizzbuzz"

array[-1]; // "Fizzbuzz"

但请注意,这不是数组值的一部分,因为它的“索引”无效。相反,它被添加到数组中作为另一个成员。我们可以以同样的方式访问其他数组成员:

array["pop"]; // function pop() { [native code] }

请注意,我们正在访问数组上的pop方法,这告诉我们它包含本地代码。我们没有使用键为"pop"的任何数组值,而是在数组对象本身上访问成员。我们可以通过循环遍历对象的公共成员进一步确认这一点:
for (var prop in array) 
    console.log(prop, array[prop]);

它会输出以下内容:

 0 Foo
 1 Bar
-1 Fizzbuzz

所以,再次强调,它存在于“对象”中,但不存在于“数组”中。

很棒的问题!确实让我做了两次思考。


1
即使规范说明具有正数数字属性名称的属性被称为数组的元素(任何其他属性都不是):http://es5.github.com/#x15.4。 - Felix Kling
结果不是 ["Foo", "Bar"] 而是 ["Foo", "Bar", -1: "Fizzbuzz"],请查看 此处的代码。就像你所写的那样,它变成了一个键为-1的属性,但它在输出中肯定是可见的。否则,您的答案很好而且完整。如果您进行更改,我会再次点赞,我的反对票太过分了,但无法取消;时间已经过期。 - Wilt

6
如果您真的希望负索引和其他索引包含在长度中,那么请自己创建此功能:
function getExtendedArray() {
    var a = [];

    Object.defineProperty(a, 'totalLength', { 
        get : function() { return Object.keys(a).length; }
    });

    return a;
}

然后举个例子:
var myArray = getExtendedArray();
console.log(myArray.totalLength); // 0
myArray.push("asdf");
myArray[-2] = "some string";
myArray[1.7777] = "other string";
console.log(myArray.totalLength); // 3

如果您希望长度仅包括编号索引/属性,可以为键添加if语句 - if(Number(key) != NaN) length++; 如果您想要过滤掉浮点数,请将&& key % 1 == 0添加到条件中。 - Bonnev

4
JavaScript中的数组实际上是对象。它们只是从Array构造函数原型化而来。
数组索引实际上是哈希映射中的键,所有键都被转换为字符串。您可以创建任何键(例如“-1”),但Array上的方法被设计成像数组一样工作。因此,length不是对象的大小,而只保证大于最大的整数索引。同样,打印arr将仅列出具有大于等于0的整数键的值。
更多信息请参见这里

或者我们可以开始思考为什么array ['foo']不是有效的索引 - JavaScript允许您这样做,但这并不意味着数组(及其元素)的定义会从一种语言更改为另一种语言。按预期工作。 - tw airball
事实上,在JS中并不存在“数组”。但是,“关联数组”,也被称为“对象”,是JS的核心。因此,制作一个模仿常规数组的Array对象并不是一件很困难的事情。 - Tony R
1
数组和普通对象之间有一个非常特别的区别:自动调整长度属性。 - RobG

4

Array 只是 JS 中的一种特殊类型的 Object。有一种特殊的语法,这很好,因为它使我们能够有效地使用它们。想象一下,如果按照那种方式编写数组文本会多么乏味:

const array = {0: 'foo', 1: 'bar', 2: 'baz'} //etc...

但是它们仍然是对象。因此,如果您执行以下操作:
const myArray = [42, 'foo', 'bar', 'baz'];

myArray[-1] = 'I am not here';
myArray['whatever'] = 'Hello World';

那些非整数属性将附加到一个对象上(数组就是这样),但它们不能被一些原生方法访问,例如Array.length
由于specs,原生的length方法作为getter(exampleArray.length)将检查所有“小于2^32的非负整数”键,获取最大值并返回其数值+1。
作为setter(exampleArray.length = 42),如果给定的长度大于实际的非负整数键数量,则它将为“缺失”的索引创建一些空插槽,或删除所有不大于给定长度的非负整数键(及其值)。
伪代码实现:

const myArray = {
  '-1': 'I am not here', // I have to use apostrophes to avoid syntax error
  0: 42,
  1: 'foo',
  2: 'bar',
  3: 'baz',
  whatever: 'Hello World',

  get myLength() {
    let highestIndex = -1;
    for (let key in this) {
      let toInt = parseInt(key, 10);
      if (!Number.isNaN(toInt) && toInt > -1) {
        // If you're not an integer, that's not your business! Back off!
        if (toInt > highestIndex) highestIndex = toInt;
      }
    }
    return highestIndex + 1;
  },
  set myLength(newLength) {
    /* This setter would either:
      1) create some empty slots for 'missing' indices if the given length is greater than the actual number of non-negative-integer keys
      2) delete all non-negative-integer keys (and their values) which are not greater then the given length.
    */
  }
}

console.log(myArray.myLength); // 4

myArray[9] = 'foobar';

console.log(myArray.myLength); // 10


1
根据MDN:
长度属性的值是带有正号的整数,其值小于2的32次方(232)。
在Javascript中,您可以在创建的任何对象上设置属性。
var array = new Array();
array = [1,2,3];
array["boom"] = "pow";

同样地,当您设置负索引时,它会将其存储为数组的属性而不是索引的一部分。
array[-1] = "property does not add to array length";

这就是为什么长度不反映它,但 for..in 循环显示它的原因。

-1
当您使用非正数或非数字索引时,数组的行为类似于具有键值对的关联数组。
要遍历数组,可以使用
for(var index in myArray) {
  document.write( index + " : " + myArray[index] + "<br />");
}

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