Back
Node.jsJavaScriptArticle · Mar 26, 2024 · 16 min

jonathanjuliani

EcmaScript (Javascript and NodeJS) - What's New from ES7 to ES10

JavaScript ES7–ES10 highlights: includes, object spread, flat, fromEntries, and real-world gotchas. Practical guide with code.

Timeline of ECMAScript features from ES7 to ES10 in JavaScript

ES7 to ES10: the features you use every day without knowing where they came from

If you use JS day to day, you've almost certainly used includes(), object spread, and flat()—but do you know which ES version each came from? Probably not. Most developers don't.

ES7 through ES10 landed some of the most practical additions to the language, and knowing where they came from helps you understand their limitations.

ecmascript-7-to-10

This isn't an exhaustive list of every spec change. The goal is the features you'll actually hit in day-to-day work—with the edge cases the docs tend to skip over.

What you'll learn:

  • Array.includes() and String.includes() — and the gotcha with objects that trips everyone up
  • Object.values() and Object.entries() — and their ES10 sibling that reverses them
  • Object spread operator — why it replaced Object.assign for good
  • Promise.finally() and for await...of — where each one fits
  • flat(), flatMap(), Object.fromEntries(), trimStart/End, and optional catch binding
  • Why stable sort() in ES10 matters more than you'd expect

Prerequisites: comfortable with JavaScript basics — functions, arrays, objects, and a rough idea of what a Promise is.


ES7: includes, exponentiation operator

The new features in ES7 are:

  • includes method on Arrays
  • includes method on Strings
  • Exponentiation operator **

Wait, you've been using includes all this time and didn't realise it was "that recent"? Yeah, same. Let's code it up—it's simple:

includes() returns true or false depending on whether the string contains the character, word, or phrase you pass in. Case-sensitive.

First example with strings:

code
// testing includes() with strings
 
const phrase = 'Hello world'
 
let result = phrase.includes('w') // returns true
console.log(result)
 
// it's case sensitive
result = phrase.includes('L') // returns false
console.log(result)
 
result = phrase.includes('x') // returns false
console.log(result)

Now with arrays:

code
const animals = ['dog', 'cat', 'bear']
 
result = animals.includes('dog')
console.log(result) // returns true
 
result = animals.includes('me')
console.log(result) // returns false — yep, I'm not an animal :D
 
const numbers = [1, 2, 3, 4, 5]
 
result = numbers.includes(3)
console.log(result) // returns true
 
result = numbers.includes(0)
console.log(result) // returns false
 
// let's make it more interesting:
 
const arrayOfObjects = [
  { id: 1, desc: 'Obj 1' },
  { id: 2, desc: 'Obj 2' },
]
 
result = arrayOfObjects.includes(4) // ?
console.log(result) // false — expected
 
result = arrayOfObjects.includes({}) // ?
console.log(result) // also false
 
result = arrayOfObjects.includes({ id: 1, desc: 'Obj 1' })
console.log(result) // false — wait, WHAT?!

includes works great until we start dealing with objects inside arrays.

But it makes sense if you think about it: includes does a simple equality check, and comparing objects requires deeper logic (checking each key and value). We'd need a different approach for that—topic for another post.

Now the exponentiation operator, also very simple:

code
// exponentiation
 
const squareOf = (num) => num ** 2
console.log(squareOf(5)) // returns 25
 
const cubeOf = (num) => num ** 3
console.log(cubeOf(5)) // returns 125

To raise a number to a power, use ** followed by the exponent. Clean and readable.

ES8: padStart, padEnd, Object.values, Object.entries

Here are the main features from ES8 that you might have already used without realising they came from this release:

  • String padding with padStart and padEnd
  • Object.values
  • Object.entries

padStart and padEnd

padStart and padEnd add padding to the beginning or end of a string:

code
const name = 'Jonathan'
 
console.log(name.padStart(10))
//  Jonathan  // <= 10-character padding at the start
 
console.log(name.padEnd(10))
// Jonathan   // <= 10-character padding at the end

Object.values

Object.values returns all the values of an object's keys as an array:

