Diversity w polskim IT
Adrian Kałuziński
Adrian KałuzińskiSoftware Engineer

Nowości w standardzie ECMAScript 2021

Poznaj zmiany, jakie zaszły w standardzie ECMAScript 2021 języka JavaScript.
6.09.20216 min
Nowości w standardzie ECMAScript 2021

W czerwcu został opublikowany nowy standard standardu ECMAScript 2021 (ES21). Przyjrzyjmy się zmianom przygotowanym przez komitet ​​TC39. Większość z nich jest już wspierana w najnowszych wersjach znanych przeglądarek i można z nimi działać.

replaceAll

Dodano nową, użyteczną funkcję String.prototype.replaceAll(), która zamienia wszystkie wystąpienia danego ciągu znaków lub wyrażenia regularnego. 

const someText = "Ten tekst pojawia się wiele, wiele razy. Zduplikowany tekst można zmienić nieco łatwiej";

const updatedText = someText.replaceAll('tekst', 'napis');
console.log(updatedText);
// "Ten napis pojawia się wiele, wiele razy. Zduplikowany napis można zmienić nieco łatwiej"


W przypadku zamieniania wszystkich wystąpień za pomocą wyrażenia regularnego

const anotherText = 'Ten tekst'.replaceAll(/t/gi, '.');
console.log(anotherText); //".en .eks."


Mała uwaga: w wyrażeniu regularnym należy użyć flagę g. W przeciwnym razie czeka nas:

TypeError: String.prototype.replaceAll called with a non-global RegExp argument.

Numeric Separators

Rzućcie okiem na liczbę 100000000. Co widzicie? Sto milionów? Dziesięć? A teraz: 100_000_000? Aby ułatwić odczytywanie długich liczb, wprowadzono możliwość oddzielania cyfr znakiem niskiej linii (_, U+005F). Podobny zapis znamy już z języków Java7+ czy Swift.

let milionyMonet = 100_000_000.00;
const countryBudget = 2_230_001_000_000n; 
let someHexValue = 0xA0_B0_F0;


Ten separator nie wpływa na wartość liczby. Ma wyłącznie poprawić czytelność naszego kodu.

Promise.any

Promise (obietnice) zostały wprowadzone do JavaScript w standardzie ES6 w 2015. Zdecydowanie ułatwiają pracę z kodem asynchronicznym. Więcej o obietnicach możesz przeczytać w artykule na blogu. Teraz do języka dodano funkcję Promise.any(), która ma być uzupełnieniem dla od dawna istniejącej Promise.all(). Funkcja Promise.any() zwróci pierwszą spełnioną (fulfilled) obietnicę. Poniżej znajdziecie przykład.

Załóżmy, że mamy funkcję callWithRandomWait, która symuluje powolny backend, zwracający odpowiedź w czasie do 500 milisekund. 

   const callWithRandomWait = callback => {
        const interval = Math.floor(Math.random() * 500);
        setTimeout(callback, interval);
    };


Do Promise.any() przekazujemy tablicę obietnic, na które czekamy. 

    const promiseA = new Promise((resolve, _) => {
        callWithRandomWait(() => resolve('Wynik z promiseA'));
    });

    const promiseB = new Promise((resolve, _) => {
        callWithRandomWait(() => resolve('Wynik z promiseB'));
    });

    (async () => {
        const result = await Promise.any([ promiseA, promiseB ]);
        console.log(result);
    })();


Po kilkukrotnym uruchomieniu kodu zobaczycie w konsoli albo napis Wynik z promiseA albo Wynik z promiseB. Promise.any() może się okazać przydatna w sytuacji, gdy mamy kilka źródeł danych, ale interesuje nas uzyskanie odpowiedzi od któregokolwiek z nich.

AggregateError

Obiekt AggregateError reprezentuje błąd składający się z kilku błędów. Możemy go zaobserwować po zmianie poprzedniego przykładu w taki sposób, że każdy z naszych Promise zwraca reject zamiast fulfill. Może się to zdarzyć np. w przypadku wystąpienia kodu błędu 404 ze strony serwera. 

     const promiseA = new Promise((_, error) => {
        callWithRandomWait(() => error('Błąd z promiseA'));
    });

    const promiseB = new Promise((_, error) => {
        callWithRandomWait(() => error('Błąd z promiseB'));
    });

    (async () => {
        const result = await Promise.any([ promiseA, promiseB ]);
        console.log(result);
    })();


Tym razem w konsoli przeglądarki ujrzymy: Uncaught (in promise) AggregateError: All promises were rejected.

Jeżeli złapiemy nasze błędy i odwołamy się do pola AggregateError.errors, uzyskamy ich treść.

      (async () => {
   	try {
        	await Promise.any([ promiseA, promiseB ]);
    	} catch (error) {
        	console.log(error.errors);
    	}
    })();
// ["Błąd z promiseA", "Błąd z promiseB"]


Operatory przypisań logicznych ??=, &&=, ||=

Twórcy języka wprowadzili trzy nowe operatory ??=, &&=, ||= dokonujące przypisania do zmiennej, gdy jest spełniony warunek logiczny.

Operator x &&= y (Logical AND assignment) dokonuje przypisania tylko wtedy, gdy x jest truthy. Oznacza to skrócony zapis:

 x && (x = y); 


a także:

if (x) {
  x = y;
}


Dla przykładu jeśli x jest truthy:

let x = 4;
let y = 5;
x &&= y;
console.log(x); //5


