定义还是未定义? 在JavaScript中创建数组的细节

图片

几个月前,我偶然发现了一个关于stackoverflow的有趣问题,简而言之,一个人想要创建一个空的5x5矩阵,并使用他成功的一种方法,但是没有使用另一种方法。 在随后的讨论中,对此主题提出了有趣的想法。

的确,提出问题的人以及回答该问题的人都没有注意到事实上无法创建矩阵并且计算结果不正确的事实。 这一切使我感兴趣,因此我决定更深入地研究,然后得出有趣的结论,我现在将与您分享。

注意:在讨论中,我也以昵称AndreyGS进行了回答-在这里我做了简短的回答,在这里我将尽力解决问题。

通常,挑战在于创建数组。 我们该怎么做? 奇怪的是,有不同的选择,这取决于我们想要得到什么。

我们知道JavaScript中的函数有两个内部方法, CallConstruct 。 如果我们使用new关键字,则使用Construct方法,该方法创建对象的新实例, 将此引用分配给 ,然后执行该函数的主体。 并非所有函数都具有此方法,但是对我们而言,这现在并不重要。

创建数组时,有一个特点:我们使用Array(...)还是新的Array(...)都没关系-ECMAScript规范不区分它们,而且认为它们是等效的。

22.1.1 The Array Constructor The Array constructor is the %Array% intrinsic object and the initial value of the Array property of the global object. When called as a constructor it creates and initializes a new exotic Array object. When Array is called as a function rather than as a constructor, it also creates and initializes a new Array object. Thus the function call Array(…) is equivalent to the object creation expression new Array(…) with the same arguments. 

因此,我不会调皮捣蛋,在示例中,我将仅使用新的Array(...)构造,以免混淆任何人。

让我们开始吧。

创建一个数组:

 let arr = new Array(5); 

我们得到了什么?

 console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined 

嗯...嗯,原则上应该是这样-我们设置长度并得到五个空单元格,其值是undefined ,可以进一步处理,对吧? 没错,有几点让我感到困惑。 让我们来看看。

 let arr = new Array(5).map(function() { return new Array(5); }); console.log(arr); // Array(5) [ <5 empty slots> ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // undefined console.log(arr[0][0]); // TypeError: arr[0] is undefined 

毕竟,我们必须得到一个矩阵,然后在每个单元格中应该有5个元素的数组,这是怎么回事?

让我们再次转到ECMAScript文档,看看其中写的有关使用一个参数创建数组的方法的内容:

 22.1.1.2 Array (len) This description applies if and only if the Array constructor is called with exactly one argument. 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs = 1. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(0, proto). 7. If Type(len) is not Number, then 1. Let defineStatus be CreateDataProperty(array, "0", len). 2. Assert: defineStatus is true. 3. Let intLen be 1. 8. Else, 1. Let intLen be ToUint32(len). 2. If intLen ≠ len, throw a RangeError exception. 9. Let setStatus be Set(array, "length", intLen, true). 10. Assert: setStatus is not an abrupt completion. 11. Return array. 

而且,我们看到的是,事实证明对象已创建,在ArrayCreate过程中创建了length属性(第6点),在length属性中设置了值(第9点),单元格又如何? 除了特殊情况(传递的参数不是数字,并且使用单个单元格“ 0”创建具有对应值(点7)的数组)外,没有关于它们的任何单词……也就是说,== 5的长度,但是没有五个单元格。 是的,当我们尝试访问单个单元格时,编译器使我们感到困惑,它表明其值是undefined ,而实际上不是。