code
const obj = {
  id: 1,
  name: 'test',
  desc: 'test description',
}
 
const values = Object.values(obj)
console.log(values)
// [ 1, 'test', 'test description' ]

Super simple, right? So now when you have an object and just need its values, you know what to reach for.

Object.entries

And its bigger sibling Object.entries returns all key/value pairs as an array of [key, value] tuples:

code
const obj = {
  id: 1,
  name: 'test',
  desc: 'test description',
}
 
const entries = Object.entries(obj)
console.log(entries)
 
//  [
//    [ 'id', 1 ],
//    [ 'name', 'test' ],
//    [ 'desc', 'test description' ]
//  ]

With both of these methods, you can easily convert an object into something you can iterate over—then throw a for, map, reduce, or whatever you need at it.

Top-tier utilities. If you have a cool real-world example using these, drop it in the comments.

ES9: spread in objects, finally, for await

A few highlights from ES9:

  • Spread operator on objects
  • finally
  • for await

Spread in objects: replacing Object.assign

Spread already worked well with arrays, but with objects it wasn't great until ES9. Now we can destructure an object—pull out what we want and throw the rest into a ...rest variable:

code
// object spread operator
 
const obj = {
  id: 1,
  name: 'Jonathan',
  bio: 'Fullstack developer — lorem ipsum I got tired of writing',
}
 
const { id, ...rest } = obj
 
console.log(id)
// 1
 
console.log(rest)
//  {
//    name: 'Jonathan',
//    bio: 'Fullstack developer — lorem ipsum I got tired of writing'
//  }

We can also create a shallow copy of an object using spread, without needing Object.assign:

code
const obj2 = { ...obj }
console.log(obj2)
 
// { id: 1,
//   name: 'Jonathan',
//   bio: 'Fullstack developer — lorem ipsum I got tired of writing' }

And the position of the key doesn't matter—you can extract any key from anywhere in the object, and the rest lands in ...rest:

code
const { name, ...restOfObj } = obj
console.log(name)
// Jonathan
console.log(restOfObj)
// { id: 1,
//   bio: 'Fullstack developer — lorem ipsum I got tired of writing' }

finally: the block that always runs

If you've worked in other languages, you've probably seen finally. It took a while, but it's here in JavaScript now. You can use it in try/catch blocks and promise chains.

In the example below, I fetch from a basic API and use finally to log a string—notice that finally executes even before the fetch completes:

code
const url = 'https://jsonplaceholder.typicode.com/users'
 
const fetchUsers = async () => {
  const data = await fetch(url)
    .then((data) => console.log(data))
    .catch((err) => console.log('Oops, something went wrong...'))
    .finally(console.log('No matter what happens, I run.'))
}
 
fetchUsers()

for await...of: iterating promises in sequence

This one is a bit trickier to get at first.

The idea: you need to await multiple async operations in a loop. You could do Promise.all(fetch(url1), fetch(url2), ...), or you could write out 10 await statements one by one (please don't), or you use for await:

code
// 3 URLs:
const urls = [
  'https://jsonplaceholder.typicode.com/users',
  'https://jsonplaceholder.typicode.com/posts',
  'https://jsonplaceholder.typicode.com/albums',
]
 
// fetching all 3 using for await
const fetchData = async (urls) => {
  const fetchArray = urls.map((url) => fetch(url))
  for await (const result of fetchArray) {
    const data = await result.json()
    console.log(data)
  }
}
 
fetchData(urls)

ES10: flat, fromEntries, trimStart/End, stable sort

ES2019 additions:

  • flat and flatMap
  • Object.fromEntries
  • trimStart and trimEnd
  • catch{} without a parameter
  • sort() is always stable now

flat: flattening nested arrays

Quick note: some features like flat() don't work in older online repls, so test this one in your browser console (as long as it's not from 1997).

flat() is a new method on the Array prototype. It takes nested arrays (arrays inside arrays) and "flattens" them into the parent array. Simple example:

code
const array = [1, 2, [10, 20]]
console.log(array.flat())
// [1, 2, 10, 20]

