Joel's dev blog

New javascript specifications in 2019 (What's new in Javascript - Google I/O '19)

September 10, 2019

10 min read

Must must must watch!

I got so much great insight from this video in Google IO 2019 detailing latest javascript specs.

Improvements from the past

  • 2x faster javascript parsing on Chrome 75 (V8 v7.5) compared to Chrome 61 (V8 v6.1)
  • 11x faster async on Chrome 74 (Node.js 12) compared to Chrome 55 (Node.js 7)
  • -20% memory usage on Chrome 76 compared to Chrome 70

What’s new

Public & private class fields

Before

You arbitrarily set a private class variable that is accessible outside the class

class Hello {
  constructor(){
    this._msg = 'Hello'
  }
  get msg(){
    return this._msg  
  }
  prepareBye(){
    this._msg = 'Bye'
  } 
}

You have to call super(...) in a child class

class Animal{
  constructor(name){
    this.name = name  
  }
}

class Cat extends Animal{
  constructor(name){
    super(name)
    this.likesBaths = false
  }
}

After

You can omit the constructor

class Hello {
  _msg = 'Hello'
  get msg(){
    return this._msg  
  }
  prepareBye(){
    this._msg = 'Bye'
  } 
}

You can declare private fields

class Hello {
  #msg = 'Hello'
  get msg(){
    return this.#msg  
  }
  prepareBye(){
    this.#msg = 'Bye'
  } 
}

An error is going to occur if you try to access helloInstance.#msg.

You don’t have to call super(...) in a child class

class Animal{
  constructor(name){
    this.name = name  
  }
}

class Cat extends Animal{
  likesBaths = false
}

Regular expressions

Before

You have to run an inconvenient, unnatural loop to get match objects

const string = 'hex nums: DEADBEEF CAFE'
const regex = /\b\p{ASCII_Hex_Digit}+\b/gu
let match
let result = []
while (match = regex.exec(string)){
  result = [match, ...result]
}
/*
result = 
[
  [
    'CAFE',
    index: 19,
    input: 'hex nums: DEADBEEF CAFE',
    groups: undefined
  ],
  [
    'DEADBEEF',
    index: 10,
    input: 'hex nums: DEADBEEF CAFE',
    groups: undefined
  ]
]
// NOTE: this weird-looking array is actually a valid array; each element contains its value (in this example, string) and also has properties (index, input, groups)
*/

After

You can get all match objects with str.matchAll(regex)

for (const match of string.matchAll(regex)){
  result = [match, ...result]
}
// result: same as above

Numeric literals

Before

Confusing

const num = 1000000000000

After

Clear

const num = 1_000_000_000_000

BigInt

Before

Wrong calculation

const num = 1234567890123456789 * 123 
num // 151851 ... 0000, which is obviously wrong

After

Correct calculation with BigInt literal

const = 1234567890123456789n * 123n
num // 151851 ... 158047n correct.

Intl & Locale

Locale for different languages for numbers

12_345_678_901_234_567_890n.toLocaleString('en')
// '12,345,678,901,234,567,890'
const nf = new Intl.NumberFormat('fr')
nf.format(12_345_678_901_234_567_890n)
// 12 345 678 901 234 567 890

Flattening an array

Before

Used a 3rd party library

Used map and flat together

[2,3,4].map(x=>[x,x]).flat() // slower

After

array.flat

const array = [1, [2, [3]]]
const array2 = [1, [2, [3]]]
array.flat() // [1,2,[3]]
array2.flat(Infinity) // [1,2,3]

flatMap

[2,3,4].flatMap(x=>[x,x]) // faster

Object.fromEntries

Before

There is no convenient way back to make it into the original object

const object = {x : 42, y : 50 }
const entries = Object.entries(object) // returns an array of arrays containing key and value
const result = {}

for (const [k, v] of entries){
  result[k] = v
}

After

There is a convenient way back to make it into the original object

const result = Object.fromEntries(entries)

Map

