Kyle Simpson 证明我仍然不懂 JavaScript(数组)
[编辑:一些补充]
如果你还没读过凯尔·辛普森的精彩系列《你不懂JS》,我建议你先别读这个,去读那个,然后再回来。这可能需要六周左右的时间,不过没关系。我会等的。
…………
看到你回来了。太好了。感觉你现在懂 JavaScript 了?确定?回去再读一遍。我等着。这篇文章不会消失。
…………
感觉你现在真的了解 JS 了?太棒了。我们来聊聊最简单的数据结构——数组,以及我读了四遍那套书之后还不知道的东西。
JavaScript,我以为我了解你……
首先,我想向你指出这个推特话题,在该系列的第三条推文中,凯尔发表了这样的声明:
只是为了清楚起见,这是我发现的令人惊讶的......
现在,我看着那张图片,心想:“这很有道理。但是……为什么它有道理呢?”
我越想思考为什么这是有意义的,就越意识到我不懂 JavaScript。
首先,关于制作阵列的入门知识
在 JavaScript 中创建数组有几种不同的方法。它们相当于“你可以实例化带数据或不带数据的数组”。
使用数据实例化
有时我们需要写入一个已经包含数据的数组。当你不希望列表在声明后发生实际变化时,这种情况很常见。因此,我们有两种实例化该数组的方法:
使用Array
构造函数
const mistakes = new Array('my liberal arts degree', 'eating 15 slim jims')`
这在 JavaScript 中被认为是一种很奇怪且不好的做法。我稍后会解释原因。
使用数组字面量
const moreMistakes = [tequila', 'Michael Bay\'s TMNT movie'];
这是更常见的方法,我希望我们大多数人在家里都使用这种方法。
无数据实例化
在将数据从一个数据结构移动到另一个数据结构的情况下,我们经常声明一个空数组,然后对其进行修改。
一种极为常见的模式是声明空数组,然后将内容推送到该数组:
const moreFutureMistakes = [];
moreFutureMistakes.push('TMNT sequels')
但如果你想成为那个人,当然你可以使用数组构造函数:
const moreUnusualMistakes = new Array();
moreUnusualMistakes.push('what you\'re going to see next');
实例化数组的奇怪方法
我觉得自己从来没在野外见过这些,但它们一直萦绕在我的脑海里。有点像《夏威夷特工》的主题曲。它在我的脑海里什么也没做,只是静静地待在那里。确保我不会忘记它。
我在 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);
如果你想知道为什么使用Array
构造函数被认为是不好的做法,现在你明白了。这是因为如果你只给它一个参数,并且该参数是一个整数,它就会创建一个该大小的数组。因此,数组构造函数在处理数字时可能会得到意想不到的结果。
其他方法也远非最佳实践。它们全都是些古怪的做法,出自某些人之手,这些人或许好奇心过强,或者好奇诡计之神洛基是否真的活着,并且正在设计编程语言。
奇怪的实例和奇怪的设置数据的方式导致
奇怪的结果完全可以预料到的行为。
现在我们回到 Kyle 的推文,看看这有多么奇怪:
[,,,3,4,,5].forEach(x=>console.log(x));
// 3
// 4
// 5
- 好吧,我们同意逗号实例化的数组很奇怪。
- 好的。它记录了……3、4、5
没问题。一切都很好。其他插槽必须可用undefined
或不可用。
for (let x of [,,,3,4,,5]) { console.log(x); }
// undefined
// undefined
// undefined
// 3
// 4
// undefined
// 5
耽误....
那些“奇怪实例化的数组”里有什么?
让我们退一步来看看这些预先设定大小的数组:
const myRegrets = new Array(3);
const moreRegrets = [,,,];
const noRegerts = [];
noRegerts.length = 3;
如果您使用的是 Firefox,请打开控制台,运行此程序,然后查看这些数组。
你可能会看到类似这样的内容:
Array(3) [undefined, undefined, undefined]
但是那个数组真的填充了三个吗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]);
}
因此,这里的第一个重要要点是,预先设定大小的数组或用逗号/槽创建的数组在这些槽中没有值。
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]);
}
你带了根木头,对吧?就一根,对吧?
双倍T恤效果
数组中有隐式和显式 undefined
我认为这就是凯尔在这里谈论的内容。
当我们使用一些奇怪的数组技巧,比如预先设置大小,或者用逗号分隔(例如[,,undefined]
)时,JavaScript 实际上并没有将值放入这些位置。相反,它只是说这些位置存在……有点像。
如果某物存在,但没有价值,我们就会给它起一个名字:
undefined
const myRegrets = [,,undefined];
const youWillRegretThis;
myRegrets[0] === youWillRegretThis; // true, so very true
但我称之为“隐式未定义”,因为它不会记录在我们执行的任何循环中。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
所以孩子们,记住这一点。数组有隐式 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
为什么它会记录这三个?
我不太清楚,但我有一个理论,一个比我聪明得多的人需要调查。
该for of
模式实现了迭代协议
我的理论是,数组上的迭代协议的行为类似于示例页面中显示的:
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
如果for - of
在底层实现类似的东西,它将通过索引进行迭代,而不是属性(数组实际上只是具有数字属性的对象。有点)。
那么,回到我们所说的“存在但没有价值的东西”这个问题。你知道吗?我们的老朋友。那个我们从不邀请参加派对,但他却总是出现的人?还记得他吗?
undefined
我真的开始不喜欢那家伙了。他让我感觉怪怪的。
TL;DR
- 数组的属性
.length
与“这些位置都有值”不同 - 数组可以有“隐式未定义”和“显式未定义”,这取决于空间是否真的有值,“隐式未定义”更像是“未声明”
- 如果你不以奇怪的方式创建数组,或者用数组做奇怪的事情,你可能永远不会遇到这种情况
- 如果您是那个骗子神洛基,或者是一个粉丝,并选择以奇怪的方式创建数组,或者以奇怪的方式操作它们,您可能需要使用
for of
循环来获得最一致的结果。
JavaScript 在清晨很有意义,但在深夜就没意义了。
[编辑:一些补充]
这篇文章的一些评论促使我做了一些研究和测试,或许对某些人有用。这篇文章已经很长了,所以除非你因为深夜狂吃奶酪而困在厕所隔间里,或者你真的对讨论规格感兴趣,否则你可以跳过这些内容。
是否存在被简单设置为不可枚举的数组属性?
我不这么认为。
我浏览了一些ECMA 规范,看看这种行为是否在任何地方定义。
规范规定,没有赋值表达式的数组元素没有定义
第 12.2.5 节规定
当元素列表中的逗号前面没有赋值表达式(即,逗号位于逗号开头或另一个逗号之后)时,缺失的数组元素会增加数组的长度,并增加后续元素的索引。省略的数组元素没有定义。
因此,如果有[,,'foo']
,逗号后没有某种表达式的数组元素将被“省略”。
还值得注意的是,规范指出这['foo',]
不会影响数组的长度。
另外值得注意的是,我还没有发现将值推送到长度以上的随机索引是否算作省略。例如:
const gapped = [];
gapped[2] = "please mind the gap";
规范似乎没有说明数组元素被创建但不可枚举
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 将属性设置为可枚举:
- 让 newDesc 成为 PropertyDescriptor { [[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]:true }。
- 返回?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`);
}
}
这将只记录一次,并显示2 is in the array
在您的控制台中。
但是,如果您有明确的 ,它将记录三次。undefined
const 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