By default flat() only goes one level deep. But you can pass it a depth, or Infinity to flatten everything no matter how deep:

code
const array = [
  1,
  2,
  [10, 20, [100, 200, [1000, 2000, [1, 4, 5, 6], [134, 1234, 5666, 77, [12, 15, 6]]]]],
  [50, 38, 80],
]
console.log(array.flat(Infinity))
// [1, 2, 10, 20, 100, 200, 1000, 2000, 1, 4, 5, 6, 134, 1234, 5666, 77, 12, 15, 6, 50, 38, 80]

flatMap

flatMap is like doing a map() followed by flat(1). You iterate each item, do something with it, and the result is automatically flattened one level:

code
const array = [1, 2, 3, 4]
console.log(array.flatMap((value) => [value, value * 2]))
 
// [1, 2, 2, 4, 3, 6, 4, 8]

In the example above: for each item we return [item, item * 2]. Normally this would give us an array of arrays—but flatMap flattens that one level, giving us a single flat array. I'll leave you to play around with this one.

Object.fromEntries: the entries() companion

Remember Object.entries() which gives you all key/value pairs as an array? Object.fromEntries() does the exact opposite—it takes an array of [key, value] pairs and turns them back into an object:

object-from-entries

Nice, right? This is super useful when a backend or database returns data as arrays of pairs and you want to convert them into a proper object to work with.

trimStart and trimEnd

According to the ECMAScript spec, trimStart and trimEnd are better-named alternatives to the existing trimLeft and trimRight. Same function—strip whitespace from the start or end of a string:

trimstart-trimend-javascript

Note: I used JSON.stringify() in the console.log to make the spaces visible at the start and end of the string. You don't need any JSON utilities in your actual code.

catch without a parameter

Admit it: you've written console.log(err) inside a catch just to stop the linter from complaining about an unused variable.

We've all been there. But now you can skip the parameter entirely:

code
try {
  // something risky
} catch {
  // handle the error without declaring `err`
}

Clean.

sort() is always stable now: why it matters

I won't go deep into sorting algorithm theory here, but a quick concept you need:

A stable sort preserves the relative order of elements that are considered equal. An unstable sort may swap equal elements around.

array-sort-stable

In the image: in an unstable sort, A and B (equal in value) might swap positions—same with D and C. In a stable sort, they stay in the original order.

From ES10 onward, Array.sort() is required to be stable by the spec. If you had bugs related to unstable sort behaviour before—no more worrying.

Want to know more about stable vs unstable sorts? Drop it in the comments.


TL;DR — ES7 to ES10 quick reference

FeatureVersionWhat it doesGotcha
Array.includes()ES7Check if value exists — returns booleanReference comparison: doesn't work with objects
String.includes()ES7Check if substring existsCase-sensitive
** (exponentiation)ES7Raises to a power
padStart / padEndES8Pads a string to a target length
Object.values()ES8Array of the object's values
Object.entries()ES8Array of [key, value] pairs
Object spread ...ES9Copies / destructures objectsShallow copy — nested objects still share references
Promise.finally()ES9Runs always, with or without error
for await...ofES9Iterates promises in sequenceSequential, not parallel like Promise.all
flat()ES10Flattens nested arraysOnly 1 level by default — use flat(Infinity)
flatMap()ES10map() + flat(1) combinedOnly flattens 1 level
Object.fromEntries()ES10Array of pairs → objectReverse of Object.entries()
trimStart / trimEndES10Removes whitespace from start / endAliases for trimLeft / trimRight
catch {} no paramES10Omits the err param when unused
sort() stableES10Preserves relative order of equal elementsBehaviour was inconsistent across browsers before

Next steps

Want to go deeper on some of these features?


Before you go

If you spotted a mistake or want to add something, let me know.

Let's share knowledge and code—best way to learn and actually remember things.

Subscribe to the Engineering Ledger

Get architecture and performance notes in your inbox. Same list as the timed prompt—subscribe here anytime.

No spam. No third-party APIs. Just me sending updates.

The Engineering Ledger

Bi-weekly transmissions on architecture, performance, and practical engineering. Subscribe from any article—no spam.