To jest natywny JavaScript? Prawda??
Moje pierwsze spotkanie z optional chaining i nullish coalescing operator.
Zaktualizowano: 23 lipca 2022Pierwszy raz, gdy użyłem frameworka Gatsby, zauważyłem dziwny fragment kodu.
src/components/seo.jsJSX
1const defaultTitle = site.siteMetadata?.title
Zastanawiałem się: “O co chodzi z tym znakiem zapytania po kropce? To jest jakaś specjalna składnia Gatsby?”. Wątpiłem w to, więc zacząłem szukać informacji. Po krótkich poszukiwaniach i googlowaniu głupich pytań, znalazłem termin, którego szukałem - optional chaining. Byłem zaskoczony - to jest natywny JavaScript. Składnia ta została zaprezentowana w ES2020 (w dniu, w którym piszę ten post jest na czwartym etapie procesu wdrażania). Najpierw przyjrzymy się kilku błędom, aby zobaczyć dlaczego jest użyteczna.
39nth Technical Committee (TC39) jest to grupa będąca częścią ECMA International, która zrzesza deweloperów, wdrożeniowców i akademików zajmujących się językiem JavaScript. Komitet ten współpracuje ze społecznością w utrzymywaniu i rozwijaniu specyfikacji ECMAScript (JavaScript jest zgodny z tą specyfikacją). Proces rozwoju specyfikacji ma cztery etapy. Czwarty etap oznacza, że funkcjonalność jest gotowa do umieszczenia w najbliższym projekcie specyfikacji.
Jeżeli nie jesteś kompletnym nowicjuszem JavaScript, prawdopodobnie widziałeś wiadomość jak ta: “Cannot read properties of undefined”. Zwykle oznacza, że próbujesz uzyskać dostęp do nieistniejącej wartości zagnieżdżonego obiektu. Odtwórzmy ten błąd, ale tym razem celowo. Wyobraź sobie, że chcesz wyświetlić dokładne informacje o komputerach - stacjonarnych i laptopach. Składają się prawie z tych samych części, ale niektóre laptopy nie mają dedykowanej karty graficznej - nie ma o niej informacji. Jeżeli zechcesz uzyskać do niej dostęp, otrzymasz nasz błąd.
JS
1const desktop = {2 processor: {3 manufacturer: 'Intel',4 type: 'I7'5 },6 graphicsCard: {7 manufacturer: 'Nvidia'8 }9 //...więcej komponentów10}1112const laptop = {13 processor: {14 manufacturer: 'AMD',15 type: 'Ryzen 5'16 }17 //...więcej komponentów bez karty graficznej18}1920const info = laptop.graphicsCard.manufacturer21//Uncaught TypeError: Cannot read properties of undefined (reading 'manufacturer')
Spróbujmy rozwiązać ten problem. Na początek dodajmy brakujące informacje.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 },6 graphicsCard: {7 manufacturer: ''8 }9}1011const info = laptop.graphicsCard.manufacturer //Brak błędu. Zwraca ""
A co gdybyśmy nie mogli? Nie zawsze mamy kontrolę nad API. Innym problemem jest sytuacja, gdy obiekt ma więcej wartości - każda z nich to musiałby być pusty string
, null
, itp. Średni pomysł. JavaScript jest dynamicznym językiem programowania i wartości mogą być null
albo undefined
. Musimy radzić sobie z takimi sytuacjami. Możemy użyć wyrażeń logicznych, aby rozwiązać ten problem.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop && laptop.graphicsCard && laptop.graphicsCard.manufacturer //undefined
Ten kod działa. Ale jest rozwlekły i niezgrabny. Chcemy się dostać do jednej wartości, a potrzebujemy dwóch operatorów logicznych. Wyobraź sobie, że jest ich wiele. Może składnia warunkowa pomoże.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop9 ? undefined10 : laptop.graphicsCard11 ? undefined12 : laptop.graphicsCard.manufacturer //undefined
Pisałem coś o niezgrabnym kodzie? To wygląda nawet gorzej. Zagnieżdżony operator trójskładnikowy to rzadko jest dobry pomysł. Ale JavaScript ma przecież składnię do obsługi błędów. Spróbujmy złapać ten błąd.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78let info9try {10 info = laptop.graphicsCard.manufacturer11} catch (error) {12 info = undefined //undefined13}
To jest bardziej czytelne, ale nadal rozwlekłe. Definiujemy nowe zakresy pomiędzy klamrami i nie możemy użyć const
. Pora rozwiązać ten problem jak prawdziwy programista JavaScript - użyjmy zewnętrznej biblioteki!
JS
1const R = require('ramda')23const laptop = {4 processor: {5 manufacturer: 'AMD',6 type: 'Ryzen 5'7 }8}910const info = R.path(['graphicsCard', 'manufacturer'], laptop) //undefined
Ten fragment jest zwięzły i czytelny. Ale czy naprawdę potrzebujemy zewnętrznej biblioteki do czegoś tak podstawowego? Nie, już nie. Optional chaining przychodzi na ratunek!
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop?.graphicsCard?.manufacturer //undefined
Używając tego zapisu możesz próbować uzyskać dostęp do wartości, które mogą być niedostępne. Powyższy fragment nie wyrzuca błędu. Zwraca undefined
. Jeżeli odniesienie jest null
albo undefined
, wyrażenie zwraca undefined
. Myślę, że operator ?.
jest zwięzły i intuicyjny. Jest jak pytanie: “czy istnieje producent (karty graficznej) w obiekcie laptop?”. Jeżeli tak - zwróć tę wartość. W inny wypadku zwróć undefined
. Ten operator jest bardziej uniwersalny. Możesz użyć tej składni przy wywoływaniu funkcji.
JS
1const laptop = {2 processor: {3 manufacturer: 'AMD',4 type: 'Ryzen 5'5 }6}78const info = laptop.nonExistingMethod?.() //undefined
Obiekt nie ma żadnej metody. Mimo to fragment powyżej nie wyrzuca wyjątku. Zwraca undefined
. Pójdźmy dalej - czy można użyć tego operatora z tablicami? Tak, można.
JS
1const laptop = {2 ram: ['Kingston 8GB', 'Kingston 8GB']3}45const info = laptop.ram?.[3] //undefined
Mimo że nie ma czterech elementów w tablicy, kod nie zwraca błędu.
Nullish coalescing operator
Jeżeli “dodamy” kolejny znak zapytania do naszego operatora i usuniemy kropkę, otrzymamy nowy operator logiczny - nullish coalescing operator.
JS
1const laptop = {2 processor: {3 type: 'Ryzen 5'4 }5}67const info = laptop.graphicsCard?.manufacturer ?? 'integrated' //integrated
Możesz interpretować powyższy fragment w ten sposób: "Jeżeli istnieje producent karty graficznej w obiekcie laptop, przypisz tę wartość do zmiennej info. W innym wypadku przypisz wyraz 'integrated'". Z poprzedniej sekcji wiemy, że optional chaining zwraca undefined
. Następnie interpreter przechodzi do logicznego operatora. Nullish coalescing operator to specjalny przypadek logicznego OR. Zwraca prawą część wyrażenia, gdy lewa część jest null
lub undefined
. Logiczne OR zwraca prawą część jeżeli po lewej stronie jest dowolna wartość falsy
.
JS
1const a = null ?? 'default' //"default"2const b = undefined ?? 'default' //"default"3const c = '' ?? 'default' //""4const d = NaN ?? 'default' //NaN - zwraca każdą wartość falsy56const e = '' || 'default' //"default"7const f = NaN || 'default' //"default"8const g = 0 || 'default' //"default" - zwraca "default" dla każdego falsy
Takie zachowanie logicznego OR może prowadzić to niespodziewanych błędów. Na przykład, gdy chcemy domyślną liczbę, ale 0 też jest poprawną, spodziewaną wartością. Dlatego nullish coalescing operator jest przydatny. Jest bardziej restrykcyjny i pomaga uniknąć takich błędów. Ale nie zastępuje on logicznego OR. Nie możesz także łączyć go z innymi operatorami logicznymi bez nawiasów.
null || undefined ?? "default"
Powyższy kod zwraca błąd składni.
(null || undefined) ?? "default”
Powyższy kod jest poprawny i zwraca “default”
Bonus - logical nullish assignment
Zbierając informację do tego posta, znalazłem jeszcze jeden operator z podwójnym znakiem zapytania. “Dodając” znak równości do nullish coalescing operator uzyskamy logical nullish assignment. Łatwo wydedukować co robi. Zamiast zwracania, przypisuje konkretną wartość do x, jeżeli x jest nullish (null lub undefined).
JS
1const laptop = {2 processor: 'Intel'3}45laptop.processor ??= 'AMD'6laptop.graphicsCard ??= 'Nvidia'78console.log(laptop.processor) //"Intel"9console.log(laptop.graphicsCard) //"Nvidia"
To jest skrót wyrażenia z nullish coalescing operator. Te dwa wyrażenia są równoznaczne.
JS
1const laptop = {2 processor: 'Intel'3}45laptop.graphicsCard ??= 'Nvidia'6laptop.graphicsCard ?? (laptop.graphicsCard = 'Nvidia')7//Powyższe dwa wyrażenia robią to samo89console.log(laptop.graphicsCard) //"Nvidia"
Jeżeli chcesz więcej przykładów lub szczegółów, sprawdź MDN Web Docs: