JavaScript: what's next for us next year





Good day, friends!



This article focuses on the JavaScript capabilities that will be presented in the new version of the specification (ECMAScript 2021, ES12).



It will be about the following:



  • String.prototype.replaceAll ()
  • Promise.any ()
  • WeakRefs
  • Boolean assignment operators
  • Number separators




String.prototype.replaceAll ()



String.prototype.replaceAll () ( Mathias Bynens' clause ) allows you to replace all instances of a substring in a string with a different value without using a global regex.



In the following example, we replace all "+" characters with commas with a space using a regular expression:



const strWithPlus = '++'
const strWithComma = strWithPlus.replace(/+/g, ', ')
// , , 


This approach requires the use of a regular expression. However, complex regular expressions are often a source of errors.



There is another approach based on using the String.prototype.split () and Array.prototype.join () methods:



const strWithPlus = '++'
const strWithComma = strWithPlus.split('+').join(', ')
// , , 


This approach avoids using regular expressions, but you have to split the string into separate parts (words), convert it to an array, and then concatenate the array elements into a new string.



String.prototype.replaceAll () solves these problems and provides a simple and convenient way to globally replace substrings:



const strWithPlus = '++'
const strWithComma = strWithPlus.replaceAll('+', ', ')
// , , 


Note that for consistency with previous APIs, the behavior of String.prototype.replaceAll (searchValue, newValue) (searchValue is the search value, newValue is the new value) is the same as String.prototype.replace (searchValue, newValue), except for the following:



  • If the value you are looking for is a string, then replaceAll replaces all matches, and replace only the first
  • If the desired value is a non-global regular expression, then replace replaces the first match, and replaceAll throws an exception to avoid a conflict between the absence of the "g" flag and the method name (replace all - replace all [matches])


If a global regular expression is used as the lookup value, then replace and replaceAll behave the same.



What if we have a line with an arbitrary number of spaces at the beginning, end of the line, and between words?



const whiteSpaceHell = '          '


And we want to replace two or more spaces with one. Can replaceAll solve this problem? No.



With String.prototype.trim () and replace with a global regular expression, this is done like this:



const whiteSpaceNormal =
  whiteSpaceHell
    .trim()
    .replace(/\s{2,}/g, ' ')
    // \s{2,}     
    //   


Promise.any ()



Promise.any () (a suggestion from Mathias Bynens, Kevin Gibbons, and Sergey Rubanov ) returns the value of the first fulfilled promise. If all promises passed to Promise.any () as an argument (as an array) are rejected, an "AggregateError" exception is thrown.



AggregateError is a new Error subclass that groups individual errors. Each AggregateError instance contains a reference to an array with exceptions.



Let's consider an example:



const promise1 = new Promise((resolve, reject) => {
  const timer = setTimeout(() => {
    resolve('p1')
    clearTimeout(timer)
  }, ~~(Math.random() * 100))
}) // ~~ -   Math.floor()

const promise2 = new Promise((resolve, reject) => {
  const timer = setTimeout(() => {
    resolve('p2')
    clearTimeout(timer)
  }, ~~(Math.random() * 100))
})

;(async() => {
  const result = await Promise.any([promise1, promise2])
  console.log(result) // p1  p2
})()


The result will be the value of the first resolved promise.



Example from the sentence:



Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {
  //  ()   
  console.log(first);
  // β†’ 'home'
}).catch((error) => {
  //    
  console.log(error);
})




Note that Promise.race (), unlike Promise.any (), returns the value of the first resolved promise, whether fulfilled or rejected.



WeakRefs



WeakRefs (weak references) ( proposed by Dean Tribble, Mark Miller, Till Schneidereit, etc. ) provides two new features:



  • Creating Weak References to an Object Using the WeakRef Class
  • Running custom finalizers after garbage collection using the FinalizationRegistry class


In short, WeakRef allows you to create weak references to objects that are the values ​​of properties of another object, and finalizers can be used, among other things, to remove references to objects "cleaned up" by the garbage collector.



This technique can be useful when creating a memorization (memoization) function that uses the built-in cache to prevent repeated execution of the function if there is a calculated value for the argument passed to the function in the cache (provided that objects are used as values ​​for the properties of the cache object and the risk of their subsequent deletion) ...



As you remember, the reason for the appearance in JavaScript of such a structure as Map (hash table), in addition to the faster search for a value by key, was that the keys of an ordinary object can only be strings or characters. Map, on the other hand, allows using any data type as a key, including objects.



