So, what’s new in ES2022?
A presentation by Christophe Porteneuve at Confoo Montréal 2020
A presentation by Christophe Porteneuve at Confoo Montréal 2020
const christophe = {
family: { wife: 'Élodie', sons: ['Maxence', 'Elliott'] },
city: 'Paris, FR',
company: 'Delicious Insights',
trainings: ['360° ES', 'Modern Web Apps', 'Node.js', '360° Git', 'Webpack'],
webDevSince: 1995,
mightBeKnownFor: [
'Prototype.js',
'Prototype and Script.aculo.us',
'dotJS',
'Paris Web',
'NodeSchool Paris',
],
}
ECMA is an international standards body
(like ISO, IETF, W3C or WHATWG, to name a few)
ES = ECMAScript. The official standard for JavaScript*
TC39 = Technical Committee 39. Caretaker of several standards:
ECMAScript (ECMA-262), Intl
(ECMA-402), JSON (ECMA-404), etc.
Meetings every two months, mostly in the U.S. Yearly release in June.
“ES6” = ES2015, “ES7” = ES2016, and now we say ES2020, etc.
This is all transparent and public.
Stage | Description |
---|---|
0 Strawman | “Say, it’d be nifty to get a Unicorn (🦄) operator to…” |
1 Proposal | A TC39 member becomes the proposal’s “champion.” The general shape of the API is defined, and most of the cross-cutting concerns are handled. |
2 Draft | The initial spec text is done, and covers all critical aspects and the tech semantics. |
3 Candidate | The spec is complete, duly reviewed and approved. The API is finalized and no stone is left unturned. |
4 Finished | Full Test262 coverage, 2+ shipped implementations (usually v8 and Spidermonkey), significant real-world feedback, and imprimatur by the Spec Editor. Will then be part of the next feature freeze (January-March), hence ship in the associated yearly release. |
class OldSchoolHOC extends Component {
render() {
// Rest on properties
const { component: Component, ...props } = this.props
return (
// …
<Component {...props} />
)
}
}
// Spread on properties
const DEFAULTS = { first: 'John', last: 'Doe' }
const person = { last: 'Smith', age: 42 }
const result = { ...DEFAULTS, ...person, age: 36 }
// => { first: 'John', last: 'Smith', age: 36 }
// Flag: “dotAll” / “singleLine”
'<p>line 1\nline 2\nline 3</p>'.match(/<p>.*<\/p>/s) // => '<p>…</p>'
// Named captured groups
const ATTR = /\s+(?<name>\w+)=(?<quote>['"])(?<value>.+?)\k<quote>/
const { name, quote, value } = '<p class="foo"></p>'.match(ATTR).groups
// => name === 'class', quote === '"', value === 'foo'
'<p class="foo"></p>'.replace(ATTR, '[$<name>=$<value>]')
// => '<p[class=foo]></p>'
// Lookbehinds (here a positive one)
'$10.53'.match(/(?<=\$)\d+(?:\.\d+)?/) // => ['10:53', …]
// Unicode Property Escapes
'By Ζεύς!'.match(/\p{Script=Greek}+/u)[0] // => 'Ζεύς'
Promise
#finally
Invoked regardless of the promise chain’s outcome (fulfilled or rejected), and does not alter that state.
So very much like the finally
block for a try
.
function doSomeOldSchoolAsync() {
return Promise.resolve()
.then(setUp)
.then(onSuccess, onError)
.finally(anyway)
}
A promise-based variation of the synchronous iteration from ES2015.
Instead of implementing Symbol.iterator
, we implement Symbol.asyncIterator
.
Easily consumable using the new for await (… of …)
Node (10+) and WHATWG streams (e.g. Fetch) are asynchronous iterables.
async function echoLines(filename) {
const input = createReadStream(filename, 'utf-8')
for await (const line of input) {
console.log(`> ${line}`)
}
}
catch
bindingSometimes you don’t need the error…
function safeRequire(pathspec) {
try {
require(pathspec)
} catch {
debug(`Could not load optional dependency ${pathspec}`)
}
}
Object.fromEntries
The inverse of Object.entries
: builds an object from its key/value pairs.
function pick(src, ...propertyNames) {
return Object.fromEntries(
Object.entries(src).filter(([name]) => propertyNames.includes(name))
)
}
String
#trimStart/End
Finer-grained variations of ES2015’s trim()
that let us pick an extremity of the string.
const spaceyText = '\n\t hello world!\u00a0\n\n'
spaceyText.trimStart()
// => 'hello world!\u00a0!\n\n'
spaceyText.trimEnd()
// => '\n\t hello world!'
Array
#flat/flatMap
flat(depth = 1)
“flattens” nested arrays up to a given depth.
const data = ['Alice', ['Bob', 'Claire'], ['David', ['Erin', 'Fred'], 'Georges']]
data.flat() // => ['Alice', 'Bob', 'Claire', 'David', ['Erin', 'Fred'], 'Georges']
data.flat(2) // => ['Alice', 'Bob', 'Claire', 'David', 'Erin', 'Fred', 'Georges']
flatMap(mapper[, thisArg])
runs a map
whilst flattening the result on the fly, at depth 1.
const data = ['Hey 😘 wassup? 😁', 'HBD 🎂 sweetheart 💖']
const extractEmojis = (text) => text.match(/\p{Emoji}/gu)
data.map(extractEmojis) // => [['😘', '😁'], ['🎂', '💖']]
data.flatMap(extractEmojis) // => ['😘', '😁', '🎂', '💖']
String
#matchAll
Grabs all group matches for a sticky or global regex.
const text = 'Get in touch at tel:0983450176 or sms:478-555-1234'
text.match(/(?<protocol>[a-z]{3}):(?<number>[\d-]+)/g)
// => ['tel:0983450176', 'sms:478-555-1234'] -- 😞 WHERE THEM GROUPS AT?!
Array.from(text.matchAll(/([a-z]{3}):([\d-]+)/g)).map(
([, protocol, number]) => ({ protocol, number })
)
// => [{ number: '0983450176', protocol: 'tel' }, { number: '478-555-1234', protocol: 'sms' }]
Array.from(text.matchAll(/(?<protocol>[a-z]{3}):(?<number>[\d-]+)/g)).map((mr) => mr.groups)
// => [{ number: '0983450176', protocol: 'tel' }, { number: '478-555-1234', protocol: 'sms' }]
import(…)
Dynamically imports ES Modules (“ESM”), using promises
(incidentally, this is the heart of Webpack’s automatic code splitting)
async function evaluate(code) {
const Babel = await import('@babel/standalone')
return Babel.transform(code, {
ast: true,
filenameRelative: 'exercice.js',
presets: ['stage-3'],
})
}
async function loadLocale(locale) {
const locale = await import(`./locales/${locale}`)
LocaleManager.registerLocale(locale, { activate: true })
}
BigInt
“Arbitrary precision” (infinite) integers (> 253)
const theBiggestInt = 9007199254740991n
const alsoHuge = BigInt(9007199254740991)
// => 9007199254740991n
const hugeButString = BigInt('9007199254740991')
// => 9007199254740991n
const bigN = 2n ** 54n
// => 18014398509481984n
[42n === 42, 42n == 42]
// => [false, true]
Promise.allSettled
3rd promise combinator (with all
and race
). any
is at stage 3.
The idea: doesn’t short-circuit, be it on the first rejection or fulfillment: we get all settlements for analysis.
await Promise.allSettled([wait(100), wait(50), throwAfter(75)])
// => [
// { status: 'fulfilled', value: 100 },
// { status: 'fulfilled', value: 50 },
// { status: 'rejected', reason: Error: 75 at… }
// ]
globalThis
Finally a standardization of the “global object,” regardless of the environment!
(web page, frame, worker, process, Node…)
Depending on context, aliases to: window
, self
, global
and sometimes this
(not to mention edge cases).
To make resilient yet readable property chains.
// BEFORE
user != null && user.names != null && user.names.first
user != null && user.signUp != null && user.signUp()
user != null && user.hobbies != null && user.hobbies[0]
// AFTER
user?.names?.first
user?.signup?.()
user?.hobbies?.[0]
To better handle default values.
// 50 only if `options.duration == null`
const duration = options.duration ?? 50
const easing = options.advanced?.easing ?? 'inOutCubic'
It’s nicer when you can read it.
const BILLION = 1_000_000_000 // You immediately know
const FEE_CENTS = 20_00 // Cents / centiles
const RATE = 2_90 // Ditto
const QUADS = 0b0010_1010 // “Quads” (4 bits)
const TX_VALUE = 1_234_5418 // Fixed point (4), financial
const WORDS = 0xDEAD_C0DE // “Words” (2 bytes)
await
3Until now, we could only await
in an async
function…
But this can be super-handy for module initialization, etc.
import { engine, process } from './some-module.mjs'
// Might as well run these in parallel…
const engineModule = import(`./engines/${engine}.mjs`)
const data = fetch(url)
// And presto!
export const output = process((await engineModule).default, await data)
Already in Node REPL, Chromium DevTools console, Safari Web Inspector console.
Private methods (incl. accessors), static and instance fields (public / private)
class APIClient extends Component {
// Private instance field
#oauthToken = null
// Public static field
static propTypes = {
authServer: URLPropType.isRequired,
}
// Public instance field
state = { authenticated: this.#oauthToken != null, loggedIn: false }
// Private instance method
#shareAuthWith(recipient) {
// Demands that `recipient` be an `APIClient`
recipient.#oauthToken = this.#oauthToken
}
}
So much nicer for AOP… ES provides the plumbing and the ecosystem provides operational decorators.
class SuperWidget extends Component {
@deprecate
deauth() { … }
@memoize('1m')
userFullName() { … }
@autobind
logOut() {
this.#oauthToken = null
}
@override
render() { … }
}
Temporal
2Aims to (beneficially) replace Moment, Luxon, data-fns, etc.
Immutable, nanosecond-precise, has all TZ, distinguishes absolute vs. local, explicit…
Great complement to Intl
and its formatting functions.
const meeting1 = Temporal.Date.from('2020-01-01')
const meeting2 = Temporal.Date.from('2020-04-01')
const time = Temporal.Time.from('10:00:00')
const timeZone = new Temporal.TimeZone('America/Montreal')
const absolute1 = timeZone.getAbsoluteFor(meeting1.withTime(time))
// => 2020-01-01T15:00:00.000Z
const absolute2 = timeZone.getAbsoluteFor(meeting2.withTime(time))
// => 2020-01-01T14:00:00.000Z
Check out Maggie’s awesome talk at dotJS 2019!
Pipeline operator + partial application = 😍
|
|
Pattern matching! 😍
const getLength = (vector) => case (vector) {
when { x, y, z } -> Math.sqrt(x ** 2 + y ** 2 + z ** 2)
when { x, y } -> Math.sqrt(x ** 2 + y ** 2)
when [...etc] -> vector.length
}
This is even better than traits, mixins or modules… The syntax is temporarily—and intentionally—verbose.
|
|
We publish about one new video course every month, mostly around Git and JS.
Or if that’s easier for you:
Christophe Porteneuve
Slides are at bit.ly/confoo-es2022