为了进行比较,这里提供了使用多个参数发送到构造函数的创建数组的方法:

 22.1.1.3 Array (...items ) This description applies if and only if the Array constructor is called with at least two arguments. When the Array function is called the following steps are taken: 1. Let numberOfArgs be the number of arguments passed to this function call. 2. Assert: numberOfArgs ≥ 2. 3. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. 4. Let proto be GetPrototypeFromConstructor(newTarget, "%ArrayPrototype%"). 5. ReturnIfAbrupt(proto). 6. Let array be ArrayCreate(numberOfArgs, proto). 7. ReturnIfAbrupt(array). 8. Let k be 0. 9. Let items be a zero-origined List containing the argument items in order. 10. Repeat, while k < numberOfArgs 1. Let Pk be ToString(k). 2. Let itemK be items[k]. 3. Let defineStatus be CreateDataProperty(array, Pk, itemK). 4. Assert: defineStatus is true. 5. Increase k by 1. 11. Assert: the value of array's length property is numberOfArgs. 12. Return array. 

请在这里-10点,创建相同的单元格。

现在, Array.prototype.map()接下来做什么?

 22.1.3.15 Array.prototype.map ( callbackfn [ , thisArg ] ) 1. Let O be ToObject(this value). 2. ReturnIfAbrupt(O). 3. Let len be ToLength(Get(O, "length")). 4. ReturnIfAbrupt(len). 5. If IsCallable(callbackfn) is false, throw a TypeError exception. 6. If thisArg was supplied, let T be thisArg; else let T be undefined. 7. Let A be ArraySpeciesCreate(O, len). 8. ReturnIfAbrupt(A). 9. Let k be 0. 10. Repeat, while k < len 1. Let Pk be ToString(k). 2. Let kPresent be HasProperty(O, Pk). 3. ReturnIfAbrupt(kPresent). 4. If kPresent is true, then 1. Let kValue be Get(O, Pk). 2. ReturnIfAbrupt(kValue). 3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»). 4. ReturnIfAbrupt(mappedValue). 5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue). 6. ReturnIfAbrupt(status). 5. Increase k by 1. 11. Return A. 

条款7-创建原始数组的副本,在条款10中对其元素执行len迭代,尤其是条款10.2检查源数组中是否存在特定的单元格,以便在成功的情况下映射(10.4)并在副本中创建适当的单元格-10.4.5。 由于10.2在5次传递中每一次都为false ,因此在返回数组的副本中也不会创建单个单元格。

