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.