ECMAScript 6 wydany!
Można zrozumieć fakt, że większość ludzi bardziej pasjonuje się informacją o utworzeniu projektu WebAssembly niż tym, że właśnie po długich i żmudnych negocjacjach udało się zatwierdzić nową wersję (już szóstą) standardu ECMAScirpt (17 czerwca). Nie każdy jednak rozumie, że tak szybka zgoda wiodących firm IT, co do wspólnego rozwijania wasm (skrót od WebAssembly), przyszła na fali udanej współpracy przy zatwierdzaniu standardu, będącego podstawą JavaScriptu, jedynego języka działającego po stronie przeglądarek. Są to więc jak najbardziej powiązane ze sobą wątki. Owa zgoda nie przyszła tak łatwo jak to teraz wygląda.
Prace nad ECMAScript 6 ciągnęły się dość długo (wystartowały w 2010r.). Firma Google przez dłuższy czas optując za bardzo głębokimi zmianami, próbowała stworzyć swój własny język Dart niekompatybilny z JavaScript. Tymczasem Microsoft zainwestował w język TypeScript, który to wprowadzając duże zmiany w sposobie programowanie (przede wszystkim typowanie) nie zrywał kompatybilności z masą kodu napisanego w standardowym JavaScript. Mozilla w tym czasie skupiała się na przyspieszaniu kodu działającego w przeglądarce dzięki swojemu ASM.js. Brak zgody mógł mieć bardzo przykre konsekwencje dla całej branży IT związanej z rozwiązaniami WWW. Pojawienie się niekompatybilnego Darta mimo, że umożliwiał on kompilację do JS, stanowiło zagrożenie, że niektóre przeglądarki będą działać inaczej niż pozostałe. Mogłoby się np. okazać, że piszący w Darcie nie zawsze dołączą do strony, oprócz samego skryptu w Dart, wersji skompilowanej do JS. A jednak po stronie Google coś jakby pękło i nastąpiły zmiany. Nagle spodobał im się język TypeScript i całkowita zmiana w standardzie ECMAScript przestała być konieczna. Przy okazji Google zauważyło, że to co robi Mozilla z przyspieszaniem JS przez ASM.js i kompilowaniem do niego kodu z C/C++ ma więcej sensu niż ich walka o NaCl (Native Client), czyli umożliwianiem uruchamiania kodu natywnego w przeglądarce. Moim zdaniem NaCl zbytnio przypomina ActiveX. Łatwo się domyśleć, że uczynienie tego rozwiązania bezpiecznym musi wymagać olbrzymiej pracy, a jej wynik ciągle jest niepewny. Tymczasem technologia WebAssembly może spokojnie zastąpić NaCl, dając większe bezpieczeństwo, niewiele mniejszą wydajność, a dodatkowo pozwalając zachować kompatybilność aplikacji w wielu różnych przeglądarkach.
Koniec prac nad ES6 nie kończy oczywiście drogi rozwoju tego standardu. Już wiadomo nad czym będą się toczyć pracę przy wersjach następnych. Między innymi SIMD, niedawno wprowadzony eksperymentalnie do Firefoxa ma być częścią nowego standardu. Ciekawie wygląda propozycja wprowadzenia opcjonalnego typowania znanego z TypeScriptu. Wszystko wskazuje na to, że dalsze prace dzięki osiągniętemu porozumieniu pójdą dużo szybciej niż to miało miejsce w ostatnich latach. Duży wpływ na to co będzie się działo z ECMAScirpt będzie prawdopodobnie mieć postęp prac nad WASM. Niektórzy wieszczą upadek JavaScriptu i zastąpienie go kompilatorami do WASM innych popularnych języków programowania. IMHO kierunek całkiem możliwy aczkolwiek mało prawdopodobny. Podstawą WebAssembly na początek będzie zapewne ASM.js, a ten opiera się o JavaScript. Zatem długo ten ostatni będzie podstawą tworzenia kodu działającego po stronie klienta. Poza tym język ten jest zbyt popularny, aby miał nagle zostać zastąpiony przez inne. Jednak otwiera się nowa perspektywa dla osób, które chciałyby pisać wszystko w jednym języku (po stronie serwera i przeglądarki) i niekoniecznie miałby to być JavaScript.
Nowości w JavaScript dostępne w Firefoxie
Na większe zmiany musimy jednak jeszcze trochę poczekać, a tymczasem warto zacząć myśleć nad zastosowaniem nowych elementów JavaScriptu, które pojawiły w standardzie o numerze 6. Duża ich część została już wprowadzona przez główne przeglądarki. Tutaj skupię się na Firefoxie. Składnia używa frameworka do testowania Jasmine.
Strzałka funkcji anonimowej
var multiply = (x, y) => x*y; expect(typeof multiply).toEqual("function"); expect(multiply(11, 12)).toEqual(132); expect(multiply(0, 1234)).toEqual(0); expect(multiply(0.1, 1.3)).toBeLessThan(0.14);
Dość podobna składnia do innych znanych języków. Przy braku parametrów wstawiamy puste nawiasy.
Łamanie napisów. Tworzenie napisów ze zmiennych.
var str = `W JavaScript string nie łamie linii`; var imie = "Jan"; var nazwisko = "Kowalski"; var format = `Witaj ${imie}ie ${nazwisko}!`; expect(str).toEqual('W JavaScript string\n nie łamie linii'); expect(format).toEqual("Witaj Janie Kowalski!"); expect('abc'.repeat(2)).toEqual('abcabc');
Łamanie linii i scalanie stringów ze zmiennych działa tylko przy użyciu znaku grawis (odwrócony apostrof, tam gdzie tylda ~).
Zbiorcze inicjowanie danych tabelami
var [x, , , y] = [1, 2, 3, 4]; var [z = 8] = []; expect(x).toEqual(1); expect(y).toEqual(4); expect(z).toEqual(8);
Zmienna z jest domyślnie inicjowana na 8, a ponieważ w tablicy nie ma elementów więc nie ustawimy jej wartości na inną niż domyślna.
Stałe
const pi = 3.14; expect(Math.round(2*pi*10)).toEqual(63); //pi = 3.141; - błąd
Parametry funkcji
function system(a, b = 10) { var str = ""; while(a != 0){ str = (a % b) + str; a = Math.floor(a / b); } return str; } expect(system(128, 2)).toBe("10000000"); expect(system(1234)).toBe("1234");
Domyślny parametr funkcji. [code=javascript]function tablica(a, ...b) { return a + b.join("-"); } expect(tablica("funkcja:","tablico","zmienna")).toEqual("funkcja:tablico-zmienna"); [/code]
Dowolna ilość parametrów na liście odbierana w funkcji jako tablica.
function szereg(a, b, c) { var q = `${a}, ${b} czy ${c}?`; q = q[0].toUpperCase() + q.substring(1, q.length); return q; } expect(szereg(...['kawa', 'herbata', 'woda'])).toBe('Kawa, herbata czy woda?');
Zamiana tablicy na listę argumentów.
Kolekcje
var array = [1, 2, 3, 4]; array.fill(5); expect(array).toEqual([5,5,5,5]); array.fill(2,2); expect(array).toEqual([5,5,2,2]); array.fill(1,1,2); expect(array).toEqual([5,1,2,2]); var napis = "tab"; array = [1, 2, 3, 4]; expect(Array.from(napis)).toEqual(["t", "a", "b"]); expect(Array.from(array, x => x*10)).toEqual([10, 20, 30, 40]);
Metoda wypełniania tablicy fill z parametrami (czym wypełnić, od którego miejsca, do którego miejsca). Metoda statyczna from pozwala stworzyć nową tablicę na podstawie argumentu innej kolekcji, którą również możemy wstępnie przetworzyć.
var m = new Map(); m.set("0", "pierwszy"); m.set(1, "drugi"); m.set({}, "trzeci"); m.set("obj", {cont:"czwarty"}); var itMap = m.keys(); expect(itMap.next().value).toEqual("0"); expect(itMap.next().value).toEqual(1); expect(itMap.next().value).toEqual({}); expect(itMap.next().value).toEqual("obj"); itMap = m.values(); expect(itMap.next().value).toEqual("pierwszy"); expect(itMap.next().value).toEqual("drugi"); expect(itMap.next().value).toEqual("trzeci"); expect(itMap.next().value.cont).toEqual("czwarty"); expect(m.get("obj").cont).toEqual("czwarty"); expect(m.has(1)).toBe(true); var numbers = new Array(); m.forEach((v, k) => numbers.push(v)); expect(numbers).toEqual(["pierwszy", "drugi", "trzeci", {cont:"czwarty"}]);
Mapy umożliwiają przechowywanie na zasadzie klucz-wartość. Zarówno klucz jak i wartość może być dowolnego typu. Możemy pobierać zestaw kluczy (keys()) i wartości (values()). Zwracają one iterator do mapy, przez który przechodząc, mamy możliwość przeglądać elementy.
var s = new Set(); s.add(34); s.add(34); expect(s.size).toBe(1); s.add("nowy"); expect(s.size).toBe(2); expect(s.has("nowy")).toBe(true); s.delete(34); expect(s.size).toBe(1); expect(s.has(34)).toBe(false); s.clear(); expect(s.size).toBe(0); s.add(10); s.add(1); s.add(20); s.add(5); s.add(4); s.add(16); array = []; for(e of s){ array.push(e); } expect(array).toEqual([10,1,20,5,4,16]);
Set jest rodzajem tablicy bez powtarzania się tych samych wartości. Dodanie drugiej takiej samej wartości nie powiększa rozmiaru kolekcji. W ostatniej pętli for widzimy nową składnie dla kolekcji, w której zwracany jest kolejny element, a nie jak w wersji z in jego numer.
var float32 = new Float32Array(2); float32[0] = 42; expect(float32[0]).toEqual(42); expect(float32.length).toBe(2); expect(float32.BYTES_PER_ELEMENT).toBe(4); var arr = new Float32Array([21,31]); expect(arr[1]).toBe(31); var x = new Float32Array([21, 31]); var y = new Float32Array(x); expect(y[0]).toEqual(21); // 21 var buffer = new ArrayBuffer(16); var z = new Float32Array(buffer, 0, 4); expect(z.length).toBe(4); expect(z[2]).toBe(0);
Istnieje szereg rodzajów tablic pozwalających przechowywać szczególne typy danych prostych. Pomocnicze tablice ArrayBuffer i DataView pozwalają pracować bezpośrednio z danymi binarnymi - wczytywać je i zamieniać na inne typy.
Generator
function* idMaker(){ var index = 0; while(index < 3) yield index++; } var gen = idMaker(); expect(gen.next().value).toBe(0); expect(gen.next().value).toBe(1); expect(gen.next().value).toBe(2); expect(gen.next().value).toBe(undefined);
Oznaczone gwiazdką za pomocą słowa kluczowego yield umożliwiają tworzenie kolekcji na bieżąco, bez zajmowania dużej ilości pamięci. W wyniku ich działania dostajemy iterator do przechodzenia przez generowane wartości.
var ws = new WeakSet(); var obj1 = {a:1}; var obj2 = {a:1}; var obj3 = new Object(); obj3.a = 8; var fun = function(x) {return x*5;} ws.add(obj1); ws.add(obj2); ws.add(obj3); ws.add(fun); expect(ws.has(obj2)).toBe(true); expect(ws.has(obj1)).toBe(true); expect(obj1).toEqual(obj2); expect(obj1).toEqual({a:1}); expect(ws.has({a:1})).toBe(false); expect(ws.has(fun)).toBe(true); ws.delete(obj3); expect(ws.has(obj3)).toBe(false); var obj5 = {a:67}; obj5.sub = {b:45}; ws.add(obj5.sub); expect(ws.has(obj5.sub)).toBe(true); delete obj5.sub; expect(ws.has(obj5.sub)).toBe(false);
Przechowuje tylko unikalne obiekty. Nie przechowuje typów prostych. Jest to przedstawiciel tablicy, która nie blokuje w garbage collector referencji do przechowywanego obiektu. Przez co obiekt ten znajduje się w kolekcji, dopóki istnieje do niego inna referencja. Usunięcie jej powoduje, tak jak widać w końcówce kodu, zniknięcie elementu z tablicy. Nie pozwala ona pobrać wszystkich wartości i nie ma pola
Cechy niedostępne lub dostępne w gałęzi Nighty
Aby sprawdzić składnie nowych elementów nie wspieranych przez Firefoxa dodamy skrypt, który analizuje i podmienia kod JS na stronie. Musi on znajdować się bezpośrednio w stronie w znaczniku (niżej niż skrypty parsujące). Na samym początku body dodajemy skrypty parsujące:
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script> <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
Słowo kluczowe class
class Vektor2D { constructor(x, y){ this.x = x; this.y = y; this.unit = ""; } len() { return Math.sqrt(this.x*this.x + this.y*this.y); } showLen(){ document.querySelector('#classInfo').innerHTML = this.len()+ this.unit; } }; class Force extends Vektor2D { constructor(x, y){ super(x, y); this.unit = "N"; } }; var f = new Force(40, 30); f.showLen();
Kod zakłada istnienie na stronie akapitu z id classInfo. Jak widać wygląda to na podobną do innych popularnych języków obiektowych definicję klasy z metodami i konstruktorem dwu‑parametrowym. Dziedziczenie jest realizowane dzięki słowu kluczowemu extends. We wnętrzu konstruktora Force uruchamiamy konstruktor rodzica używając słowa super. Do pełni szczęścia brakuje tylko opcjonalnego typowania ;)
Zmienna let
Zmienna deklarowana poprzez słowo let zachowuje się, jeśli chodzi o zasięg, podobnie do zmiennych w językach pochodzących od C.
if(true){ let tmp1 = "let!"; var tmp2 = "var!"; } let element = document.querySelector('#var'); element.innerHTML = tmp2; //element = document.querySelector('#let'); //element.innerHTML = tmp1;
Zmienna var tmp2 mimo, że zadeklarowana w nawiasach ograniczających zwrotnicę if jest widoczna po zamknięciu nawiasu klamrowego. Nie jest to naturalne zachowanie dla większości programistów, przyzwyczajonych do niewidoczności zmiennej po wyjściu bloku kodu. W taki sposób zachowuje się tmp1 zadeklarowana ze słowem let. Obecnie próba odwołania do niej spowoduje błąd naszego parsera.
Brakuje jeszcze szeregu metod w różnych obiektach. Aktualny stan zobaczysz na stronie: developer.mozilla.org
Można też przetestować własną przeglądarkę korzystając z testów napisanych przez firmy pracujące nad nowym standardem (nie ma nic wspólnego z przykładami kodu w tym artykule): LINK W obecnie używanej przeze mnie wersji Firefoxa 38 (Ubuntu 14.04 LTS) nie przeszło 230 z 11552 testów. Ponoć najlepiej wypada pod tym względem IE. (Dla tych co nabrali ochoty do test: może on zająć nawet kilka godzin).
Podsumowanie
Zmian jest sporo i warto się z nimi zapoznać. Wpis nie wyczerpuje ich wszystkich w tym tak ekscytujące jak Promisses, Tail Calls czy Proxies. Z wprowadzaniem ich w życie można spodziewać się jednak opóźnienia. Podejrzewam, że nie prędko do produkcji wejdzie pisanie kodu w EM6. Trudno liczyć na to, że w szybkim czasie wszystkie urządzenia zaczną go obsługiwać. Szczególnie w przeglądarkach mobilnych, które są często archaicznymi tworami sprzed dekady z powodu braku aktualizacji systemu. Pozostaje liczyć na to, że ilość takiego sprzętu będzie na tyle mała, że warto będzie zacząć pisać w nowym dialekcie. Pomóc nam mają dołączane do stron biblioteki, zapewniające kompatybilność dla przeglądarek nie posiadających możliwości interpretacji kodu ECMAScript 6. Niektóre cechy podobają mi się na tyle, że postaram się jak najszybciej zacząć go używać.
PS. Kod przykładów można pobrać z gita: https://github.com/mikolajs/ecmascript6-test.git