ES6 – modules

javascript

Do tej pory język JavaScript nie posiadał możliwości modułowego podejścia do architektury aplikacji. Mam oczywiście na myśli podejście natywne, bez używania zewnętrznych bibliotek. Wraz z implementacją ECMAScript2015 w przeglądarkach, zmieni się to na lepsze. Otrzymamy dzięki temu kolejne API, które pozwoli nam tworzyć aplikacje w bardziej ustandaryzowany sposób.


Zobacz całą serię: Let-s talk about ECMAScript 2015


Do tej pory musieliśmy się zadowalać dwoma standarami: CommonJS oraz AMD. Są to dwa najpopularniejsze sposoby w podejściu do modułowości w JavaScript, jednak zupełnie ze sobą niekompatybilne.

CommonJS – znane jest z ekosystemu Node.js. Dedykowane serwerom aplikacji, wspiera synchroniczne ładowanie modułów. Składnia oparta jest na niewielkim API, skupionym na słowach kluczowych export oraz require.

AMD – to podejście zaimplementowane zostało w bibliotece RequireJS. Dedykowane jest przeglądarkom, które wzbogacone zostają o asynchroniczne ładowanie modułów. Ceną – w porównaniu z CommonJS – jest jednak bardziej skomplikowane API.

Zadaniem ES6 jest połączenie obu znanych dzisiaj standardów oraz zapewnienie poprawnego działania zarówno po stronie serwerowej, jak i klienckiej. Otrzymujemy łatwe oraz spójne API do asynchronicznego i konfigurowalnego ładowania modułów w naszych aplikacjach.

Model asynchroniczny zakłada również, że kod nie zostaje wykonany póki wszystkie potrzebne moduły nie zostały załadowane oraz zinterpretowane.

named export

Jeden moduł posiadać może wiele nazwanych eksportów, gdzie każdy pozwala udostępniać na zewnątrz pojedyncze zmienne, obiekty, czy funkcje.

export function multiply (x, y) {
  return x * y;
};

Nie ma również problemu, by powyższą funkcję przypisać do zmiennej i eksportować na zewnątrz:

var multiply = function (x, y) {
  return x * y;
};

export { multiply };

Jedyne o czym musimy pamiętać, to nawiasy klamrowe, którymi musi być otoczone eksportowane wyrażenie.

Nic nie stoi również na przeszkodzie, by eksportować całe obiekty. Tutaj także musimy pamiętać o nawiasach klamrowych.

export hello = 'Hello World';
export function multiply (x, y) {
  return x * y;
};

// === OR ===

var hello = 'Hello World',
    multiply = function (x, y) {
      return x * y;
    };

export { hello, multiply };

import

Przejdźmy teraz do importu modułów. Wyobraźmy sobie, że mamy plik export.js, gdzie eksportowany jest obiekt z poprzedniego przykładu. Następnie tworzymy plik import.js, w którym importujemy owy obiekt. Posłużymy się tutaj składnią:

import { ... } from ...

Nawiasy klamrowe określają nam co chcemy zaimportować, a w kolejnej części wyrażenia podajemy skąd chcemy (z jakiego modułu) dokonać importu.

I tak, by zaimportować wyrażenie ‘hello’ z powyższego przykładu napiszemy:

import { hello } from './export';

W przypadku podawania nazwy modułu, możemy pominąć rozszerzenie .js. Podobnie jest w CommonJS oraz AMD.

Za zaimportowanie obu wartości posłuży nam poniższy kawałek kodu:

import { hello, multiply } from './export';

Każda z importowanych wartości może na dodatek posiadać swój alias:

import { multiply as pow2 } from './export';

Jeżeli zależy nam na imporcie całego modułu, wystarczy, że użyjemy wyrażenia typu wildcard (*):

import * as all from './export';

Musimy w tym wypadku skorzystać z aliasu, by móc się później odwoływać do metod w naszym module.

default export

Poza wartościami nazwanymi, eksportować możemy również wartość domyślną. Przydaje się to w wielu sytuacjach, ponieważ zwykle eksportujemy jeden model per moduł. Domyślny export jest więc najważniejszą wartością naszego modułu. Warte zapamiętania, biorąc pod uwagę zasadę SRP (Single responsibility principle).

Oczywiste jest również, że moduł posiadać może tylko jeden domyślny eksport. By stworzyć taki eksport w naszym module, musimy dodać słowo kluczowe default zaraz po export:

export default function (x, y) {
  return x * y;
};

Do importowania domyślnego wyrażenia nie musimy używać nawiasów klamrowych. Możemy ponad to nazwać (alias) importowane wyrażenie jak nam się podoba:

import multiply from './export';

// === OR ===

import pow2 from './export';

// === OR ===
...

Każdy moduł posiadać może zarówno eksporty nazwane, jak i jeden eksport domyślny:

// export.js
export hello = 'Hello World';
export default function (x, y) {
  return x * y;
};

// import.js
import pow2, { hello } from './export';

Domyślne wyrażenie możemy również zaimportować jak pozostałe eksporty nazwane. Importować w tym momencie będziemy wyrażenie default:

// export.js
export default function (x, y) {
  return x * y;
};

// import.js
import { default } from './export';

API modułów zakłada, że praca z nimi odbywa się z poziomu kodu i jest w pełni konfigurowalna. Specyfikacja przewiduje, że moduły zapewniają dynamiczne ładowanie oraz izolację od globalnej przestrzeni nazw.

