10 JavaScript Tricks You Didn’t Know
JavaScript, being one of the most popular languages, continually evolves with new features, often leaving developers unaware of some powerful yet underutilized tricks. Let’s explore some of these lesser-known JavaScript features and techniques:
1. Utilizing flatMap
The flatMap()
array method, a fusion of map()
and flat()
, efficiently handles flattening arrays up to one level. Here’s a comparison between using flat()
and map()
separately and flatMap()
:
// Using flat + map
const arr = [1, 2, [4, 5], 6, 7, [8]];
const result = arr.map(element => Array.isArray(element) ? element : [element]).flat();
console.log(result); // [1, 2, 4, 5, 6, 7, 8]
// Using flatMap
const arr = [1, 2, [4, 5], 6, 7, [8]];
console.log(arr.flatMap((element) => element)); // [1, 2, 4, 5, 6, 7, 8]
2. Optimize Array Method Order
When chaining multiple array methods, consider the order for better optimization. For instance, filtering before sorting reduces unnecessary computations:
const numbers = [9, 3, 6, 4, 8, 1, 2, 5, 7];
numbers
.filter((n) => n % 2 !== 0)
.sort((a, b) => a - b)
.map((n) => n ** 3);
3. Leverage reduce
For key-value data grouping, prefer reduce
over forEach
or map
for improved performance, especially with large datasets:
fetch("https://jsonplaceholder.typicode.com/todos/")
.then(res => res.json())
.then(todos => {
// using reduce
const todosForUserMap = todos.reduce((accumulator, todo) => {
if (accumulator[todo.userId]) accumulator[todo.userId].push(todo);
else accumulator[todo.userId] = [todo];
return accumulator;
}, {});
console.log(todosForUserMap);
});
4. Harness Generators
Generators facilitate asynchronous operations and infinite data streams management. They are valuable for scenarios like infinite loading:
async function* fetchProducts() {
while (true) {
const productUrl = "https://fakestoreapi.com/products?limit=2";
const res = await fetch(productUrl);
const data = await res.json();
yield data;
// Update the user interface here
// Or save it in a database or elsewhere
// Use this for side effects
// Interrupt the process if some conditions match
}
}
async function main() {
const itr = fetchProducts();
// This should be called based on the user interaction
// Or other tricks, because you don't want it to load indefinitely.
console.log(await itr.next());
}
return main();
5. Making Good Use of Console
Console is not just about console.log(). In actual production, a well-packaged log library will typically be used. The console object actually has many useful built-in methods, which can help you improve the quality and readability of your debugging output. Mastering them can help you debug and solve problems in your code more easily.
// 1. console.time and console.timeEnd
// Measure the time taken to execute a piece of code. Identify performance bottlenecks in the code and optimize them
console.time('Start fetching data');
fetch('https://reqres.in/api/users')
.then(response => response.json())
.then(data => {
console.timeEnd('Time spent fetching data:');
// ...code
});
// 2. console.dir
// The console.dir method outputs the properties of an object in a layered format. It is convenient to view the structure of the object as well as all its properties and methods
const promise = new Promise((resolve, reject) => resolve('foo'));
console.dir(promise);
// 3. console.count
// Use the console.count method to count the number of times a specific log message is output. This is very useful for tracking the number of times a specific code path is executed and identifying hotspots in the code
const fun = (x) => console.count(x);
fun('Keqing'); // 1
fun('Ganyu'); // 1
fun('Keqing'); // 2
// 4. console.trace
// trace can output stack traces. It is very useful for understanding the execution flow in the code and identifying the source of a specific log message
const foo = () => console.trace();
const bar = () => foo();
bar();
// 5. console.profile profileEnd
// Measure the performance of a block of code. This is very useful for identifying performance bottlenecks and optimizing code to improve speed and efficiency.
console.profile('MyProfile');
// Code you want to measure performance on
for (let i = 0; i < 100000; i++) {
// ...code
}
console.profileEnd('MyProfile');
6. Deep Copy with structuredClone()
Previously, if a developer wanted to do a deep copy of an object, they usually had to rely on a third-party library or manually implement a deep copy, or adopt a hack like const cloneObj = JSON.parse(JSON.stringify(obj));
. But it had a lot of shortcomings when dealing with more complex objects containing circular references or data types that don’t conform to JSON (such as Map and Set, Blob, etc.)
Now, JavaScript has a built-in method called structuredClone()
. This method provides a simple and effective way to deep clone objects and is suitable for most modern browsers and Node.js v17 and above.
// Pass the original object to this function and it will return a deep copy with different references and object attribute references
const obj = { name: 'Osman', friends: [{ name: 'Nuri' }] };
const clonedObj = structuredClone(obj);
console.log(obj.name === clonedObj); // false
console.log(obj.friends === clonedObj.friends); // false
Unlike the well-known JSON.parse(JSON.stringify())
, structuredClone()
allows you to clone circular references, which makes it the simplest method to use deep copy in JavaScript currently.
7. Tagged Templates
Tagged Templates is a more advanced form of template strings (backticks) that allows you to parse template literals using functions.
const checkCurrency = function (currency, amount) {
const symbol = currency[0] === "USD" ? "$" : "¥";
console.log(currency[0], "--" ,currency[1])// Outputs: USD -- RMB
return `${symbol}${amount}`;
};
const amount = 200;
const currency = checkCurrency`USD${amount}RMB`;
console.log(currency); // Outputs: $200
// 1. checkCurrency is a function, the first argument currency of the tagged function contains an array of string values
// 2. The array of strings is composed of the strings in the tagged template in`USD${amount}RMB`, which contains the strings USD and RMB
// 3. So currency[0] refers to the first string USD, and currency[1] corresponds to the second string RMB
// 4. The remaining arguments of the checkCurrency function are then inserted directly into the strings according to the respective expressions, such as amount = 200
// 5. Inside the checkCurrency function, it checks if the first item of the argument array is 'USD', if so, it selects the "$" symbol, otherwise it chooses "¥".
// 6. The function internally concatenates symbol and amount to return a new string, with symbol representing the currency symbol and amount referring to the money passed to the function.
// 7. The returned string is assigned to the currency constant, so the log is $200
Tagged template strings can be used for many purposes, such as security, i18n, and localization, etc.
8. Nullish Coalescing Operator ??
The Nullish Coalescing Operator ??
is a logical operator that returns its right-hand operand when its left-hand operand is null or undefined, otherwise, it returns its left-hand operand.
const foo = null ?? 'default string';
console.log(foo); //output: "default string"
const bar = 0 ?? 'default string'
console.log(bar); //output: 0
// 1. Using 0 as input
const a = 0;
console.log(`a || 10 = ${a || 10}`); // a || 10 = 10
console.log(`a ?? 10 = ${a ?? 10}`); // a ?? 10 = 0
// 2. Using an empty string '' as input
const a = ''
console.log(`a || "ABC" = ${a || "ABC"}`); // a || "ABC" = ABC
console.log(`a ?? "ABC" = ${a ?? "ABC"}`); // a ?? "ABC" =
// 3. Using null as input
const a = null;
console.log(`a || 10 = ${a || 10}`); // a || 10 = 10v
console.log(`a ?? 10 = ${a ?? 10}`); // a ?? 10 = 10
// 4. Using undefined as input
const a = {name: ''}
console.log(`a.name ?? 'varun 1' = ${a.name ?? 'varun 1'}`);
console.log(`a.name || 'varun 2' = ${a.name || 'varun 2'}`);
// a.name ?? 'varun 1' =
// a.name || 'varun 2' = varun 2
// 5. Using false as input
const a = false;
console.log(`a || 10 = ${a || 10}`); // a || 10 = 10
console.log(`a ?? 10 = ${a ?? 10}`); // a ?? 10 = false
9. Use Symbols as Keys in WeakMap
WeakMap is very similar to Map, but the difference is that its keys can only be objects or symbols, which are stored as weak references.
// Map
let user = { name: "User" };
let map = new Map();
map.set(user, "Ketching");
user = null; // Overwrite the reference by setting to null, 'user' is stored internally in the map, which can be accessed through map.keys()
// WeakMap
let user = { name: "User" };
let weakMap = new WeakMap();
weakMap.set(user, "Ganyu");
user = null; // With WeakMap, 'user' has been deleted from memory
let mySymbol = Symbol('mySymbol');
let myWeakMap = new WeakMap();
let obj = {
name: 'Frontend Kitten Writer'
};
myWeakMap.set(mySymbol, obj);
console.log(myWeakMap.get(mySymbol)); // Output: {name: 'Frontend Kitten Writer'}
10. Functional Programming
Since 2015, JavaScript versions have been updated, and this year (2023 ES14) is no exception.
The biggest update in ES14 is that many extra array methods have been added, or complementary methods that don’t cause mutation have been added to the existing array methods. That means they’ll create new arrays based on the original ones rather than directly modifying the original arrays.
The new complementary methods are:
- Array.sort() -> Array.toSorted()
- Array.splice() -> Array.toSpliced()
- Array.reverse() -> Array.toReversed()
The new array methods are:
- Array.with()
- Array.findLast()
- Array.findLastIndex()
The key theme of this year is Simpler Functional Programming (fp) and Immutability.
// For example, with Array.with(), you had to modify a value of an array with arr[2] = 3;
// This causes a mutation. This is impure! Angry💢 But with the new array methods that don't cause mutations, you can write it like this:
const arr = [5, 4, 7, 2, 1];
const replaced = arr.with(2, 3);
console.log(replaced); // [5, 4, 3, 2, 1]
These updates in ES14 aim to promote cleaner, more functional programming practices in JavaScript.
Ref: Shawn King - Medium