You can play around with Map and objects

const object = { language: 'Javascript', coolness: 9001 }
const map = new Map(Object.entries(object))
const objectCopy = Object.fromEntries(map)

GlobalThis

Before

You have to manually look for the global this

const getGlobalThis = function () { 
  if (typeof self !== 'undefined') { return self; } 
  if (typeof window !== 'undefined') { return window; } 
  if (typeof global !== 'undefined') { return global; } 
  throw new Error('unable to locate global object'); 
}; 

const globalThis = getGlobal(); 

After

You already have globalThis

const gt = globalThis

Stable sort

Before

Sort was differently implemented in different javascript engines.

Sort results were not consistent.

This is the code snippet from itnext:

const list = [
  { name: 'Anna', age: 21 },
  { name: 'Barbra', age: 25 },
  { name: 'Zoe', age: 18 },
  { name: 'Natasha', age: 25 }
];
list.sort((a,b)=>b.age-a.age)
// possible result
[
  { name: 'Natasha', age: 25 }
  { name: 'Barbra', age: 25 },
  { name: 'Anna', age: 21 },
  { name: 'Zoe', age: 18 },
]

After

Sort results are now consistent

Sort always returns the same result, with other keys sorted in their range as well

const list = [
  { name: 'Anna', age: 21 },
  { name: 'Barbra', age: 25 },
  { name: 'Zoe', age: 18 },
  { name: 'Natasha', age: 25 }
];
list.sort((a,b)=>b.age-a.age)
// always
[
  { name: 'Barbra', age: 25 },
  { name: 'Natasha', age: 25 },
  { name: 'Anna', age: 21 },
  { name: 'Zoe', age: 18 }
]

Intl

Before

You used 3rd party libraries like momentjs.

After

You can rely on Intl.RelativeTimeFormat

const { log } = console
const rtfEspanol= new Intl.RelativeTimeFormat('es', {
  numeric: 'auto'
});
log( rtfEspanol.format( 5, 'day' ) ); // dentro de 5 días
log( rtfEspanol.format( -5, 'day' ) ); // hace 5 días
log( rtfEspanol.format( 15, 'minute' ) ); // dentro de 15 minutos

const rtfKo = new Intl.RelativeTimeFormat('ko')
rtf.format(5, 'day') // "5일 후"

Intl.ListFormat

const lfEspanol = new Intl.ListFormat('es', {
  type: 'disjunction'
});
const list = [ 'manzanas', 'mangos', 'plátanos' ];
log( lfEspanol.format( list ) ); // manzanas, mangos o plátanos

Other new Intl APIs like DateTimeFormat#formatRange, Locale also exist

Side note: these features might need to be still be specifically built depending on the version of 12.x you are using. For more, check this PR in node repo.

Top-level async

Before

await is wrapped by an outer async

async function main(){
  const result = await doSomethingAsync();
  doSomethingElse();
}

main();

// or..

(async () => {
  const result = await doSomethingAsync();
  doSomethingElse();
})()

After

Top-level await is possible

const result = await doSomethingAsync();
doSomethingElse();

More Promise APIs

First we need to know about the promise state definitions:

  • Fulfilled: When a promise is resolved successfully. (opposite of rejected)
  • Rejected: When a promise failed.
  • Pending: When a promise is “neither fulfilled nor rejected“.
  • Settled: Not really a state but an umbrella term to describe that a promise is either fulfilled or rejected (important for the new methods)

Before

Only two: Promise.all & Promise.race

Promise.all

  • Returns a Promise when all promises in an interable are fulfilled or one of them gets rejected.
  • Returns a single Promise that that resolves when all of the promises passed as an iterable have resolved or when the iterable contains no promises (See MDN Docs for more)
const promises = [
  fetch('/component-a.css')
  fetch('/component-b.css')
  fetch('/component-c.css')
]
try{
  const styleResponses = await Promise.all(promises)
  enableStyles(styleResponses)
  renderNewUi();
}
catch(reason){
  displayError(reason)
}