因此,我们弄清楚了数组构造函数和Array.prototype.map()方法是如何工作的 ,但由于未构建矩阵,因此任务仍未解决。 Function.prototype.apply()会救出来的!
让我们立即检查一下它的作用:

 let arr = Array.apply(null, new Array(5)); console.log(arr); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(arr[0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

欢呼,这里清楚地观察到所有五个单元格,第一个测试数字为“ 0”的单元格都有一个描述符。

在这种情况下,该程序的工作方式如下:

  1. 我们调用了Function.prototype.apply()方法,并将null上下文传递给它,并将其作为数组new Array(5)传递。
  2. new Array(5)创建了一个没有单元格但长度为5的数组。
  3. Function.prototype.apply()使用了将数组拆分为单独参数的内部方法,因此,将五个具有未定义值的参数传递给Array构造函数。
  4. 数组接收到5个具有未定义值的参数,并将它们添加到相应的单元格中。

一切似乎都很清楚,除了Function.prototype.apply()的内部方法是什么之外,它使5个参数完全变为空白 -我建议再次查看ECMAScript文档:

 19.2.3.1 Function.prototype.apply 1. If IsCallable(func) is false, throw a TypeError exception. 2. If argArray is null or undefined, then Return Call(func, thisArg). 3. Let argList be CreateListFromArrayLike(argArray). 7.3.17 CreateListFromArrayLike (obj [, elementTypes] ) 1. ReturnIfAbrupt(obj). 2. If elementTypes was not passed, let elementTypes be (Undefined, Null, Boolean, String, Symbol, Number, Object). 3. If Type(obj) is not Object, throw a TypeError exception. 4. Let len be ToLength(Get(obj, "length")). 5. ReturnIfAbrupt(len). 6. Let list be an empty List. 7. Let index be 0. 8. Repeat while index < len a. Let indexName be ToString(index). b. Let next be Get(obj, indexName). c. ReturnIfAbrupt(next). d. If Type(next) is not an element of elementTypes, throw a TypeError exception. e. Append next as the last element of list. f. Set index to index + 1. 9. Return list. 

我们来看最有趣的几点:

19.2.3.1-第3段:从类似于数组的对象创建一个参数列表(我们记得,此类对象应具有length属性)。

7.3.17-列表创建方法本身。 它检查对象是否存在,如果是,则检查对长度字段的请求(第4段)。 然后创建一个等于“ 0”的索引(第7段)。 创建一个循环,将索引的增量增加到从长度字段中获取的值(第8段)。 在此循环中,我们用相应的索引(第8a和8b条)指代所传输数组的像元值。 就像我们记得的那样,当访问实际上没有任何单元格的数组中的单个单元格的值时,它仍然会给出值undefined 。 结果值将添加到参数列表的末尾(第8e段)。

好了,既然一切都准备就绪,您就可以安全地建立非常空的矩阵。

 let arr = Array.apply(null, new Array(5)).map(function(){ return Array.apply(null,new Array(5)); }); console.log(arr); // Array(5) [ (5) […], (5) […], (5) […], (5) […], (5) […] ] console.log(arr[0]); // Array(5) [ undefined, undefined, undefined, undefined, undefined ] console.log(Object.getOwnPropertyDescriptor(arr,"0")); // Object { value: (5) […], writable: true, enumerable: true, configurable: true } console.log(arr[0][0]); // undefined console.log(Object.getOwnPropertyDescriptor(arr[0],"0")); // Object { value: undefined, writable: true, enumerable: true, configurable: true } 

现在,您可以看到,所有内容都收敛并且看起来非常简单:以我们已经知道的方式,我们创建了一个简单的空Array.apply(空,新的Array(5))数组,然后将其传递给map方法,后者在其中创建了相同的数组。每个单元。

此外,您可以使其变得更加容易。 扩展 -...运算符出现在ECMAScript6中 ,这是典型的,它也特别适用于数组。 因此,我们可以简单地进入:

 let arr = new Array(...new Array(5)).map(() => new Array(...new Array(5))); 

还是我们将其完全简化,即使我之前曾承诺不要接触新的东西...

 let arr = Array(...Array(5)).map(() => Array(...Array(5))); 
注意:这里我们还使用了箭头功能,因为我们仍在处理与它们相同的规范中出现的扩展运算符。

我们将不讨论散布算子的原理,但是,对于一般开发而言,我相信此示例也很有用。

此外,我们当然可以构建函数,使用Function.prototype.apply()排序将为我们创建带有空单元格的常规数组,但是,了解JavaScript的内部原理以及相应的正确用法内置功能,是掌握的基础,这是当务之急。 嗯,当然,它是如此简单,快捷,方便。

最后,回到关于stackoverflow的相同问题-我记得,那个人错误地认为他收到的方法导致了正确的答案,但是他收到了一个5x5矩阵,但是-一个小错误悄悄潜入了那里。

他开车进去了:

Array.apply(null, new Array(5)).map(function(){
return new Array(5);
});


您认为实际结果如何?

答案
console.log(arr); //数组(5)[(5)[...],(5)[...],(5)[...],(5)[...],(5)[...]]
console.log(arr [0]); //数组(5)[<5空插槽>]
console.log(Object.getOwnPropertyDescriptor(arr,“ 0”)); //对象{value:(5)[...],可写:true,可枚举:true,可配置:true}
console.log(arr [0] [0]); //未定义
console.log(Object.getOwnPropertyDescriptor(arr [0],“ 0”)); //未定义

是不是,那不是他想要的...

参考文献:

ECMAScript 2015语言规范
Array.apply实际在做什么

Source: https://habr.com/ru/post/zh-CN463041/


All Articles