However, a memory leak problem soon emerged: deleting objects that were Map keys did not make them unreachable (mark-and-sweep), which prevented the garbage collector from destroying them, freeing the memory they were occupying.



In other words, the objects used as keys in the Map are saved forever.



Another structure, WeakMap (and WeakSet), was introduced to solve this problem. The difference between WeakMap and Map is that references to key objects in WeakMap are weak: deleting such objects allows the garbage collector to reallocate the memory allocated for them.



Thus, this proposal represents the next stage in the development of a hash table in JavaScript. Objects can now be used both as keys and as values ​​in other objects without the risk of memory leaks.



Once again, when it comes to building an inline cache:



  • If there is no risk of memory leaks, use Map
  • When using key objects that can be subsequently deleted, use WeakMap
  • When using value objects that can be subsequently deleted, use Map in conjunction with WeakRef


An example of the last case from the proposal:



function makeWeakCached(f) {
  const cache = new Map()
  return key => {
    const ref = cache.get(key)
    if (ref) {
      //     
      const cached = ref.deref()
      if (cached !== undefined) return cached;
    }

    const fresh = f(key)
    //    ( )
    cache.set(key, new WeakRef(fresh))
    return fresh
  };
}

const getImageCached = makeWeakCached(getImage);


  • The WeakRef constructor takes an argument that must be an object and returns a weak reference to it
  • The deref method of a WeakRef instance returns one of two values:


In the case of a built-in cache, the finalizer is designed to complete the cleanup process after a value object is destroyed by the garbage collector, or, more simply, to remove a weak reference to such an object.



function makeWeakCached(f) {
  const cache = new Map()
  //    -   
  const cleanup = new FinalizationRegistry(key => {
    const ref = cache.get(key)
    if (ref && !ref.deref()) cache.delete(key)
  })

  return key => {
    const ref = cache.get(key)
    if (ref) {
      const cached = ref.deref()
      if (cached !== undefined) return cached
    }

    const fresh = f(key)
    cache.set(key, new WeakRef(fresh))
    //      ( )
    cleanup.register(fresh, key)
    return fresh
  }
}

const getImageCached = makeWeakCached(getImage);


Read more about finalizers and how to use them in the proposal. In general, finalizers should only be used when absolutely necessary.



Boolean assignment operators



Boolean assignment operators ( Justin Ridgewell's proposal and Hemanth HM ) are a combination of Boolean operators (&&, ||, ??) and assignment expressions.



As of today, JavaScript has the following assignment operators:



=
 

+=
  

-=
  

/=
  

*=
  

&&=
   

||=
   

??=
      (null  undefined -  , 0, false,  '' -  )

**=
    

%=
    

&=
   

|=
   

^=
    

<<=
    

>>=
    

>>>=
       

  
[a, b] = [ 10, 20 ]
{a, b} = { a: 10, b: 20 }


The clause allows you to combine logical operators and assignment expressions:



a ||= b
// : a || (a = b)
//     ,   "a"  

a &&= b
// : a && (a = b)
//     ,   "a"  

a ??= b
// : a ?? (a = b)
//     ,   "a"   (null  undefined)


Example from a sentence:



//    
function example(opts) {
  //  ,    
  opts.foo = opts.foo ?? 'bar'

  //   ,     
  opts.baz ?? (opts.baz = 'qux')
}

example({ foo: 'foo' })

//    
function example(opts) {
  //     
  opts.foo ??= 'bar'

  //  ""   opts.baz
  opts.baz ??= 'qux';
}

example({ foo: 'foo' })


Number separators



Number separators ( Christophe Porteneuve's suggestion ), or more precisely, number separators in numbers, allow you to add an underscore (_) character between numbers to make numbers more readable.



For instance:



const num = 100000000
//     num? 1 ? 100 ? 10 ?


Separators solve this problem:



const num = 100_000_000 //  : 100 


Separators can be used in both the integer and decimal parts of a number:



const num = 1_000_000.123_456


Separators can be used not only in integers and floating point numbers, but also in binary, hexadecimal, octal, and BigInt literals.



Further development of number separators implies the possibility of useful use of several sequential separators and separators before and after the number.



Looking to test or brush up on your JavaScript knowledge? Then pay attention to my wonderful application (you can't praise yourself ...).



I hope you found something interesting for yourself. Thank you for attention.



All Articles