Defined or Undefined? The nuances of creating arrays in JavaScript

image






A couple of months ago I stumbled upon an interesting question on stackoverflow , where, in short, a person wanted to create an empty 5x5 matrix, and using one method he succeeded, but using another he didn’t. In the ensuing discussion, interesting thoughts were cited on this subject.



True, the person who asked the question, as well as those who answered him, did not pay attention to the fact that in fact the matrix could not be created, and the result of the calculation was incorrect. All this interested me, and, I decided to dig a little deeper, then to come to interesting conclusions, which I’ll share with you now.



Note: I also answered under that discussion, under the nickname AndreyGS - there I answered rather briefly, here I will try to fully cover the problem.



In general, so, we are faced with the task of creating an array. How are we going to do this? Oddly enough, there are different options, depending on what we want to get.



We know that functions in JavaScript have two internal methods, Call and Construct . If we use the new keyword, the Construct method is used, which creates a new instance of the object, assigns this reference to it , and then executes the body of the function. Not all functions have this method, but for us this is not so important right now.



When creating arrays, there is one feature: it doesn’t matter if we use Array (...) or new Array (...) - the ECMAScript specification does not make any differences for them and, moreover, considers them equivalent.



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.
      
      





Therefore, I will not philosophize mischievously, and in the examples I will only use the new Array (...) construct, so as not to confuse anyone.



Let's get started.



Create an array:



 let arr = new Array(5);
      
      





What did we get?



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





Hmm ... well, in principle, it should be so - we set the length and got five empty cells, with the value undefined , which can be worked on further, right? True, there are a couple of points that confuse me. Let's check.



 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
      
      





How is it that, after all, we had to get a matrix, and in each cell, accordingly, there should be an array of 5 elements ...



Let us again turn to the ECMAScript documentation and see what is written in it regarding the method of creating arrays with one argument:



 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.
      
      





And, what we see, it turns out that the object is created, the length property is created in the ArrayCreate procedure (point 6), the value in the length property is set down (point 9), and what about the cells? Apart from the special case when the argument passed is not a number, and an array is created with a single cell "0" with the corresponding value (point 7), there is not a word about them ... That is, there == 5 length, but there are no five cells. Yes, the compiler confuses us when we try to access a single cell, it gives out that its value is undefined , while it actually is not.



Here, for comparison, the method of creating arrays with several arguments sent to the constructor:



 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.
      
      





Here, please - 10 point, the creation of those same cells.



Now what does Array.prototype.map () do next?



 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.
      
      





Clause 7 - a copy of the original array is created, in clause 10 len iterations are performed on its elements, and, in particular, clause 10.2 checks whether there is a specific cell in the source array, then, if successful, perform mapping (10.4) and create the appropriate cell in the copy - 10.4.5. Since 10.2 gives false for each of the 5 passes, not a single cell in the returned copy of the array will be created either.



So, how the array constructor and the Array.prototype.map () method work, we figured out, but the task remained as before unsolved, because the matrix was not built. Function.prototype.apply () will come to the rescue!

Let's check it in action right away:



 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 }
      
      





Hurray, all five cells are clearly observed here, and also the first, test, cell with the number “0” has a descriptor.



In this case, the program worked as follows:



  1. We called the Function.prototype.apply () method and passed the null context to it, and as the array new Array (5) .
  2. new Array (5) created an array without cells, but with a length of 5 .
  3. Function.prototype.apply () used the internal method of breaking the array into separate arguments, as a result of which, it passed five arguments with undefined values ​​to the Array constructor.
  4. Array received 5 arguments with undefined values, added them to the corresponding cells.


Everything seems to be understandable, except what is this internal method of Function.prototype.apply () , which makes 5 arguments out of nothing - I suggest again to look at the ECMAScript documentation:



 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.
      
      





We look at the most interesting points:



19.2.3.1 - clause 3: creating a list of arguments from an object similar to an array (as we recall, such objects should have a length property).



7.3.17 - the list creation method itself. It checks whether the object is or not, and, if so, a request for the value of the length field (point 4). Then an index equal to “0” is created (paragraph 7). A cycle is created with an increment of the index to the value taken from the length field (paragraph 8). In this cycle, we refer to the values ​​of the cells of the transmitted array with the corresponding indices (clauses 8a and 8b). And as we recall, when accessing the value of a single cell in an array in which there are actually no cells, it still gives a value of - undefined . The resulting value is added to the end of the argument list (paragraph 8e).



Well, now that everything has fallen into place, you can safely build the very empty matrix.



 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 }
      
      





Now, as you can see, everything converges and looks pretty simple: we, in the way we already know, create a simple empty Array.apply (null, new Array (5)) array and then pass it to the map method, which creates the same array in each of the cells.



In addition, you can make it even easier. The spread - ... operator appeared in ECMAScript6 , and, which is typical, it also specifically works with arrays. Therefore, we can simply drive in:



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





or we’ll simplify it completely, even though I had previously promised new not to touch ...



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



note: here we also used arrow functions, since we are still dealing with a spread operator that appeared in the same specification as them.



We will not go into the principle of the spread operator, however, for general development, I think this example was also useful.



In addition, we, of course, can build our own functions, which, like Function.prototype.apply () by enumeration, will create normal arrays for us with empty cells, however, understanding the internal principles of JavaScript and, accordingly, the correct and adequate use built-in functions, is a basis to master which is a priority. Well, and, of course, it’s so simple faster and more convenient.



And finally, returning to the same question on stackoverflow - there, I recall, the person mistakenly considered that the method he received led to the correct answer, and that he received a 5x5 matrix, however - a small error crept in there.



He drove in:



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

return new Array(5);

});








What do you think the actual result will be here?



Answer
console.log (arr); // Array (5) [(5) [...], (5) [...], (5) [...], (5) [...], (5) [...]]

console.log (arr [0]); // Array (5) [<5 empty slots>]

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")); // undefined



isn't it, that’s not exactly what he wanted ...



References:



ECMAScript 2015 Language Specification

What is Array.apply actually doing



All Articles