Kyle Simpson 证明我仍然不懂 JavaScript(数组)[编辑:一些补充]

2025-06-10

Kyle Simpson 证明我仍然不懂 JavaScript(数组)

[编辑:一些补充]

如果你还没读过凯尔·辛普森的精彩系列《你不懂JS》,我建议你先别读这个,去读那个,然后再回来。这可能需要六周左右的时间,不过没关系。我会等的。

…………

看到你回来了。太好了。感觉你现在懂 JavaScript 了?确定?回去再读一遍。我等着。这篇文章不会消失。

…………

感觉你现在真的了解 JS 了?太棒了。我们来聊聊最简单的数据结构——数组,以及我读了四遍那套书之后还不知道的东西。

JavaScript,我以为我了解你……

首先,我想向你指出这个推特话题,在该系列的第三条推文中,凯尔发表了这样的声明:

只是为了清楚起见,这是我发现的令人惊讶的......

kyle simpson 展示了在 foreach 和 for of 中用逗号创建的数组,其中 foreach 不显示未定义,但 for of 却显示

现在,我看着那张图片,心想:“这很有道理。但是……为什么它有道理呢?”

我越想思考为什么这是有意义的,就越意识到我不懂 JavaScript。

首先,关于制作阵列的入门知识

在 JavaScript 中创建数组有几种不同的方法。它们相当于“你可以实例化带数据或不带数据的数组”。

使用数据实例化

有时我们需要写入一个已经包含数据的数组。当你不希望列表在声明后发生实际变化时,这种情况很常见。因此,我们有两种实例化该数组的方法:

使用Array构造函数