Promise.race

  • Returns a Promise as soon as one of the inputs settles
  • Returns a Promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise.
try{
  const result = await Promise.race([
    performHeavyComputation(),
    rejectAfterTimeout(2000),
  ])
  renderResult(result)
} catch(error){
  renderError(error)
}

After

We’ve got two more: Promise.allSettled & Promise.any

Promise.allSettled

  • When all of the input promises are settled (either fulfilled or rejected).
  • You don’t care about the success or failure
const promises = [
  fetch('/api-1')
  fetch('/api-2')
  fetch('/api-3')
]

await Promise.allSettled(promises)
removeLoadingIndicator(); // you want to do this regardless of successes or failures in promises.

Promise.any

  • Gives a signal as soon as one of the promises fulfills
  • It does not reject early when one of the promises rejects (it is going to wait to see if other promises will reject too)
  • Only if ALL promises reject, you end up in the catch block.
  • Otherwise it will give you the first promise that fulfills
  • Similar to .race, but .race rejects as soon as one of the promises rejects. But .any is still going to wait.
  • In short, .any is going to wait for the first promise that fulfills, but .race is going to wait for the first promise that either fulfills or rejects.
const promises = [
  fetch('./endpoint-a').then(()=>'a'),
  fetch('./endpoint-b').then(()=>'b'),
  fetch('./endpoint-c').then(()=>'c'),
]
try{
  const first = await Promise.any(promises)
  // Any of the promises was fulfilled
  console.log(first)
} catch (error){
  // All promises were rejected
  console.log(error)
}

WeakRef

Objects are strongly referenced in javascript. WeakMap and WeakSet allow garabge collection at any time. WeakRef is kind of more advanced API than these.

Before

Inefficient use of Map with a memory leak

  • Map holds onto values and keys strongly, so image names and data will never be garbage-colelcted, which is going to cause a memory leak.
  • WeakMap is not going to help here because it does not storing a key as a string.
function getImage(name){
  const image = performExpensiveOperation(name)
  return image
}

const cache = new Map()
function getImageCached(name){
  if(cache.has(name)) return cache.get(name)
  const image = performExpensiveOperation(name)
  cache.set(name, image)
  return image
}

After

Use WeakRef to allow garbage collection

const cache = new Map()
function getImageCached(name){
  let ref = cache.get(name);
  if (ref !== undefined){ // You have a ref already
    const deref = ref.deref()
    if (deref !== undefined) return deref // garbage collector may have cleaned up some of the references if it were running out of memory, OR simply the reference has not yet been added. Check if this happened. 
  }
  const image = performExpensiveOperation(name)
  ref = new WeakRef(image)
  cache.set(name, ref) // You set a ref as a value instead of the image itself 
  return image
}

But recognize one last problem: the name keys in the cache won’t be removed.

Use WeakRef & FinalizationGroup to allow garbage collection

const finalizationGroup = new FinalizationGroup(iterator => {
  for (const name of iterator){
    const ref = cache.get(name)
    if (ref !== undefined && ref.deref() === undefined){ // name still exists, but there is no corresponding reference. Delete name as it is dangling. 
      cache.delete(name)
    }
  }
})

const cache = new Map()
function getImageCached(name){
  let ref = cache.get(name);
  if (ref !== undefined){
    const deref = ref.deref()
    if (deref !== undefined) return deref 
  }
  const image = performExpensiveOperation(name)
  ref = new WeakRef(image)
  cache.set(name, ref) // You set a ref as a value instead of the image itself 
  finalizationGroup.register(image, name); // Removes the name (only for name, not for ref) from the cache once the data is garbage-collected.
  return image
}

Written by Joel Mun. Joel likes Rust, GoLang, Typescript, Wasm and more. He also loves to enlarge the boundaries of his knowledge, mainly by reading books and watching lectures on Youtube. Guitar and piano are necessities at his home.

© Joel Mun 2024