Written by Ohans Emmanuel✏️
The latest version of the JavaScript language standard is ECMAScript 2023, which is the 14th edition. This update includes new methods on the Array
prototype.
I’ll guide you through the prominent four new methods in this article, including their behavior with sparse arrays and array-like objects. If you’re a fan of a declarative, functional style of writing JavaScript programs, you’re in for a treat.
Dive in:
- Is it important to preserve the original array without any mutations?
- The
toReversed()
method - The
toSorted()
method - The
toSpliced(start, deleteCount, ...items)
method - The
with(index, value)
method
Is it important to preserve the original array without any mutations?
A common theme with the four new array methods is the focus on not mutating the original array, but returning a completely new array. You may wonder, why is this behavior significant?
Generally speaking, there are numerous advantages to leaving data unmodified, as demonstrated by these four new array methods. These benefits are not limited to arrays, but rather extend to all JavaScript objects.
Although there are many benefits, some of the most significant ones are outlined below:
- Pure functions: In functional programming, pure functions are functions that always produce the same output when given the same input: they don’t have any side effects and their behavior is predictable. Working with this functional mental model is ideal when you're not modifying data, and these four new array methods are a great addition for this reason
- Predictable state management: Creating new copies of our state object (or array) makes state management more predictable by eliminating unexpected changes and representing the data at a specific point in time with new copies. This simplifies managing the state at scale and improves reasoning about state management in general
- Change detection: Frameworks like React use simplified change detection by comparing two copies of the state or props object to identify any alterations and render the user interface accordingly. Detecting changes becomes simpler with these methods, as we can compare the two objects at any given moment to identify any alterations
The toReversed()
method
The toReversed()
method is similar to the classic reverse()
method, but with a significant distinction. toReversed()
reverses the elements in an array, without mutating the original array.
Consider the following array of fruits below:
const fruits = ["🍎apple", "🍊orange", "🍌banana"]
Now, reverse fruits
with .reverse()
:
// Reverse the array
const result = fruits.reverse()
console.log(result)
// ['🍌banana', '🍊orange', '🍎apple']
console.log(fruits)
// ['🍌banana', '🍊orange', '🍎apple']
// ↗️ original array is mutated
With reverse()
, the original array is mutated.
To reverse the array without mutating it, we can use the toReversed()
method as demonstrated below:
// Reverse the array
const result = fruits.toReversed()
console.log(result)
// ['🍌banana', '🍊orange', '🍎apple']
console.log(fruits)
// ["🍎apple", "🍊orange", "🍌banana"]
// ↗️ original array is preserved
Voilà!
If you’re using the latest version of a current browser like Chrome, you can access your browser console and test out the code examples provided in the article:
Behavior with sparse arrays
For a quick refresher, sparse arrays are arrays without sequential elements. For example, consider the following:
const numbers = [1,2,3]
// Assign an item to index 11
numbers[11] = 12
console.log(numbers)
// [1, 2, 3, empty × 8, 12]
In the example above, numbers
has eight empty item slots. numbers
is a sparse array. Now, back to toReversed()
. How does this work with sparse arrays?
toReversed()
never returns a sparse array. If the original array had empty slots, they would be returned as undefined
.
Consider calling toReversed()
on the numbers
array below:
const numbers = [1,2,3]
// Assign an item to index 11
numbers[11] = 12
numbers.toReversed()
// [12, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 3, 2, 1]
As expected, all empty slots are returned as undefined
array item values.
Behavior with array-like objects
Even though toReversed()
exists specifically on the Array
prototype, it may also be invoked on array-like objects.
An array-like object typically has a length
property and, optionally, properties with integer index names. String objects are an example of array-like objects.
The function toReversed()
first reads the length
property of the object it is called on and then iterates through the integer keys of the object from the end to the start, which means from length - 1
to 0
. It adds the value of each property to the end of a new array, which is then returned.
Let’s give this a try. Consider the wrong application of toReversed()
on a string:
const s = "Ohans Emmanuel"
// call `toReversed` directly on the string
s.toReversed()
//Uncaught TypeError: s.toReversed is not a function
Even though a string object is an array-like object, this program is wrong: we cannot invoke it in the manner string.toReversed()
because toReversed
doesn’t exist on the string
prototype.
However, we may use the call()
method as shown below:
const s = "Ohans Emmanuel"
// Array.prototype.toReversed.call(arrayLike)
Array.prototype.toReversed.call(s)
//['l', 'e', 'u', 'n', 'a', 'm', 'm', 'E', ' ', 's', 'n', 'a', 'h', 'O']
How about a self-constructed, array-like object? Consider the example below:
// Has a length property and integer index property.
const arrayLike = {
length: 5,
2: "Item #2"
}
If this were a standard array, it would be a sparse array, i.e., of length five and a value in the second index.
Consider the result of calling toReversed
on this:
console.log(Array.prototype.toReversed.call(arrayLike))
// [undefined, undefined, 'Item #2', undefined, undefined]
The toReversed()
function produces a reversed array without creating a sparse array. As expected, the empty slots are returned as undefined
.
The toSorted()
method
.toSorted()
is the counterpart to the classic .sort()
method.
As you may have guessed, unlike .sort()
, .toSorted()
will not mutate the original array. Consider the basic sort operation with .sort()
below:
const list = [1, 5, 6, 3, 7, 8, 3, 7]
//Sort in ascending order
const result = list.sort()
console.log(result)
// [1, 3, 3, 5, 6, 7, 7, 8]
console.log(list)
// [1, 3, 3, 5, 6, 7, 7, 8]
As shown above, sort()
sorts the array in place and consequently mutates the array. Now, consider the same with toSorted()
:
const list = [1, 5, 6, 3, 7, 8, 3, 7]
// Sort in ascending order
const result = list.toSorted()
console.log(result)
// [1, 3, 3, 5, 6, 7, 7, 8]
console.log(list)
// [1, 5, 6, 3, 7, 8, 3, 7]
As seen above, toSorted()
returns a new array with the elements sorted.
Note that toSorted()
retains the same syntax as sort()
. For example, we may specify a function defining the sort order e.g., list.toSorted(compareFn)
.
Consider the example below:
const list = [1, 5, 6, 3, 7, 8, 3, 7]
//Sort the array in descending order
list.toSorted((a,b) => a < b ? 1 : -1)
// [8, 7, 7, 6, 5, 3, 3, 1]
Behavior with sparse arrays
Empty slots will always be returned as undefined
. In fact, they are treated as if they had a value of undefined
. However, the compareFn
will not be invoked for these slots and they’ll always come at the end of the returned array.
Consider the following example with an array with an empty first slot:
// Note the empty initial slot
const fruits = [, "🍎apple", "🍊orange", "🍌banana"]
console.log(fruits.toSorted())
// ['🍊orange', '🍌banana', '🍎apple', undefined]
This behavior is identical to what would happen if the initial value were undefined
.
Consider the example below:
const fruits = [undefined, "🍎apple", "🍊orange", "🍌banana"]
console.log(fruits.toSorted())
// ['🍊orange', '🍌banana', '🍎apple', undefined]
Also, note that the empty slots (or undefined
slots) will always be moved to the end of the returned array, regardless of their position in the original array.
Consider the following example:
// empty slot is in index 2
const fruits = ["🍎apple", "🍊orange", , "🍌banana"]
console.log(fruits.toSorted())
// returned last
// ['🍊orange', '🍌banana', '🍎apple', undefined]
// undefined value is in index 2
const otherFruits = ["🍎apple", "🍊orange", undefined , "🍌banana"]
console.log(otherFruits.toSorted())
// returned last
// ['🍊orange', '🍌banana', '🍎apple', undefined]
Behavior with array-like objects
When using the toSorted()
function with objects, it will first read the length
property of the this
object. It will then collect the object's integer keys from the start to the end, which is from 0
to length - 1
. After sorting them, it will return the corresponding values in a new array.
Consider the following example with a string:
const s = "Ohans Emmanuel"
// Array.prototype.toSorted.call(arrayLike)
Array.prototype.toSorted.call(s)
(14) [' ', 'E', 'O', 'a', 'a', 'e', 'h', 'l', 'm', 'm', 'n', 'n', 's', 'u']
Consider the following example with a constructed array-like object:
// Has a length property and integer index property.
const arrayLike = {
length: 5,
2: "Item #2"
10: "Out of bound Item" // This will be ignored since the length is 5
}
console.log(Array.prototype.toSorted.call(arrayLike))
// ['Item #2', undefined, undefined, undefined, undefined]
The toSpliced(start, deleteCount, ...items)
method
.toSpliced()
is the counterpart to the classic .splice()
method. As with the other new methods we’ve covered, toSpliced()
will not mutate the array it is invoked on, unlike .splice()
.
The syntax for toSpliced
is identical to .splice
, as shown below:
toSpliced(start)
toSpliced(start, deleteCount)
toSpliced(start, deleteCount, item1)
toSpliced(start, deleteCount, item1, item2, itemN)
Add a new array item with the classic .splice()
, as shown below:
const months = ["Feb", "Mar", "Apr", "May"]
// Insert item "Jan" at index 0 and delete 0 items
months.splice(0, 0, "Jan")
console.log(months)
// ['Jan', 'Feb', 'Mar', 'Apr', 'May']
splice()
inserts the new array item and mutates the original array. To create a new array without mutating the original array, use toSpliced()
.
Consider the example above rewritten to use toSpliced()
:
const months = ["Feb", "Mar", "Apr", "May"]
// Insert item "Jan" at index 0 and delete 0 items
const updatedMonths = months.toSpliced(0, 0, "Jan")
console.log(updatedMonths)
// ['Jan', 'Feb', 'Mar', 'Apr', 'May']
console.log(months)
// ['Feb', 'Mar', 'Apr', 'May']
toSpliced()
returns a new array without mutating the original array. Note how the syntax for both toSpliced()
and splice()
are identical.
Behavior with sparse arrays
toSpliced()
never returns a sparse array. As such, empty slots will be returned as undefined
. Consider the example below:
const arr = ["Mon", , "Wed", "Thur", , "Sat"];
// Start at index 1, and delete 2 items
console.log(arr.toSpliced(1, 2));
// ['Mon', 'Thur', undefined, 'Sat']
Behavior with array-like objects
With array-like objects, toSpliced
gets the length of the this
object, reads the integer key needed, and writes the result to a new array:
const s = "Ohans Emmanuel"
// Start at index 0, delete 1 item, insert the other items
console.log(Array.prototype.toSpliced.call(s, 0, 1, 2, 3));
// [2, 3, 'h', 'a', 'n', 's', ' ', 'E', 'm', 'm', 'a', 'n', 'u', 'e', 'l']
The with(index, value)
method
The .with()
array method is particularly interesting. First, consider the bracket notation for changing the value of a specific array index:
const favorites = ["Dogs", "Cats"]
favorites[0] = "Lions"
console.log(favorites)
//(2) ['Lions', 'Cats']
With the bracket notation, the original array is always mutated. .with()
achieves the same result of inserting an element in a specific index, but does not mutate the array. Instead, it returns a new array with the replaced index.
Let’s rewrite the initial example to use .with()
:
const favorites = ["Dogs", "Cats"]
const result = favorites.with(0, "Lions")
console.log(result)
// ['Lions', 'Cats']
console.log(favorites)
// ["Dogs", "Cats"]
Behavior with sparse arrays
with()
never returns a sparse array. As such, empty slots will be returned as undefined
:
const arr = ["Mon", , "Wed", "Thur", , "Sat"];
arr.with(0, 2)
// [2, undefined, 'Wed', 'Thur', undefined, 'Sat']
Behavior with array-like objects
Similar to other methods, with()
reads the length
property of the this
object. It then reads every positive integer index (less than the length
) of the object. As these are accessed, it saves their property values to the return array index.
Finally, the index
and value
in the call signature with(index, value)
are set on the returned array. Consider the example below:
const s = "Ohans Emmanuel"
// Set the value of the first item
console.log(Array.prototype.with.call(s, 0, "F"));
// ['F', 'h', 'a', 'n', 's', ' ', 'E', 'm', 'm', 'a', 'n', 'u', 'e', 'l']
Conclusion
The ECMAScript standard keeps improving, and taking advantage of its new features is a good idea. Go ahead and leverage toReversed
, toSorted
, toSpliced
, and with
to create more declarative JavaScript applications.
LogRocket: Debug JavaScript errors more easily by understanding the context
Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.
LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Top comments (5)
I love
with()
function. I’m gonna have to look into that more.Very instructive, thank you :-)
Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍
Thank you
Thank you for your clear explanation 👍