const mistakes = new Array('my liberal arts degree', 'eating 15 slim jims')`
Enter fullscreen mode Exit fullscreen mode

这在 JavaScript 中被认为是一种很奇怪且不好的做法。我稍后会解释原因。

使用数组字面量

const moreMistakes = [tequila', 'Michael Bay\'s TMNT movie'];
Enter fullscreen mode Exit fullscreen mode

这是更常见的方法,我希望我们大多数人在家里都使用这种方法。

无数据实例化

在将数据从一个数据结构移动到另一个数据结构的情况下,我们经常声明一个空数组,然后对其进行修改。

一种极为常见的模式是声明空数组,然后将内容推送到该数组:

const moreFutureMistakes = [];

moreFutureMistakes.push('TMNT sequels')
Enter fullscreen mode Exit fullscreen mode

但如果你想成为那个人,当然你可以使用数组构造函数:

const moreUnusualMistakes = new Array();

moreUnusualMistakes.push('what you\'re going to see next');
Enter fullscreen mode Exit fullscreen mode

实例化数组的奇怪方法

我觉得自己从来没在野外见过这些,但它们一直萦绕在我的脑海里。有点像《夏威夷特工》的主题曲。它在我的脑海里什么也没做,只是静静地待在那里。确保我不会忘记它。

我在 C++ 课上唯一记得的一件事就是数组必须有大小。但我不知道为什么。我现在也不知道。(答案somethingSomething[memory]:)

因此,实例化数组的三种奇怪方法都涉及预先设置其大小:

const superSizeMe = [];
superSizeMe.length = 3; // SURPRISE! Length is a setter

const preSized = new Array(3); // "This won't confuse anyone," said no one ever.

const commaSized= [,,,]; 

const isWeirdButTrue= (superSizeMe.length === preSized.length === commaSized.length);

Enter fullscreen mode Exit fullscreen mode

如果你想知道为什么使用Array构造函数被认为是不好的做法,现在你明白了。这是因为如果你只给它一个参数,并且该参数是一个整数,它就会创建一个该大小的数组。因此,数组构造函数在处理数字时可能会得到意想不到的结果。

其他方法也远非最佳实践。它们全都是些古怪的做法,出自某些人之手,这些人或许好奇心过强,或者好奇诡计之神洛基是否真的活着,并且正在设计编程语言。

奇怪的实例和奇怪的设置数据的方式导致 奇怪的结果完全可以预料到的行为。

现在我们回到 Kyle 的推文,看看这有多么奇怪:

[,,,3,4,,5].forEach(x=>console.log(x));
// 3
// 4
// 5
Enter fullscreen mode Exit fullscreen mode
  1. 好吧,我们同意逗号实例化的数组很奇怪。
  2. 好的。它记录了……3、4、5

没问题。一切都很好。其他插槽必须可用undefined或不可用。

for (let x of [,,,3,4,,5]) { console.log(x); }
// undefined 
// undefined
// undefined
// 3
// 4
// undefined
// 5
Enter fullscreen mode Exit fullscreen mode

耽误....

那些“奇怪实例化的数组”里有什么?

让我们退一步来看看这些预先设定大小的数组:

const myRegrets = new Array(3);
const moreRegrets = [,,,];
const noRegerts = [];

noRegerts.length = 3;
Enter fullscreen mode Exit fullscreen mode

如果您使用的是 Firefox,请打开控制台,运行此程序,然后查看这些数组。

你可能会看到类似这样的内容:

Array(3) [undefined, undefined, undefined]
Enter fullscreen mode Exit fullscreen mode

但是那个数组真的填充了三个吗undefined

不,并非如此。Kyle Simpson 恰如其分地指出了这一点。如果你循环遍历这些“预先确定大小”的数组,并尝试记录值,你将不会得到任何记录:

const myRegrets = new Array(3);
myRegrets.forEach((regret, regretIndex) => {
  console.log(regret, regretIndex);
});


for (regretName in myRegrets) {
 console.log(regretName, myRegrets[regretName]);
}

Enter fullscreen mode Exit fullscreen mode

因此,这里的第一个重要要点是,预先设定大小的数组用逗号/槽创建的数组在这些槽中没有值

myRegrets不是一个包含 3 的数组undefined。它是一个包含三个空值的数组

为了进一步证明这一点,在第三个位置添加一个实际值: undefined

const myRegrets = new Array(3);
myRegrets[1] = undefined; 
myRegrets.forEach((regret, regretIndex) => {
  console.log(regret, regretIndex);
});


for (regretName in myRegrets) {
 console.log(regretName, myRegrets[regretName]);
}

Enter fullscreen mode Exit fullscreen mode

你带了根木头,对吧?就一根,对吧?

双倍T恤效果

数组中隐式显式 undefined

我认为这就是凯尔在这里谈论的内容。

当我们使用一些奇怪的数组技巧,比如预先设置大小,或者用逗号分隔(例如[,,undefined])时,JavaScript 实际上并没有将值放入这些位置。相反,它只是说这些位置存在……有点像

如果某物存在,但没有价值,我们就会给它起一个名字:

undefined

const myRegrets = [,,undefined];
const youWillRegretThis;

myRegrets[0] === youWillRegretThis; // true, so very true
Enter fullscreen mode Exit fullscreen mode

但我称之为“隐式未定义”,因为它不会记录在我们执行的任何循环中。Not forEach、neither for - in、normap及其伙伴将记录一个没有值的 slot;即隐式未定义;

如果您不喜欢“隐式未定义”,您也可以将其称为“未声明”。

显式undefined必须占用内存

当你循环遍历一个显式指定 undefined 的数组时,它必然会占用实际内存。这就是为什么它会被打印出来:

const myRegrets = [,,undefined];

myRegrets.forEach((regret, regretIndex) => {
  console.log(regret, regretIndex);
});
// will log 3

for (regretName in myRegrets) {
 console.log(regretName, myRegrets[regretName]);
}

// will log 3
Enter fullscreen mode Exit fullscreen mode

所以孩子们,记住这一点。数组有隐式 undefined 和显式 undefined。

除非您合并数组,否则这可能不会成为您很快遇到的事情。

或者使用for of...

等待。for of

是的。

(双你开球有效)

for - of是唯一不关心隐式或显式未定义的循环机制。

再次,将“隐式未定义”视为“未声明”:


const myRegrets = [,,undefined];


let regretCounter = 0;

for (regret of myRegrets) {
 console.log(regret, regretCounter++)
}
// undefined 0
// undefined 1
// undefined 2
Enter fullscreen mode Exit fullscreen mode

为什么它会记录这三个?

我不太清楚,但我有一个理论,一个比我聪明得多的人需要调查。

for of模式实现了迭代协议

我的理论是,数组上的迭代协议的行为类似于示例页面中显示的:

function makeIterator(array) {
    var nextIndex = 0;

    return {
       next: function() {
           return nextIndex < array.length ?
               {value: array[nextIndex++], done: false} :
               {done: true};
       }
    };
}
Enter fullscreen mode Exit fullscreen mode

如果for - of在底层实现类似的东西,它将通过索引进行迭代,而不是属性(数组实际上只是具有数字属性的对象。有点)。

那么,回到我们所说的“存在但没有价值的东西”这个问题。你知道吗?我们的老朋友。那个我们从不邀请参加派对,但他却总是出现的人?还记得他吗?

undefined

我真的开始不喜欢那家伙了。他让我感觉怪怪的。

TL;DR

  1. 数组的属性.length与“这些位置都有值”不同
  2. 数组可以有“隐式未定义”和“显式未定义”,这取决于空间是否真的有值,“隐式未定义”更像是“未声明”
  3. 如果你不以奇怪的方式创建数组,或者用数组做奇怪的事情,你可能永远不会遇到这种情况
  4. 如果您是那个骗子神洛基,或者是一个粉丝,并选择以奇怪的方式创建数组,或者以奇怪的方式操作它们,您可能需要使用for of循环来获得最一致的结果。

JavaScript 在清晨很有意义,但在深夜就没意义了。

[编辑:一些补充]

这篇文章的一些评论促使我做了一些研究和测试,或许对某些人有用。这篇文章已经很长了,所以除非你因为深夜狂吃奶酪而困在厕所隔间里,或者你真的对讨论规格感兴趣,否则你可以跳过这些内容。

是否存在被简单设置为不可枚举的数组属性?

我不这么认为

我浏览了一些ECMA 规范,看看这种行为是否在任何地方定义。

规范规定,没有赋值表达式的数组元素没有定义

第 12.2.5 节规定

当元素列表中的逗号前面没有赋值表达式(即,逗号位于逗号开头或另一个逗号之后)时,缺失的数组元素会增加数组的长度,并增加后续元素的索引。省略的数组元素没有定义。

因此,如果有[,,'foo'],逗号后没有某种表达式的数组元素将被“省略”。

还值得注意的是,规范指出这['foo',]不会影响数组的长度。

另外值得注意的是我还没有发现将值推送到长度以上的随机索引是否算作省略。例如:

const gapped = [];
gapped[2] = "please mind the gap";
Enter fullscreen mode Exit fullscreen mode

规范似乎没有说明数组元素被创建但不可枚举

22.1.1.3 节的步骤 8 描述了如何创建数组:

重复,直至 k < numberOfArgs
a. 令 Pk 为 !ToString(k)
b. 令 itemK 为 items[k]
c. 令 determineStatus 为 CreateDataProperty(array, Pk, itemK)
d. 断言:defineStatus 为 true
e. 将 k 增加 1

Pk是键(即索引),itemK是值。

如果 JavaScript 引擎遵循此算法,则无论项目的值是什么,都会传递到CreateDataProperty函数/方法/任何内容中。

问题是,“第一个插槽是否[,,'foo']构成一个项目?12.2.5 说不是。(我认为)

但是,是否有可能CreateDataProperty创建一个属性使其不可枚举?

如果您阅读第 7.3.4 节,就会发现它没有提供enumerable描述符中属性的任何逻辑或条件。步骤 3 和步骤 4 将属性设置为可枚举:

  1. 让 newDesc 成为 PropertyDescriptor { [[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]:true }。
  2. 返回?O. [DefineOwnProperty]

我还没有读完规范中关于数组的全部内容。但这似乎进一步表明这些“空槽”确实是空的

那么in操作员怎么办呢?它会找到这些空槽吗?

不,不会!

const slotted = [,,'one'];
let i = 0;

while (i < slotted.length) {
 if (i++ in slotted) {
  console.log(`${i - 1} is in the array`);
 }
}
Enter fullscreen mode Exit fullscreen mode

这将只记录一次,并显示2 is in the array在您的控制台中。

但是,如果您有明确的 ,它将记录三次。undefinedconst explicit = [undefined, undefined, 'one']

STL;SDR(还是太长,还是没读)

首先,我要声明一下,我更有资格跟你讨论法国存在主义,而不是 JavaScript。我所说的一切都是错的,可能性相当高。

根据我对规范的理解,“隐式未定义”是一种描述数组中没有值的“槽”的有效方式。

当然,实际上根本就不存在一个槽位。甚至连槽位的概念都不存在。没有价值,槽位就不存在。(#存在主义)

正如 Kyle Simpson 指出的那样,undefined之间存在差异undeclared,但 JavaScript 并不总是能向您提供明确哪个是哪个的信息。

这种“隐含的未定义”更像是一个存在问题,我们只有这么多方法来描述存在和虚无。

const existentialCrisis= [,undefined,'Waiting for Godot']`;
console.log(typeof existentialCrisis[1]); // undefined
console.log(typeof existentialCrisis[0]); // undefined

Enter fullscreen mode Exit fullscreen mode
鏂囩珷鏉ユ簮锛�https://dev.to/paceaux/kyle-simpson-proved-i-still-don-t-know-javascript-2fnp
PREV
DevRel: The basics.
NEXT
四种远程工作类型