Wpis z mikrobloga

Dopiero co ukazała się świeża, jeszcze ciepła specyfikacja ES6 (zwanego także ES2015), natomiast dziś przygotowałem dla Was wpis w którym wyjaśnię w jaki sposób słaba jakość kodu ExtJS sprawiła, że jedna z części ES6 rozwalała wiele stron na nim opartych, oraz jaki brzydki hack zastosowano, żeby tego uniknąć. Zapraszam do czytania.



Dynamic scoping
Przy okazji jednego z wpisów popełniłem już dość obszerny fragment odnośnie tego jak działa scoping w JSie. Uzupełnijmy go o informację, że w JavaScript dostępne są dwa typy scope'ów – statyczny oraz dynamiczny.

Statyczny (reprezentowany przez lexical environment record) jest tym dobrym - kompilator jest go w stanie zanalizować już w trakcie kompilacji kodu, co daje pewien zysk w postaci lepszej optymalizacji. Z drugiej zaś strony stoi scoping dynamiczny, tworzony dopiero w trakcie działania programu. Ten reprezentowany jest przez object environment record, i w praktyce sprowadza się do tego, że do scope chaina dodawany jest zwykły, JavaScriptowy obiekt. A że obiekty w JavaScript są bardzo dynamiczne, to dynamic scoping dość mocno utrudnia optymalizację. Takich dynamicznych scope’ów mamy w ES5 trzy: scope globalny, reprezentowany przez obiekt globalny (w przeglądarce to będzie window, w node global, a ogólnie jest to obiekt dostępny pod this w kontekście globalnym), block catch oraz with statement. Skupimy się na tym ostatnim, ponieważ to ono jest winowajcą całego zamieszania.

With statement
W dużym skrócie, taki kod:

with(obj) { … }
spowoduje że przed wykonaniem kodu z bloku, do scope chain dodany zostanie dynamic scope reprezentowany przez obiekt obj. Oznacza to, że jeśli wewnątrz bloku odwołamy się do jakiejś zmiennej, powiedzmy o nazwie x, to najpierw binding o tej nazwie zostanie wyszukany wśród własności obiektu obj. Tym samym ten kod:

var x = 1;
var obj = { x: 2 };
with(obj) {
console.log(x);
}
wypisze nam do konsoli 2 a nie 1. Gdyby natomiast własność x nie istniała wewnątrz obiektu obj, wtedy binding o tej nazwie zostałby znaleziony scope wyżej, a w konsoli ujrzelibyśmy 1.

Bardzo dużym problemem w kontekście with statement są prototypy. Wszystko dlatego że zanim kod przejdzie od analizowania własności obiektu do analizowania otaczających go scope’ów, analizowany jest także cały łańcuch prototypów danego obiektu. A to utrudnia zarówno analizowanie go przez człowieka, jak i przez kompilator.

Niestety zgodnie z regułą don’t break the web nie można było od tak sobie usunąć go z języka, w pewnym sensie został on zatem uznany za deprecated - od wersji ES5 jego użycie w trybie ścisłym powodowało rzucenie błędu SyntaxError.

ExtJS, czyli jak napisać paskudną, jedną linijkę…
Niestety nie wszyscy programiści są świadomi konsekwencji, więc tworzą kod wątpliwej jakości. W jednym z modułów do popularnego frameworka ExtJS znalazł się taki oto fragment:

WITHVALUES = 'with(values){ ',
Przy czym interesuje nas tylko środek stringa (moduł ten odpowiada za kompilację szablonów), a dodatkową informacją niech będzie fakt, że values to może być tablica.

Czaicie to?

Jedna prosta linijka, dwa wyrazy i dwa niezłe fuckupy. Nie dość, że użyto odradzanego with statement, to jeszcze użyto go na tablicy, a więc użyto tablicy w taki sposób w jaki używa się obiektu. Paskudne. Ten fragment kodu znalazł się w module ExtJS, skąd trafił do części postawionych na nim stron.

