由Array构造函数创建的未定义数组上执行forEach操作

24

我想知道为什么无法对未定义的数组使用forEach方法。

代码:

var arr = new Array(5); // [undefined x 5]

//ES5 forEach
arr.forEach(function(elem, index, array) {
    console.log(index);
});

//underscore each
_.each(arr, function(elem, index, array) {
    console.log(index);
});

这两个示例都没有执行函数。

现在要使用foreach,我必须做:

var arr = [0,0,0,0,0];

然后对它进行forEach操作。

我正在尝试创建一个指定大小的数组并循环遍历它,避免使用for循环。 我认为使用forEach比for循环更清晰。 对于长度为5的数组,这不是问题,但对于更大的数组来说,会很丑陋。

为什么循环遍历未定义值的数组存在问题?


3
因为 Array(5) 不是一个由 undefined 组成的数组。即使它的 .length 已经预定义,它也没有任何成员,数量为 0 - cookie monster
1
虽然我不确定你说的例子“返回undefined”时期望什么。无论它遍历什么,.forEach()方法始终会返回undefined。这个问题似乎应该是回调函数是否被调用的问题。 - cookie monster
1
Array(5) 本质上等同于 var arr = []; arr.length = 5。这意味着更改数组的长度不会为其数值属性设置任何值(数值属性为 undefined 而不是具有未定义的值)。 - soulcheck
@Tukkan:这是因为你明确地将arr的第二个成员设置为undefined值,所以该成员确实存在。现在像这样做var arr = [1, , 2]。你会发现第二个成员被跳过了。这是因为JS数组可以是稀疏的,当forEach()迭代时,它会进行一个in测试来查看成员是否存在,如果不存在,它会跳过该成员而不是调用回调函数。 - cookie monster
2
如果你想要一个快速的解决方案,你可以这样做:var arr = Array.apply(null, Array(5))。这有点长,但不是太糟糕。或者创建一个单独的函数来为你完成它。function A(n) { return Array.apply(null, Array(n)); } var arr = A(5); - cookie monster
显示剩余3条评论
5个回答

31

Array(5) 实质上等同于

var arr = []; 
arr.length = 5;

在JavaScript中,改变数组的长度不会为其数值属性设置任何值,也不会在数组对象中定义这些属性。因此,数值属性未定义,而不是具有未定义的值。您可以使用以下方式进行检查:

Object.keys(arr)

当JavaScript迭代数组时,它会迭代数字属性,因此如果这些属性不存在,则没有可迭代的内容。

您可以通过执行以下操作进行检查:

var arr = Array(5)

//prints nothing
arr.forEach(function(elem, index, array) {
    console.log(index);
});

arr[3] = 'hey'
//prints only 'hey' even though arr.length === 5
arr.forEach(function(elem, index, array) {
    console.log(index);
});

以下代码:

var arr = [undefined, undefined];

创建一个 length === 2 的数组,并将数字属性0和1都设置为未定义。


1
你能否解释一下在这种情况下 spread operator 的行为? - Hitmands

6

浏览一个简化的.forEach()实现或许有所帮助。

Array.prototype.my_for_each = function(callback, thisArg) {
    for (var i = 0; i < this.length; i++) {
        if (i in this) {
            callback.call(thisArg, this[i], i, this);
        }
    }
};

因此,您可以看到方法实际上会遍历整个数组(根据规范),但只有在成员实际存在时才调用回调函数。它通过使用in运算符查找属性(索引)来检查,该运算符测试对象是否具有或继承属性。
如果in显示索引存在,则调用回调函数。

所以,考虑以下这个数组:

var arr = ["foo", "bar", "baz"];

这将输出所有3个项目:
arr.my_for_each(function(item) {
    console.log(item);
});
// "foo" "bar" "baz"

但是如果我们使用delete来删除一个成员,它会在数组中留下一个空洞,导致该空洞被跳过。

delete arr[1];

arr.my_for_each(function(item) {
    console.log(item);
});
// "foo" "baz"

当您使用Array(5)创建数组时,它将创建一个没有任何成员但长度为5的数组。因此,这是一个稀疏数组的示例(在这种情况下非常稀疏)。由于in找不到任何索引,因此回调永远不会被调用。请注意保留HTML标记。

3
你可以使用Array.from创建一个数组,并传递lambda函数,该函数将在数组中的每个项目上被调用。 详细文档

  const arr = Array.from(
      { length: 5 },
      () => 0
  )

  console.log(arr)


2

其他回答已经解释了问题,但没有提供解决方案。

ES6 Spread语法会用undefined填充稀疏值。使用Array.apply(null, sparseArray)也会这样做,它的好处是可以在旧浏览器中使用,但需要更多的工作来理解。

const sparseArray = new Array(5);
const unsparseArray1 = Array.apply(null, sparseArray);
const unsparseArray2 = [...sparseArray];

function arrayApply() {
  // ES5 forEach works with either approach
  unsparseArray1.forEach(function(elem, index, array) {
    console.log(index);
  });
}

function es6Spread() {
  // Lodash each works with either approach
  _.each(unsparseArray2, function(elem, index, array) {
    console.log(index);
  });
}
<html><head>
  <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
  <title>Making sparse arrays unsparse</title>
</head><body>
  <p><button onclick="arrayApply();">Array.apply(null, sparseArray)</button></p>
  <p><button onclick="es6Spread();">[...sparseArray]</button></p>
</body>
</html>


0
在我的情况下,我正在寻找一种优雅的解决方案来创建一个以数字0X开头的数组。
通过箭头函数以优雅的方式,只需一行代码即可实现。
const arrayLength = 10;
const arrayOfDigits = Array.apply(null, Array(arrayLength)).map((_, index) => index);

对我来说,这似乎是一个相当复杂的问题,远比一个带有for循环的代码块复杂得多。


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