Data manipulation essentially means adjusting data to make it easier to read, understand and use. Whether you’re rendering UI, fetching data from an API, or reading from files, you will have to manipulate data in all sorts of JavaScript applications.
If you’ve worked enough on JavaScript projects, you’ll notice that it’s very common to see data in the form of arrays. Surely there are tons of resources covering JavaScript arrays out there, and anyone reading this article probably knows how to perform basic operations on arrays. Nevertheless, the main purpose of this article is to introduce you to different JavaScript array methods and practices that can be utilized to achieve simpler and cleaner code.
Starting with the basics
In this section, I will briefly mention some of the common static and instance array methods used to create, access, or mutate arrays. The content of this section might sound very basic or high-level for most readers, yet, it is important to cover it before diving deeper into more advanced topics.
Static methods
These methods are used to create new arrays or convert existing iterable and array-like objects to arrays.
// New array
Array.of("🍏", "🍌", "🍒"); // ["🍏", "🍌", "🍒"]
Array(3).fill("⭐️"); // ["⭐️", "⭐️", "⭐️"]
// From array-like object
Array.from("hello"); // ["h", "e", "l", "l", "o"]
// From iterable
Array.from([1, 2, 3], (x) => x * 2); // [2, 4, 6]
// From object
const person = { name: "John", age: "23" };
Object.keys(person); // ["name", "age"]
Object.values(person); // ["John", "23"]
Object.entries(person); // [["name", "John"], ["age", "23"]];
Mutation methods
The following methods are instance methods or prototypal methods. They are called on a specific array instance to apply mutations.
const fruits = ["🍏", "🍌", "🍒", "🍑", "🥑"];
// Inserts element at the end
fruits.push("🥭"); // 6
console.log(fruits); // ["🍏", "🍌", "🍒", "🍑", "🥑", "🥭"]
// Removes last element
fruits.pop(); // "🥭"
console.log(fruits); // ["🍏", "🍌", "🍒", "🍑", "🥑"]
// Inserts element at the start
fruits.unshift("🍉"); // 6
console.log(fruits); // ["🍉", "🍏", "🍌", "🍒", "🍑", "🥑"]
// Removes first element
fruits.shift(); // "🍉"
console.log(fruits); // ["🍏", "🍌", "🍒", "🍑", "🥑"]
Other instance methods
These are other pure instance methods that won’t mutate the array.
const tags = ["js", "react", "node", "js"];
tags.join(", "); // "js, react, node, js"
tags.indexOf("react"); // 1
tags.lastIndexOf("js"); // 3
tags.reverse(); // ["js", "node", "react", "js"]
// String to array
"29-10-2021".split("-"); // ["29", "10", "2021"]
Questioning traditional loops
When thinking about arrays, iteration is usually the first word that pops in mind. We iterate over arrays to apply the same logic for each value. At this point, it might seem that anything we want to apply to array members can be achieved using a traditional loop with all its variants (i.e for, for-of, and .forEach() loops).
const fruits = ["🍏", "🍌", "🍒", "🍑", "🥑"]; // yes, emoji arrays are cool
// ✅ A simple loop to print elements
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i]);
}
// ✅✅ A better implementation
// implicit breaking condition but no index variable
for (fruit of fruits) {
console.log(fruit);
}
// ✅✅✅ An even better implementation
// implicit breaking condition with optional access to index
fruits.forEach((fruit, _index) => {
console.log(fruit);
});
// While loops are not included for brevity
But what if we need to do more than that, maybe modify or filter array elements? We can for sure use traditional loops, but can’t we do any better?
const cart = [
{
itemId: "szXwXmq9i0HpHS1_OlxxN",
dateAdded: "06/14/2020 4:41:48 PM UTC",
isItemAvailable: true,
quantity: 2,
price: 10.12,
},
{
itemId: "hKhoBgnD0cccymkGlGHaZ",
dateAdded: "06/14/2020 4:51:50 PM UTC",
isItemAvailable: true,
quantity: 3,
price: 15.01,
},
{
itemId: "EvxqG2f4Edd55DpRdcJ8y",
dateAdded: "06/14/2020 4:56:01 PM UTC",
isItemAvailable: true,
quantity: 1,
price: 24.98,
},
{
itemId: "tJONu8dVSWWTZbbElvZBP",
dateAdded: "06/14/2020 5:00:20 PM UTC",
isItemAvailable: false,
quantity: 1,
price: 35.6,
},
];
Replacing traditional loops
.map()
As the name suggests, Array.protoype.map() maps the elements of the current array to a new array after applying some logic on each element. Let’s say you’ve received the previous cart data which includes UTC timestamps and prices (as numbers), and that you need to localize your data based on the user’s browser settings:
const localizeDate = (utcStr = "") =>
new Date(Date.parse(utcStr)).toLocaleString();
const localizePrice = (price = 0) =>
price.toLocaleString("en-US", { style: "currency", currency: "USD" });
// Don't ❌
let localizedCart = [];
cart.forEach(({ dateAdded, price, ...item }) => {
const localizedItem = {
...item,
dateAdded: localizeDate(dateAdded),
price: localizePrice(price),
};
localizedCart.push(localizedItem);
});
// Do ✅
let localizedCart = cart.map(({ dateAdded, price, ...item }) => ({
...item,
dateAdded: localizeDate(dateAdded),
price: localizePrice(price),
}));
As you can see, the use of .map() saves us from creating an empty array and pushing each modified element into it by providing a much elegant solution.
.filter()
Array.prototype.filter() is another method that returns a new array of elements each satisfying the provided test condition. Using the same cart data, imagine we wanted to filter all elements that have a price greater than or equal to $15.00:
// Don't ❌
let filteredCart = [];
cart.forEach((item) => {
if (Number(item?.price) >= 15) {
filteredCart.push(item);
}
});
// Do ✅
let filteredCart = cart.filter(({ price }) => Number(price) >= 15);
.find() & .findIndex()
Array.prototype.find() and Array.prototype.findIndex() are pretty self-explanatory. The first method returns the value of the first element that satisfies a given test condition, while the second returns the index of this element. In the following example, we are interested in finding the index and the value of the first element whose quantity is less than 2:
// Don't ❌
let maybeItem = null;
let maybeIndex = 0;
cart.forEach((item, index) => {
if (Number(item?.quantity) < 2) {
maybeItem = item;
maybeIndex = index;
return;
}
});
// Do ✅
const isQuantityLessThan2 = ({ quantity }) => Number(quantity) < 2;
let maybeItem = cart.find(isQuantityLessThan2);
let maybeIndex = cart.findIndex(isQuantityLessThan2);
.reduce()
Array.prototype.reduce() is a very useful method that applies some logic to each iteration while accumulating results of all previous iterations. In this context, the “accumulator” could be a new object, array, or a simple variable. In this example, we are interested in calculating the total price of all cart items:
// Don't ❌
let totalPrice = 0;
cart.forEach(({ price, quantity }) => {
totalPrice += price * quantity;
});
// Do ✅
let totalPrice = cart.reduce(
(sum, { price, quantity }) => sum + price * quantity,
0 // This is the initial value of the accumulator (sum)
);
Obviously, .reduce() has much powerful use-cases than computing sums, however, I chose to keep examples simple in this article to cover as much concepts as possible.
.every() & .some()
Array.prototype.every() and Array.prototype.some() check if every or some elements satisfy a test condition respectively. These methods should be used instead of .filter() and .find()— and obviously traditional loops — whenever you’re only interested in a “Yes or No” answer rather than the actual values.
const isItemUnavailable = ({ isItemAvailable }) => !isItemAvailable; // Our test condition
// Don't ❌
let doesContainUnavailableItems = false;
cart.forEach(({ isItemAvailable }) => {
if (!isItemAvailable) {
doesContainUnavailableItems = true;
return;
}
});
// Don't ❌
let doesContainUnavailableItems = cart.filter(isItemUnavailable).length > 0;
// Don't ❌
let doesContainUnavailableItems = Boolean(cart.find(isItemUnavailable));
// Do ✅
let doesContainUnavailableItems = cart.some(isItemUnavailable);
.sort()
The last method to cover in this section is Array.prototype.sort() which takes a comparator function as an argument and returns a sorted array. The default sort order is ascending when no custom comparator is provided.
Chaining methods
- All items should be marked as available
- We only want to present itemId and netPrice
- Items should be sorted in ascending order of quantities
We can easily achieve all the constraints by chaining array methods:
cart
.filter(({ isItemAvailable }) => isItemAvailable) // Get available items
.sort((itemA, itemB) => itemA.quantity - itemB.quantity) // Sort by ascending order of quantity
.map(({ itemId, quantity, price }) => ({
itemId,
netPrice: quantity * price,
})); // Only present itemId and netPrice
// Result:
// [
// { itemId: "EvxqG2f4Edd55DpRdcJ8y", netPrice: 24.98 },
// { itemId: "szXwXmq9i0HpHS1_OlxxN", netPrice: 20.24 },
// { itemId: "hKhoBgnD0cccymkGlGHaZ", netPrice: 45.03 },
// ];
Keep in mind that over-chaining might increase time complexity and affect performance, so make sure you optimize these calls.
Bonus: Using ES6 features
Destructuring
const [apple, banana, cherries] = ["🍏", "🍌", "🍒"];
console.log(apple); // 🍏
console.log(banana); // 🍌
console.log(cherries); // 🍒
Default values
const [apple, banana, cherries, mango = "🥭"] = ["🍏", "🍌", "🍒"];
console.log(mango); // 🥭
Skipping values
const [apple, , cherries] = ["🍏", "🍌", "🍒"];
console.log(apple); // 🍏
console.log(cherries); // 🍒
Spread operator
const [apple, ...others] = ["🍏", "🍌", "🍒"];
console.log(apple); // 🍏
console.log(others); // ["🍌", "🍒"]
0 Comments