..która zepsuje internet
Jedną z ważniejszych rzeczy, które zostały dodane w ES6, są kolekcje danych wraz z mechanizmem iteracji po tych kolekcjach, czyli iteratorach. Tablica jest jedną z takich kolekcji i ona również ma zaimplementowane nowe metody, udostępniane przez interfejs iterable. Jedną z takich metod jest metoda values(), która zwraca po prostu kolejne wartości z kolekcji – w przypadku tablicy, kolejne jej elementy.

5 czerwca 2013 do kodu Firefoxa Nightly [zostaje wysłany commit]( http://hg.mozilla.org/mozilla-central/rev/5d35dc039af7), który dodaje metodę Array.prototype.values. Zmiana jest niewielka, ponieważ values() zwraca dokładnie ten sam iterator, co metoda Array.prototype[Symbol.iterator]. Aktualizacja zostaje rozesłana do wielu sadystów używających wersji Nightly i już kilka dni później na Bugzillę trafiają bugi informujące o wystąpieniu regresji.

Dochodzenie nie trwa długo i dość szybko winnym zostaje wskazany wymieniony już przeze mnie kod z ExtJS, a dokładnie with(values). Otóż jeśli values będzie tablicą, a wewnątrz tego bloku with znajdzie się odwołanie do values, czyli przykładowo taki kod:

var values = [];

with(values) {

var a = values[0];
to co stanie się teraz? Ano, z powodu użycia with binding o nazwie values będzie wyszukiwany od obiektu values czyli od naszej tablicy. Jeśli nie zostanie znalezione wśród jej własności, to zgodnie z tym, co mówiłem wcześniej, zostanie sprawdzony prototyp tego obiektu, a więc w przypadku tablicy - Array.prototype. A tam już czeka własność o nazwie values, której wcześniej nie było. Więc tak naprawdę próbujemy odczytać klucz 0 nie z tablicy, a z funkcji której zadaniem jest zwrócić tablicowy iterator.

TC39, mamy problem
Pojawia się problem. Jedną z fundamentalnych zasad przy tworzeniu nowych wersji EcmaScript, związanych z tym jak jest wydawany, jest „don’t break the web”. A tutaj mamy sytuację kiedy sieć została zepsuta, ponieważ jedna drobna zmiana sprawia, że część stron może z dnia na dzień przestać działać. Co prawda ExtJS nie ma zbyt dużego udziału w rynku (szacuje się go na 0.1%), jednak ta wartość została uznana za i tak zbyt dużą. Dyskusje nt. rozwiązań trwały aż w końcu wybrano jedno z nich. Jest ono moim zdaniem brzydkie, bo to bardziej takie hackowanie, ale lepszego chyba nie było.

Rozwiązanie
Aby rozwiązać problem, posłużono się symbolami. Tl;dr – symbole to nowy typ w EcmaScript, każdy ma unikalną wartość i mogą być używane jaki klucz w obiektach. Co więcej, bardzo dużo zachowań języka zależy teraz także od metod ukrytych pod specjalnymi symbolami (well-known symbols), które są umieszczone w konstruktorze Symbol. I tak Symbol.hasInstance steruje zachowaniem operatora instanceof, Symbol.iterator mówi w jaki sposób dany obiekt ma być iterowany, Symbol.toStringTag mówi co zwraca Object.prototype.toString wykonany w kontekście danego obiektu (pełna lista tutej). W związku z problematycznym with został dodany także Symbol.unscopables.

Symbol.unscopables służy do bezpośredniego ukrywania własności obiektu przed wyszukiwaniem bindingów w dynamic scope’ach. To znaczy ni mniej, ni więcej tyle, że używając tego symbolu jawnie wskazujemy, które własności w danym obiekcie mają być pominięte wewnątrz dynamic scope. A więc właśnie w with statement:

var a = 0;
var objProto = { a: 1 };
var obj = Object.create(objProto);
obj.a = 2;
obj[Symbol.unscopables] = { 'a': true };
with(obj) {
console.log(a);
// --> 0, a nie 2 (bezpośrednia własność) czy 1 (własność dziedziczona z prototypu)
}
try-catch oraz obiekt globalny, mimo że są zakresami dynamicznymi, nie obsługują Symbol.unscopables - wynika to bezpośrednio z 8.1.1.2.1 (krok 6.)

Konkluzja
Specyfika JavaScriptu nie pozwala na frywolne szastanie rzeczami, które są zawarte w języku. W takiej np. Javie czy PHP możemy coś oznaczyć jako deprecated żeby następnie za, powiedzmy, 2 wydania tę rzecz zupełnie wyrzucić. Ci, którzy zdecydują się na aktualizację, będą musieli dane fragmenty kodu przepisać. Ci, którzy tego nie zrobią, nie będą musieli nic robić a wszystko nadal będzie działać tak samo.

Z JS jest inaczej. Przy wprowadzaniu każdej nowej rzeczy trzeba mieć świadomość, że zostanie ona z tym językiem aż do jego śmierci. Trzeba także analizować czy nowe rzeczy nie stoją w jakimś konflikcie z tymi, które w języku już są. To jest trudne, zapewne dość mocno spowalnia rozwój samego JSa, a także grozi nieprzewidywanymi regresjami i koniecznością ich późniejszego łatania – także takimi brzydkimi fixami. Czy można było zrobić to lepiej? Ciężko powiedzieć. Rozważano zmianę nazwy metody, rozważano olanie problemu i podejście „śmierć frajerom którzy nie zaktualizują sobie ExtJS”. Ostatecznie zdecydowano się na taką wersję, a więc – prawdopodobnie – była to najlepsza, jaką wymyślono.

W razie czego zachęcam do zadawania pytań.

Źródła:
https://esdiscuss.org/topic/array-prototype-values-compatibility-hazard
https://esdiscuss.org/topic/removing-array-prototype-values
http://www.2ality.com/2011/06/with-statement.html
https://bugzilla.mozilla.org/show_bug.cgi?id=883914
https://bugzilla.mozilla.org/show_bug.cgi?id=881782
https://hg.mozilla.org/mozilla-central/rev/5d35dc039af7
http://docs.sencha.com/extjs/4.0.7/source/XTemplate.html

#programowanie #javascript #webdev #dlugipost #marmiteopowiadaojs
  • 17
z góry bardzo przepraszam za wciskanie tutaj wielu angielskich terminów takich jak scope, czy binding.


@Marmite: Nie przepraszaj za uƶywanie zrozumiałej terminologii. Pamiętaj ƶe framework to po polsku "zręb" :/
@KrzaQ2: A myślałem, że jak ktoś narzeka na polskie tłumaczenia książek, to jednak zazwyczaj przesadza :0

A przeprosiłem bo prywatnie nienawidzę i piętnuję zawsze jakiekolwiek formy ponglisha - a w takiej dziedzinie jak programowanie, to po prostu się nie da niestety. Więc musiałem to zrobić, żeby być w zgodzie ze swoim sumieniem.
@KrzaQ2: No nie wiem... Wrzucam jednak zbyt rzadko jak na bloga. No i nie uważam się za jakiegoś guru. Pewnie by leżał odłogiem :S może jak będe mieć więcej czasu...
@Marmite: Spróbuj. Jak będą ciekawe teksty po Polsku to sam chętnie zajrzę. Głównie czytam po ang i trochę brakuje mi technicznych blogów w rodzimym języku.
@regis3: No to podobnie jak z polskim - to też nie skończy się żadnym sukcesem. Jeśli ktoś chce czytać o JS na poziomie specyfikacji, to na pewno nie po polsku.
@regis3: O tym mówię, jeśli ktoś jest już na tyle zaawansowany, żeby interesować się JSem na takim dość niskim, technicznym poziomie, to najpewniej i tak już zna angielski na tyle dobrze, że mu nie zrobi różnicy. A po angielsku łatwiej dotrzeć do większej grupy odbiorców.