Arrays
Predefined objects
In JavaScript arrays are not of an explicit data type. JavaScript provides a predefined Array
object.
The keys are called indexes, being nonnegative integers starting with zero.
The indexed properties are called elements.
Next to elements, an array can also have "regular" object properties, like the Array.prototype.length
property,
which sets or returns the number of elements in an array.
const myArray = ["value1", "value2"];
console.log(myArray[0]); // logs: "value1"
console.log(myArray["0"]); // logs: "value1"
console.log(myArray[1]); // logs: "value2"
console.log(myArray[2]); // logs: undefined
Arrays should not be confused with array-like objects, which also have indexed properties and a length
property.
Arrays are predefined objects with their own specific inherited methods and a specific literal syntax,
and unlike array-like objects, arrays are inherently iterable.
Array-likes were covered in the previous chapter.
const arrayLike = {
0: "pit",
1: "pot",
2: "pat",
length: 3,
}
for (let elem of arrayLike) {
console.log(elem);
}
// Logs: Uncaught TypeError: arrayLike is not iterable
const arrayLike = {
0: "pet",
1: "pot",
2: "put",
length: 3,
}
Array.prototype.forEach.call(arrayLike, (element) => {
console.log(element);
});
// logs:
// "pet"
// "pot"
// "put"
arrayLike.forEach(element => console.log(element)); // Uncaught TypeError: arrayLike.forEach is not a function
Creating an array
To create an array you can use the constructor function (with or without the keyword new
), but generally
the preferred way is to use an array literal (aka array initializer).
const myArray1 = ["value1", "value2", 3];
// ... which has exactly the same effect as:
const myArray2 = new Array("value1", "value2", 3);
// or:
const myArray3 = Array("value1", "value2", 3);
If you use the constructor function with a single argument that is a number, the argument is interpreted as the length of the array,
and not as the array's single element with a number value.
In other words, it creates an empty array of the specified length; in the example below, an array of 3 empty slots.
If the argument is a non-integer number, the creation results in a RangeError
.
const myArray = new Array(3);
console.log(myArray[0]); // logs: undefined
console.log(myArray[1]); // logs: undefined
console.log(myArray.length); // logs: 3
// The above array creation has exactly the same effect as:
const myArray2 = [];
myArray2.length = 3;
// What the programmer possibly intended to create:
const myArray3 = [3];
console.log(myArray3[0]); // logs: 3
console.log(myArray3.length); // logs: 1
const myArray4 = new Array(3, 2);
console.log(myArray4); // logs: [ 3, 2 ]
const myArray5 = new Array(3.2); // logs: RangeError: invalid array length
You can also use the Array.of()
method to create a new array.
This method handles a single element with a number value as expected.
const myArray1 = Array.of(3);
const myArray2 = Array.of(3.2);
const myArray3 = Array.of(3, "a", "b");
console.log(`${myArray1[0]}, ${myArray2[0]}, ${myArray3[1]}`); // logs: "3, 3.2, a"
Maximum length
According to ECMAScript specification, an array can have a maximum of 232 − 1 = 4 294 967 295 elements (maximum index: 4 294 967 294).
However, on an arbitrary user agent an application may work fine with (far) more elements in an array.
The length
property though, is always an integer less than 232.
const myArray = [];
myArray[5000000000] = "hello";
console.log(myArray[5000000000]); // logged on my machine: "hello"
console.log(myArray.length); // logs: 0
myArray.length = 4294967296; // logs: RangeError: invalid array length
Accessing elements
The elements of an array cannot be accessed by means of the dot-notation since indexes are not valid identifiers. Only the square bracket notation can be used. If the key is an integer, it will be interpreted as the index of an array element, if it is a non-integer number, it will be interpreted as a key of a "regular" property of the array. The square bracket notation can also be used to access computed indexes.
const myArray = ['one', 'two', 'three'];
console.log(myArray[2]); // logs: "three"
console.log(myArray["0"]); // logs: "one"
console.log(myArray['length'] === myArray.length); // logs: true
myArray[3] = "four";
myArray[3.2] = "five";
console.log(myArray); // logs: ['one', 'two', 'three', 'four']
console.log(myArray[3.2]); // logs: 'five'
console.log(myArray[myArray.length - 1]); // logs: "four"
Finding and checking elements in an array
You can use the
indexOf()
method to find the index of a given element.
indexOf()
returns -1 if the element is not present.
const myArray = ["pit", "pet", "pot"];
console.log(myArray.indexOf('pit')); // logs: 0
console.log(myArray.indexOf('pet')); // logs: 1
console.log(myArray.indexOf('pat')); // logs: -1
You can check if an array contains a certain element by, again, using indexOf()
or by using the
includes()
method.
const myArray = ["pit", "pet", "pot"];
console.log(myArray.includes('pit')); // logs: true
console.log(myArray.includes('pat')); // logs: false
console.log(myArray.indexOf('pit') !== -1); // logs: true
console.log(myArray.indexOf('pat') !== -1); // logs: false
You can check if an array index exists by either using the hasOwn()
method or
using the in
operator.
const myArray = ["pit", "pet", "pot"];
console.log( Object.hasOwn(myArray, 2) ); // logs: true
console.log( Object.hasOwn(myArray, 3) ); // logs: false
console.log(0 in myArray); // logs: true
console.log(3 in myArray); // logs: false
console.log("pot" in myArray); // logs: false
console.log("length" in myArray); // logs: true
console.log(Symbol.iterator in myArray); // logs: true
Sparse arrays
Arrays can contain "empty slots".
Empty slots are not filled with an element, but the array's length
property will return the number of elements including the empty slots.
Empty slots are not the same as elements with the value set to undefined
, nor are they the same as elements with an index equal or larger than the array's length.
Arrays with empty slots, i.e., gaps in the sequence of their indexes, are called sparse arrays, as opposed to dense arrays.
const arr1 = [1, 2, 3]; // dense array
console.log(arr1[3]); //logs: undefined
console.log(arr1.length); //logs: 3
const arr2 = [1, 2, undefined, 3 ]; // dense array
console.log(arr2[2]); //logs: undefined
console.log(arr2.length); //logs: 4
console.log(arr2); //logs: [ 1, 2, undefined, 3 ]
console.log(2 in arr2); // logs: true
const arr3 = [1, 2, , 3 ]; // sparse array
console.log(arr3[2]); //logs: undefined
console.log(arr3.length); //logs: 4
console.log(arr3); //logs: [ 1, 2, <1 empty slot>, 3 ]
console.log(2 in arr3); // logs: false
const arr4 = [1, 2, 3, ]; // dense array
console.log(arr4); //logs: [ 1, 2, 3 ]
const arr5 = [, 1, 2, 3]; // sparse array
console.log(arr5[0]); //logs: undefined
console.log(arr5.length); //logs: 4
const arr1 = new Array(3); // sparse array
console.log(arr1[2]); //logs: undefined
console.log(arr1.length); //logs: 3
console.log(arr1); //logs: [ <3 empty slots> ]
const arr2 = [1, 2, 3]; // dense array
arr2[4] = 5; // arr2 is now a sparse array
console.log(arr2); //logs: [ 1, 2, 3, <1 empty slot>, 5 ]
const arr3 = [1, 2, 3]; // dense array
arr3.length = 5; // arr3 is now a sparse array
console.log(arr3); //logs: [ 1, 2, 3, <2 empty slots> ]
console.log(arr3.length); //logs: 5
Also deleting elements from an array using the delete
operator leaves empty slots and makes the array a sparse array.
Later this section more about deleting elements.
Adding elements
Elements can simply be added by using the square bracket notation:
const myArray = [];
myArray[0] = "foo";
console.log(myArray); // logs: [ "foo" ]
// adding to the end of the array:
myArray[myArray.length] = "bar";
console.log(myArray); // logs: [ "foo", "bar" ]
The push()
method
can be used to add one or more elements to the end of an array.
const myArray = ["pit"];
myArray.push("pet", "pot");
console.log(myArray); // logs: [ "pit", "pet", "pot" ]
The unshift()
method
can be used to add one or more elements to the beginning of an array.
const myArray = ["pot"];
myArray.unshift("pit", "pet");
console.log(myArray); // logs: [ "pit", "pet", "pot" ]
Deleting elements
Elements can be deleted from an array by using the delete
operator. However, this creates a sparse array.
The deleted element will leave an empty slot. Alternatively you can set the value of the element to undefined
, which
does not create a sparse array.
const arr1 = ["foo", "bar", "baz"];
delete arr1[2];
console.log(2 in arr1); // logs: false
console.log(arr1.length); // logs: 3
const arr2 = ["foo", "bar", "baz"];
arr2[2] = undefined;
console.log(2 in arr2); // logs: true
console.log(arr2.length); // logs: 3
Truly deleting an element from an array (deleting both the element and the slot) can be achieved by using Array
method
splice()
,
that removes elements from an array and, optionally, replaces them.
const arr = ["foo", "bar", "baz", "foobar"];
// one element is removed starting from index 2:
arr.splice(2, 1);
console.log(arr); // logs: [ "foo", "bar", "foobar" ]
// the last two items are removed:
arr.splice(-2);
console.log(arr); // logs: [ "foo" ]
// remove element with value "baz":
const arr = ["foo", "bar", "baz", "foobar"];
arr.splice(arr.indexOf("baz"), 1);
console.log(arr); // logs: [ "foo", "bar", "foobar" ]
// replace elements:
const arr = ["foo", "bar", "baz", "foobar"];
const removedItems = arr.splice(1, 2, "fizz", "buzz");
console.log(arr); // logs: [ "foo", "fizz", "buzz", "foobar" ]
console.log(removedItems); // logs: [ "bar", "baz" ]
Method pop()
can be used to remove only the last element from an array.
Method shift()
can be used to remove only the first element from an array.
Also the array's length
property can be used to truly delete elements from the end of the list, i.e.,
to truncate the array.
const onWheels = ["car", "bike", "skateboard"];
onWheels.length = 2;
// onWheels.splice(2); // this does the same
// onWheels.pop(); // this does the same
console.log(onWheels); // logs: [ "car", "bike" ] // "skateboard" has been removed
onWheels.length = 0;
// onWheels.splice(0); // this does the same
console.log(onWheels); // logs: [ ] // empty array
Note that assigning 0
to the length
property is not the same as assigning an empty array to the variable.
The former alters the existing array, the latter replaces the existing array with a new (empty) array.
let onWheels = ["car", "bike", "skateboard"];
const onWheelsCopy = onWheels;
onWheels = [];
console.log(onWheels); // logs: [ ]
console.log(onWheelsCopy); // logs: ["car", "bike", "skateboard"]
const onWheels = ["car", "bike", "skateboard"];
const onWheelsCopy = onWheels;
onWheels.length = 0;
console.log(onWheels); // logs: [ ]
console.log(onWheelsCopy); // logs: [ ]
Multi-dimensional arrays
An array element can contain another array. This nesting of arrays can be used to create multi-dimensional arrays. In a multi-dimensional array each element is again an array. This can be multiple layers deep. The next example creates a two-dimensional array by means of a loop.
const myArray = [];
for (let i = 0; i < 3; i++) {
myArray[i] = [];
for (let j = 0; j < 3; j++) {
myArray[i][j] = `[${i}, ${j}]`;
}
}
console.table(myArray);
/* logs:
[0, 0] [0, 1] [0, 2]
[1, 0] [1, 1] [1, 2]
[2, 0] [2, 1] [2, 2]
*/
BTW. console.table()
is used to displays the array as a table.
An other example of a two-dimensional array:
const powers = [];
for (let x = 0; x < 10; x++) {
powers.push([
x ** 2,
x ** 3,
]);
}
console.log(powers.join('\n'));
// logs:
// 0,0
// 1,1
// 4,8
// 9,27
// 16,64
// 25,125
// 36,216
// 49,343
// 64,512
// 81,729
BTW. The Array.prototype.join()
method is used to convert the array into a string.
Iterating over arrays
The Array
object has a number of methods. In this section we will use Array
method forEach()
.
Additionally there are methods to sort, filter and find certain elements, alter the object or create a new object based on an excising one.
This is a list of Array
methods.
A number of them are explained in this chapter.
A number of these methods and some loops can be used to iterate over the elements of an array.
Elements of an array are enumerable own properties, so we can use methods and loops that enumerate properties to iterate over the elements of an array.
However, it will also iterate over all "regular" enumerable properties of the array (if they exist) and it will be in an arbitrary order.
Object.keys()
, for...in
, Object.assign
etc.
use property enumeration, as we have seen. Iterating exclusively over array elements generally does not use property enumeration.
Iterating constructs, like the for...of
loop or spread syntax,
that iterate over array elements typically use the array's iterator.
This is a built-in method of the Array.prototype
.
Methods that use the array's iterator visit the elements of an array in a successive ascending order.
Other Array
's
iterative methods,
e.g. the forEach()
method, do not use the Array
's iterator.
They use the fact that properties are indexed, in combination with the length
property,
which can be easily demonstrated using a common for
loop:
const myArray = ["pet", "pot", "pit"];
for (let i = 0; i < myArray.length; i++) {
console.log(myArray[i]);
}
// logs:
// "pet"
// "pot"
// "pit"
const myArray = [1, 2 , 3];
myArray.prop = "enumValue";
let elmArr = [];
for (let i = 0; i < myArray.length; i++) { elmArr.push(myArray[i]) }
console.log(elmArr); // logs: [ 1, 2, 3 ]
elmArr.length = 0;
myArray.forEach(elementValue => elmArr.push(elementValue));
console.log(elmArr); // logs: [ 1, 2, 3 ]
elmArr.length = 0;
myArray.forEach((elementValue, key) => elmArr.push(`${key}: ${elementValue}`));
console.log(elmArr); // logs: [ "0: 1", "1: 2", "2: 3" ]
elmArr.length = 0;
// Array's iterator:
for (const elementValue of myArray) { elmArr.push(elementValue) }
console.log(elmArr); // logs: [ 1, 2, 3 ]
elmArr.length = 0;
elmArr = [...myArray];
console.log(elmArr); // logs: [ 1, 2, 3 ]
elmArr.length = 0;
// Property enumeration:
for (const key in myArray) { elmArr.push(myArray[key]) };
console.log(elmArr); // logs: [ 1, 2, 3, "enumValue" ]
console.log( Object.assign({}, myArray) ); // logs: { 0: 1, 1: 2, 2: 3, prop: "enumValue" }
console.log(Object.keys(myArray)); // logs: [ "0", "1", "2", "prop" ]
It is possible to set the [[enumerable]]
attribute of an element to false
, but this has only effect on
constructs that enumerate.
const myArray = [1, 2, 3];
const elmArr = [];
Object.defineProperty(myArray, '3', {
value: 4,
writable: true,
enumerable: false,
configurable: true
});
console.log( myArray[3] ); // logs: 4
myArray.forEach(element => elmArr.push(element));
console.log(elmArr); // logs: [ 1, 2, 3, 4 ]
elmArr.length = 0;
for (const element of myArray) { elmArr.push(element) }
console.log(elmArr); // logs: [ 1, 2, 3, 4 ]
elmArr.length = 0;
// Property enumeration:
for (const key in myArray) { elmArr.push(myArray[key]) };
console.log(elmArr); // logs: [ 1, 2, 3 ]
Some operations will skip empty slots, others will treat empty slots as undefined
and yet others will recognize the empty slot:
const myArray = [1, , 3]; // sparse array
const elmArr = [];
console.log(myArray[1]); // Logs: "undefined"
// empty slots are treated as undefined:
for (let i = 0; i < myArray.length; i++) { elmArr.push(myArray[i]) }
console.log(elmArr); // logs: [ 1, undefined, 3 ]
elmArr.length = 0;
for (let element of myArray) { elmArr.push(element) }
console.log(elmArr); // logs: [ 1, undefined, 3 ]
elmArr.length = 0;
const copiedArray = [...myArray];
console.log(copiedArray); // logs: [ 1, undefined, 3 ]
// empty slots are skipped:
myArray.forEach(element => elmArr.push(element));
console.log(elmArr); // logs: [ 1, 3 ]
elmArr.length = 0;
[1, undefined, 3].forEach(element => elmArr.push(element));
console.log(elmArr); // logs: [ 1, undefined, 3 ]
elmArr.length = 0;
console.log(Object.keys(myArray)); // [ "0", "2" ]
for (const key in myArray) { elmArr.push(myArray[key]) };
console.log(elmArr); // logs: [ 1, 3 ]
const objectSpread = { ...myArray };
console.log(objectSpread); // logs: { 0: 1, 2: 3 }
// empty slots are recognized:
console.log(myArray.slice()); // logs: [ 1, <1 empty slot>, 3 ]
Copying and merging arrays
As mentioned before, in JavaScript arrays are objects. We covered cloning and merging objects earlier in this tutorial. We learned that there is a lot to consider when cloning or merging objects: shallow or deep copying, involving only own or also inherited properties, only enumerable or also non-enumerable properties, string keyed and/or symbol keyed properties, copy prototypes or not, dealing with getters and setters 😵. Copying an array just adds a few more considerations: only elements or also "regular" properties and how to deal with empty slots and multi-dimensional arrays 😵🥴🤪.
You can use for...in
(using property enumeration) to shallow copy an array, copying both elements and "regular" properties,
but not symbol keyed properties and skipping empty slots (see previous section).
In the previous section we already covered a few ways to shallow copy only elements of an array into a new array, like using a for
loop,
a for...of
loop, spread syntax or forEach()
.
Other methods that are able to shallow copy an array (only elements) are
Array.from()
,
Array.prototype.slice()
or
Array.prototype.concat()
.
Array.from()
and Array.slice()
can also create an adapted copy.
const myArray = [1, , 3]; // sparse array
let copiedArray = [];
copiedArray = Array.from(myArray);
console.log(copiedArray); // logs: [ 1, undefined, 3 ]
// create an adapted copy:
console.log(Array.from(myArray, x => x + x)); // logs: [ 2, NaN, 6 ]
copiedArray = myArray.slice();
console.log(copiedArray); // logs: [ 1, <1 empty slot>, 3 ]
copiedArray = myArray.concat();
console.log(copiedArray); // logs: [ 1, <1 empty slot>, 3 ]
BTW. Note that there are more methods available to create a (adapted) shallow copy of an array like
Array.prototype.filter()
or
Array.prototype.reduce()
.
BTW. Array.from()
can be used to create a (adapted) shallow-copied Array
from any
iterable or
array-like object.
Spread syntax
A very convenient way to shallow copy or merge arrays is spreading elements of an array into a new array. This uses the iterator of the array, which means that only elements and no other properties are copied.
We also used spread syntax to spread properties from a provided object into a new object to make a shallow copy of an object. In this case it copies only own enumerable properties. Also spreading an array into an object uses property enumeration, not the array's iterator.
const myArray = [1, 2, 3];
myArray.prop = "enumValue";
const copiedArray = [...myArray];
console.log(copiedArray); // logs: [ 1, 2, 3 ]
const objectSpread = { ...myArray };
console.log(objectSpread); // logs: { 0: 1, 1: 2, 2: 3, prop: "enumValue" }
const myArray = [1, , 3]; // sparse array
const copiedArray = [...myArray];
console.log(copiedArray); // logs: [ 1, undefined, 3 ]
Merging arrays:
const arr1 = [0, 1, 2];
const arr2 = [3, 4, 5];
const arr3 = arr1.concat(arr2);
const arr4 = [...arr1, ...arr2];
console.log(arr3, arr4); // logs: [ 0, 1, 2, 3, 4, 5 ] [ 0, 1, 2, 3, 4, 5 ]
let arr1 = ["pit", "put", "pet"];
const arr2 = ["hot", "hat"];
arr1 = ["pot", "pat", ...arr1, ...arr2, "hit", "hut"];
console.info(arr1); // logs: [ "pot", "pat", "pit", "put", "pet", "hot", "hat", "hit", "hut" ]
BTW. Note that in the last example arr1
gets assigned a new array value,
instead of modifying the original arr1
array.
You can use Array.prototype.push()
to append a number of elements to an array. It adds its arguments to the end of an array.
If you pass an array to push()
, it will add that array as a single element, instead of adding the elements individually.
As mentioned earlier, we can use Function.prototype.apply()
as an equivalent for spreading an array in function parameters.
So, we can use apply()
or spread syntax on method push()
to spread an array into another array as an alternative for only using the spread syntax.
However, now it appends to the existing array!
let arr1 = ["pit", "put", "pet"];
const arr2 = ["hot", "hat"];
arr1.push.apply(arr1, arr2);
console.info(arr1); // logs: [ "pit", "put", "pet", "hot", "hat" ]
let arr1 = ["pit", "put", "pet"];
const arr2 = ["hot", "hat"];
arr1.push(...arr2);
console.info(arr1); // logs: [ "pit", "put", "pet", "hot", "hat" ]
Array destructuring and rest elements
Also with array destructuring (see next chapter) and rest elements you can create a (altered) shallow copy.
const myArray = [1, , 3]; // sparse array
const [...copiedArray] = myArray;
console.log(copiedArray) // logs: [ 1, undefined, 3 ]
const myArray = [1, 2, 3];
const [a, ...newArray] = myArray;
console.log(newArray) // logs: [ 2, 3 ]
Deep cloning
All discussed methods above create shallow copies, meaning that copying goes one level deep.
If an element yet again is a reference to an array (or other object), both the original and the copy share the same lower level elements.
This may be undesirable when copying nested arrays.
You can create deep copies using the structuredClone()
method
(which also copies possible "regular" properties).
const myArray = [[1,2], 3, 4];
const copiedArray = [...myArray];
console.log( myArray[0] ); // logs: [ 1, 2 ]
console.log( copiedArray[0] ); // logs: [ 1, 2 ]
copiedArray[0][0] = "altered";
console.log( myArray[0] ); // logs: [ "altered", 2 ]
console.log( copiedArray[0] ); // logs: [ "altered", 2 ]
const myArray = [[1,2], 3, 4];
const deepCopiedArray = structuredClone(myArray);
console.log( myArray[0] ); // logs: [ 1, 2 ]
console.log( deepCopiedArray[0] ); // logs: [ 1, 2 ]
deepCopiedArray[0][0] = "altered";
console.log( myArray[0] ); // logs: [ 1, 2 ]
console.log( deepCopiedArray[0] ); // logs: [ "altered", 2 ]