Przy okazji modułów, warto wspomnieć o projekcie SystemJS. Jest to uniwersalna biblioteka do obsługi modułów ze specyfikacji ECMAScript2015 oraz CommonJS i AMD. Współpracuje także z transpilerami Traceur i Babel, zapewniając w ten sposób modułowość w aktualnych wersjach przeglądarek, które nie oferują wsparcia nowego standardu ES6.

ES6 – default + rest + spread

javascript

Biorąc pod uwagę całe lata narzekań, ECMAScript 2015 przynosi nam wiele nowości, czego rezultatem jest spora ilość usprawnień. Sprawiają one, że pisanie kodu w JavaScript jest bardziej intuicyjne oraz po prostu szybsze. Spójrzmy zatem na nowy sposób przekazywania parametrów do funkcji.


Zobacz całą serię: Let-s talk about ECMAScript 2015


default

Jest to małe, ale bardzo przydatne usprawnienie w przekazywaniu parametrów do funkcji. Wiemy przecież, że funkcje w języku JavaScript pozwalają na przekazanie do nich dowolnej ilości parametrów. Z pewnością nie raz spotkaliście się z podobnym kodem:

function inc(number, increment) {
  // set default to 1 if increment not passed
  // (or passed as undefined)
  increment = increment || 1;
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

Operator logiczny OR (||) przypisuje w tym wypadku wartość 1 do zmiennej increment, ponieważ lewy człon wyrażenia to undefined, czyli false.

ES6 pozwoli pozbyć się tego typu wyrażeń, wykorzystując w tym celu parametry z domyślnymi wartościami. Funkcja inc z wykorzystaniem składni ES6 prezentuje się zatem następująco:

function inc(number, increment = 1) {
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

Tym samym parametry z wartościami domyślnymi są opcjonalne.

Możemy również definiować domyślne wartości parametrów, które występują w środku sygnatury funkcji:

function sum(a, b = 2, c) {
  return a + b + c;
}
console.log(sum(1, 5, 10));         // 16 -> b === 5
console.log(sum(1, undefined, 10)); // 13 -> b as default

Wykorzystanie wartości domyślnej następuje w tym wypadku w momencie, gdy jako wartość parametru podamy undefined.

Wartości domyślne nie muszą być typami prymitywnymi. Jako domyślną wartość możemy przypisać choćby rezultat wykonania funkcji:

function getDefaultIncrement() {
  return 1;
}
function inc(number, increment = getDefaultIncrement()) {
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

rest

Spróbujmy napisać wcześniejszą funkcję sum tak, by brała pod uwagę wszystkie parametry jakie do niej przekażemy. By zachować czystość kodu, pominiemy tutaj walidację. Jeżeli próbowalibyśmy użyć do tego zadania składni dobrze znanej z ES5, to z pewnością otrzymalibyśmy coś takiego:

function sum() {
   var numbers = Array.prototype.slice.call(arguments),
       result = 0;
   numbers.forEach(function (number) {
       result += number;
   });
   return result;
}
console.log(sum(1));             // 1
console.log(sum(1, 2, 3, 4, 5)); // 15

Przy takim podejściu nie wiemy od razu co robi dana funkcja. Nie wystarczy, że spojrzymy na jej sygnaturę. Musimy jeszcze przeskanować jej ciało, znaleźć obiekt arguments i wywnioskować, że chodzi tutaj o zsumowanie wszystkich parametrów przekazanych do funkcji.

Składnia ES6 znacznie lepiej radzi sobie z podobnym problemem . Wprowadza ona pojęcie rest parameters, gdzie parametr poprzedzony (trzema kropkami) staje się tablicą, do której “wpadają” wszystkie pozostałe parametry przekazane do funkcji.

Obiekt arguments zawierać będzie wszystkie parametry funkcji - te nazwane i nie.

Możemy użyć nowej składni ECMAScript 2015 i przepisać funkcję sum:

function sum(…numbers) {
  var result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}
console.log(sum(1)); // 1
console.log(sum(1, 2, 3, 4, 5)); // 15

Jest jednak jedno ALE: nie możemy posiadać więcej niż jednego parametru typu …rest. W przeciwnym razie otrzymamy błąd jak niżej:

function sum(…numbers, last) { // causes a syntax error
  var result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}

spread

Kolejne wyrażenie, to spread, które przypomina trochę konstrukcję rest ze względu na swoją notację z trzema kropkami. Dzieli tablicę na poszczególne wartości, które przekazywane są jako osobne parametry do ciała funkcji.

Po raz kolejny zdefiniujmy funkcję sum, do której przekażemy wyrażenie typu spread:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum(…args)); // 6

Odpowiednik takiego kodu w ES5, to:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum.apply(undefined, args)); // 6

Zamiast więc używać funkcji apply możemy od teraz posługiwać się “rozbiciem” tablicy na osobne parametry. Rozwiązanie prostsze i według mnie również bardziej czytelne.

Ciekawostką jest fakt, że wyrażenie spread możemy łączyć ze standardowym sposobem przekazywania parametru do funkcji:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2];
console.log(sum(…args, 3)); // 6

Rezultat jest dokładnie taki sam jak w poprzednim przykładzie. Tym razem tablica z dwiema wartościami zostaje rozbita na dwa parametry odpowiadające ab, zaś trzecim parametrem jest liczba 3, która odpowiada zmiennej c.