a gdy x nie jest truthy:

let a = 0;
let b = 5;
a &&= b;
console.log(a); //0


należy zwrócić uwagę, że ten operator nie jest to równoznaczny z operacją

 x = x && y;


która zawsze wykonuje przypisanie do x.

Na podobnej zasadzie działają pozostałe dwa nowe operatory.

Operator x ||= y (Logical OR assignment) dokonuje przypisania y do x tylko wtedy, gdy x jest falsy (null, 0, "", false, undefined, NaN). 

if (!x) {
   x = y;
}

let a = undefined;
let b = 7;
a ||= b;
console.log(b); // 7


Operator x ??= y (Logical nullish assignment) dokonuje przypisania y do x tylko wtedy, gdy x jest nullish (null lub undefined). 

x ??= y jest równoznaczny z blokiem

if (x == null || x == undefined) {
  x = y;
}


Można go potraktować jako szczególny przypadek poprzedniego operatora.

Przypomnijmy, że operator ?? (Nullish Coalescing Operator) dodany w ES11 zwraca to, co jest po lewej stronie, o ile to nie jest null lub undefined. W przeciwnym razie zwraca to, co jest po prawej stronie.

let a;
console.log(a ?? 3); // 3

let b = 0;
console.log(b ?? 3); // 0


Jak wygląda użycie logical nullish assignment?

let x = null;
let y = 7;
x ??= y;
console.log(x); // 7


FinalizationRegistry

FinalizationRegistry oczekuje funkcji, która ma być wykonana, gdy obiekt jest usunięty z pamięci przez garbage collector.

const registry = new FinalizationRegistry(heldValue => {
  // co robimy po sprzątaniu pamięci?
});


Aby zarejestrować obiekty, po których usunięciu ma zostać wykonana funkcja, używamy funkcji register.

 finalizationRegistry.register(theObject, "utrzymana wartość");


Jeżeli referencja do naszego obiektu zostanie zwolniona przez przypisanie wartości undefined, garbage collector może (ale nie musi!) usunąć go z pamięci i wykonać callback z FinalizationRegistry.

(async () => {
    	const sleep = (ms) => new Promise(_ => setTimeout(_, ms));

    	let isCleanupDone = false;
   	 
    	const finalizationRegistry = new FinalizationRegistry((heldValue) => {
        	console.log(`Zwolniono pamięć po ${heldValue}`);
        	isCleanupDone = true;
    	});

    	let tempText = {
        	text: 'Coś tymczasowego',
        	blabla: 'co ma dwa pola'
    	};

    	let hugeArray = new Array(10_000_000);

    	finalizationRegistry.register(tempText, "Niepotrzebny obiekt");
    	finalizationRegistry.register(hugeArray, "Wielka, zbędna tablica");
    	tempText = undefined;
    	hugeArray = undefined;

    	console.log('Alokuj dużo pamięci aby wymusić garbage collector')

    	while (!isCleanupDone) {
        	for (let i = 0; i < 1000; i++) {
            	const eatMemory = new Array(1000);
        	}
        	await sleep(100);
    	}
})();


Opieranie się na oczyszczaniu pamięci w żadnym razie nie powinno być używane do sterowania główną logiką naszych programów. Nie wiemy kiedy, ani nawet czy garbage collector zostanie wykonany, gdyż zależy to od implementacji konkretnego silnika JavaScript. FinalizationRegistry powinniśmy potraktować raczej pomocniczo w wyjątkowych sytuacjach, jeśli nasze aplikacje zużywają sporo pamięci.

WeakRef

Najnowsza wersja standardu wprowadza obiekty WeakRef pozwalające na przechowywanie słabej referencji, która nie jest zabezpieczona przed odśmiecaniem pamięci. 

Standardowo garbage collector nie usunie obiektu z pamięci, jeśli jakakolwiek inna zwykła (silna) referencja będzie na niego wskazywała. 

	let objectRef = { test: 'value' };
	let anotherRef = objectRef;
	objectRef = undefined;
console.log(objectRef); // => undefined
	console.log(anotherRef); // => {test: 3}


anotherRef nadal wskazuje na pierwotny obiekt, więc zmiana referencji objectRef = undefined nie spowoduje usunięcia obiektu przez garbage collector.

WeakRef natomiast pozwala na sprzątanie pamięci jeśli pierwotna referencja została utracona. Obiekt taki posiada jedną funkcję WeakRef.prototype.deref(), która zwraca wskazywany obiekt, lub undefined w przypadku, gdy pamięć została zwolniona.

	let someCacheableObject = { test: 'value' };
	let weakReference = new WeakRef(someCacheableObject);

	const registry = new FinalizationRegistry((heldValue) => {
    	console.log(`weakReference po zwolnieniu ${heldValue}:`, weakReference.deref()); 
// => weakReference po zwolnieniu someCacheableObject: undefined 
	});

	registry.register(someCacheableObject, "someCacheableObject");
	someCacheableObject = undefined;


Z WeakRef podobnie jak z FinalizationRegistry, należy obchodzić się ze szczególną ostrożnością. Zastosowanie tych obiektów ogranicza się głównie do budowania cache i wsparcia ochrony przed wyciekami pamięci. 

Podsumowanie

Jak widzicie, najnowsza wersja standardu nie jest żadną rewolucją. Warto jednak mieć świadomość, że JavaScript jest żywym językiem i cały czas się rozwija.

<p>Loading...</p>