ES6 bez tajemnic. Destrukturyzacja

17 listopada 2015
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawiły się w 6. edycji standardu ECMAScript, w skrócie ES6.

Uwaga od redaktora: pierwotna wersja niniejszego artykułu autorstwa inżyniera ds. narzędzi programistycznych w Firefoksie Nicka Fitzgeralda ukazała się na jego blogu pod tytułem Destructuring Assignment in ES6.

Czym jest przypisanie destrukturyzujące?

Przypisanie destrukturyzujące umożliwia przypisanie własności tablicy lub obiektu do zmiennych z wykorzystaniem składni przypominającej składnię tablic czy literałów obiektowych. Może być ona niezwykle zwięzła, a jednocześnie znacznie czytelniejsza od tradycyjnego kodu służącego do uzyskania dostępu do własności.

Nie korzystając z przypisania destrukturyzującego, dostęp do pierwszych trzech elementów tablicy możemy uzyskać w następujący sposób:

var first = jakaśTamTablica[0];
var second = jakaśTamTablica[1];
var third = jakaśTamTablica[2];

Jeśli natomiast zastosujemy przypisanie destrukturyzujące, ten sam kod można wyrazić w bardziej zwięzły i czytelny sposób:

var [first, second, third] = jakaśTamTablica;

SpiderMonkey (silnik JavaScriptu w Firefoksie) obsługuje już destrukturyzację w znacznym stopniu, jednak wciąż wymagane są w tym zakresie poprawki. Tutaj możesz śledzić błąd 694100 dotyczący obsługi destrukturyzacji (i całego ES6) w silniku SpiderMonkey.

Destrukturyzacja tablic i obiektów iterowalnych

Widzieliśmy już przykład wykorzystania przypisania destrukturyzującego w przedstawionej powyżej tablicy. Standardowa składnia przypisania destrukturyzującego wygląda następująco:

[ zmienna1, zmienna2, ..., zmiennaN ] = nazwaTablicy;

Kod ten przypisze po prostu wszystkie zmienne począwszy od zmiennej1 do zmiennej N do odpowiadającego im elementu w danej tablicy. Jeśli w tym samym kodzie chciałbyś również zadeklarować zmienne, możesz to zrobić, dodając słowa kluczowe var, let, bądź const na początku przypisania:

var [ zmienna1, zmienna2, ..., zmiennaN ] = nazwaTablicy;
let [ zmienna1, zmienna2, ..., zmiennaN ] = nazwaTablicy ] = nazwaTablicy;
const [ zmienna1, zmienna2, ..., zmiennaN ] = nazwaTablicy ] = nazwaTablicy;

Nazwa „zmienna” jest w gruncie rzeczy myląca, ponieważ możemy zagnieździć parametry w kodzie tak głęboko, jak tylko chcemy:

var [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo);
// 1
console.log(bar);
// 2
console.log(baz);
// 3

Ponadto możliwe jest pominięcie elementów poddawanej destrukturyzacji tablicy:

var [,,third] = ["foo", "bar", "baz"];
console.log(third);
// "baz"

Można także ująć wszystkie elementy tablicy za pomocą parametru resztowego:

var [head, ...tail] = [1, 2, 3, 4];
console.log(tail);
// [2, 3, 4]

Podczas próby uzyskania dostępu do elementów nieistniejących bądź znajdujących się poza zakresem tablicy, zwracana jest ta sama wartość co w przypadku indeksowania, czyli undefined.

console.log([][0]);
// undefined

var [missing] = [];
console.log(missing);
// undefined

Warto zauważyć, że przypisanie destrukturyzujące wykorzystujące wzorzec przypisania tablicy działa także w przypadku dowolnego obiektu iterowalnego:

function* fibs() {
  var a = 0;
  var b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

var [first, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth);
// 5

Destrukturyzacja obiektów

Destrukturyzacja obiektów pozwala powiązać zmienne z innymi własnościami danego obiektu. Należy najpierw określić własność, a następnie podać zmienną, której wartość ma zostać powiązana z wybraną własnością.

var robotA = { name: "Bender" };
var robotB = { name: "Flexo" };

var { name: nameA } = robotA;
var { name: nameB } = robotB;

console.log(nameA);
// "Bender"
console.log(nameB);
// "Flexo"

Istnieje przydatny skrót składniowy, który można wykorzystać, jeśli wybrana własność i zmienna mają identyczne nazwy:

var { foo, bar } = { foo: "lorem", bar: "ipsum" };
console.log(foo);
// lorem
console.log(bar);
// ipsum

Podobnie jak w przypadku destrukturyzacji tablic, tak i w przypadku obiektów możemy zagnieżdżać i łączyć ze sobą dalsze przypisania destrukturyzujące:

var complicatedObj = {
  arrayProp: [
    "Zapp",
    { second: "Brannigan" }
  ]
};

var { arrayProp: [first, { second }] } = complicatedObj;

console.log(first);
// Zapp
console.log(second);
// Brannigan

W przypadku próby destrukturyzacji niezdefiniowanych własności, zwracana jest wartość undefined:

var { missing } = {};
console.log(missing);
// undefined

Warto pamiętać, że jeśli skorzystamy z destrukturyzacji obiektu jedynie w celu przypisania zmiennych, bez ich deklaracji (czyli jeśli w naszym kodzie nie będzie słowa kluczowego let, const, bądź var), czyhać będzie na nas pułapka:

{ blowUp } = { blowUp: 10 };
// błąd składni

Dzieje się tak, ponieważ gramatyka JavaScriptu wymusza traktowanie dowolnej instrukcji rozpoczynającej się od znaku { jako instrukcji blokowej (przykładowo { console } to poprawna instrukcja blokowa). Można ten problem rozwiązać, umieszczając całe wyrażenie w nawiasie okrągłym:

({ safe } = {});
// brak błędów

Destrukturyzacja wartości niebędących obiektem zwykłym, obiektem iterowalnym ani tablicą

Próba destrukturyzacji wartości null lub undefined spowoduje błąd typu:

var {blowUp} = null;
// TypeError: null has no properties

Możliwa jest jednak destrukturyzacja typów prostych, takich jak typy logiczne, liczby i łańcuchy, ale wówczas zwracana jest wartość undefined:

var {wtf} = NaN;
console.log(wtf);
// undefined

Może być to zaskakujące, jednak jeśli się nad tym zastanowimy, to przyczyna jest prosta. Podczas korzystania z wzorca przypisania obiektu destrukturyzowana wartość musi być typu, który da się rzutować do postaci obiektu. Większość typów można rzutować do obiektu, lecz nie wartości null i undefined. Jeśli natomiast korzystamy z wzorca przypisania tablicy, wartość musi mieć iterator.

Domyślne wartości

Można również określić wartości domyślne, na wypadek gdyby poddawana destrukturyzacji własność była niezdefiniowana:

var [missing = true] = [];
console.log(missing);
// true

var { message: msg = "Coś poszło nie tak." } = {};
console.log(msg);
//  "Coś poszło nie tak." 

var { x = 3 } = {};
console.log(x);
// 3

(Uwaga od redaktora: aktualnie z tego składnika można korzystać w Firefoksie tylko w pierwszych dwóch omawianych przypadkach. Szczegółowe informacje znajdziesz w opisie błędu 932080).

Praktyczne zastosowania destrukturyzacji

Definiowanie parametrów funkcji

My, programiści, często wolimy udostępniać bardziej ergonomiczne interfejsy API akceptujące parametry będące pojedynczymi obiektami z wieloma własnościami niż zmuszać korzystających z naszego API do zapamiętania kolejności wielu pojedynczych parametrów. Destrukturyzacja pomoże nam uniknąć powtarzania jednoparametrowego obiektu za każdym razem, gdy chcemy odwołać się do jednej z jego własności:

function removeBreakpoint({ url, line, column }) {
  // ...
}

To uproszczony fragment prawdziwego kodu z debuggera JavaScript dostępnego w narzędziach dla twórców witryn przeglądarki Firefox (swoją drogą, i on zaimplementowany jest w JavaScripcie). Naszym zdaniem to bardzo przyjemny wzorzec.

Parametry obiektu konfiguracji

Kontynuując poprzedni przykład, istnieje również możliwość przypisania wartości domyślnych do własności destrukturyzowanych obiektów. Jest to szczególnie pomocne w sytuacji, w której obiekt ma zawierać dane konfiguracyjne, a wiele z własności obiektów ma już określone dobre wartości domyślne. Przykładowo dostępna w jQuery funkcja ajax przyjmuje obiekt konfiguracji jako drugi parametr i można ją przepisać tak:

jQuery.ajax = function (url, {
  async = true,
  beforeSend = noop,
  cache = true,
  complete = noop,
  crossDomain = false,
  global = true,
  // ... więcej danych konfiguracyjnych
}) {
  // ... zrób coś
};

W ten sposób unikamy powtarzania var foo = config.foo || theDefaultFoo; dla każdej własności obiektu konfiguracji.

(Uwaga od redaktora: niestety, w Firefoksie wciąż nie można używać wartości domyślnych w skróconej składni obiektów. Wiem, wiem, poświęciliśmy im kilka akapitów mimo poprzedniej adnotacji. Najnowsze informacje na ten temat dostępne są w opisie błędu 932080).

Destrukturyzacja a protokół iteracji w ES6

W standardzie ECMAScript 6 zdefiniowany jest również protokół iteracji, o którym mówiliśmy w jednym z poprzednich artykułów. Kiedy przeglądamy iteracyjnie obiekt Map (pojawiający się w ES6 dodatek do biblioteki standardowej), otrzymujemy serię par [klucz, wartość]. Taką parę można poddać destrukturyzacji i w ten sposób uzyskać dostęp zarówno do klucza, jak i do wartości:

var map = new Map();
map.set(window, "obiekt globalny");
map.set(document, "dokument");

for (var [key, value] of map) {
  console.log(key + " to " + value);
}
// "[object Window] to obiekt globalny"
// "[object HTMLDocument] to dokument"

Kod przeglądający iteracyjnie tylko klucze:

for (var [key] of map) {
  // ...
}

lub tylko wartości:

for (var [,value] of map) {
  // ...
}

Wiele zwracanych wartości

Choć zwracanie wielu wartości nie jest standardowym składnikiem funkcjonalności JavaScriptu, to można tego dokonać poprzez zwrócenie tablicy i poddanie jej destrukturyzacji:

function returnMultipleValues() {
  return [1, 2];
}
var [foo, bar] = returnMultipleValues();

Można także wykorzystać obiekt jako kontener i nazwać zwracane wartości:

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var { foo, bar } = returnMultipleValues();

Oba te rozwiązania sprawdzają się o wiele lepiej niż kontener tymczasowy:

function returnMultipleValues() {
  return {
    foo: 1,
    bar: 2
  };
}
var temp = returnMultipleValues();
var foo = temp.foo;
var bar = temp.bar;

Są też lepsze od stosowania stylu przekazywania kontynuacji (ang. continuation-passing style):

function returnMultipleValues(k) {
  k(1, 2);
}
returnMultipleValues((foo, bar) => ...);

Importowanie nazw z modułów CommonJS

Nie korzystasz jeszcze z modułów ES6? Wciąż polegasz na modułach CommonJS? Żaden problem! Podczas importowania modułów CommonJS często zdarza się, że dany moduł eksportuje więcej funkcji niż potrzeba. Dzięki destrukturyzacji możemy określić, z których części modułu chcemy skorzystać, tym samym nie zajmując niepotrzebnie naszej przestrzeni nazw:

const { SourceMapConsumer, SourceNode } = require("source-map");

(Jeśli jednak korzystasz z modułów ES6 to wiesz, że podobna składnia jest dostępna w deklaracjach import).

Podsumowanie

Jak widzisz, destrukturyzacja przydaje się do rozwiązywania wielu pojedynczych problemów. W Mozilli mamy już w niej spore doświadczenie. Dziesięć lat temu Lars Hansen zaimplementował destrukturyzację JS w Operze, a nieco później obsługę destrukturyzacji dodał do Firefoksa Brendan Eich. Oficjalnie pojawiła się ona w Firefoksie 2. Wiemy już, że destrukturyzacja towarzyszy programistom JavaScriptu na co dzień, dyskretnie pomagając pisać nieco krótszy i schludniejszy kod.

Powiedzieliśmy kiedyś, że ES6 zmieni sposób, w jaki piszemy kod JavaScript. Mieliśmy wówczas na myśli szczególnie tego rodzaju elementy: proste usprawnienia, których można nauczyć się z osobna. Wszystkie razem w końcu sprawią, że do pracy nad każdym projektem będziesz podchodzić inaczej. Rewolucja poprzez ewolucję.

Nad zaktualizowaniem destrukturyzacji pod kątem ES6 pracował cały zespół ludzi. Szczególne podziękowania należą się Tooru Fujisawie (arai) i Arpadowi Borsosowi (Swatinem) za ich nieoceniony wkład.

Obecnie trwają prace nad zaimplementowaniem destrukturyzacji w przeglądarce Chrome. Obsługa tej techniki z pewnością zostanie również niebawem dodana do innych przeglądarek. Na tę chwilę, by używać łańcuchów szablonowych w kodzie przeglądarkowym trzeba skorzystać z kompilatora Babel lub Traceur.

Raz jeszcze dziękuję Nickowi Fitzgeraldowi za niniejszy artykuł.

W kolejnej odsłonie serii omówimy składnik ES6, za pośrednictwem którego będziemy mogli – ni mniej, ni więcej – pisać skrócony kod czegoś, co jest już dostępne w JS. Czegoś, co od zawsze stanowiło jeden z kluczowych elementów tego języka. Czy mogłoby cię to w ogóle zainteresować? Czy nieco krótsza składnia to coś, czego nie możesz się doczekać? Mógłbym się założyć, że odpowiedź brzmi „tak”, ale niczego nie obiecuję. Odwiedzaj nas zatem często, by przekonać się samemu i szczegółowo poznać funkcje strzałkowe w ES6.

Jason Orendorff

Redaktor serii ES6 bez tajemnic

Autor: Nick Fitzgerald i Jason Orendorff

Źródło: https://hacks.mozilla.org/2015/05/es6-in-depth-destructuring/

Tłumaczenie: Joanna Liana

Treść tej strony dostępna jest na zasadach licencji CC BY-SA 3.0

Zobacz również:

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *