Cienie przeszłości
Niektóre rzeczy w webdevie wydają się nie być przesadnie ekscytujące. No bo czymże może nas zaskoczyć atrybut [onclick]
? Cóż, okazuje się, że wieloma rzeczami.
Problem
Wyobraźmy sobie prosty kod HTML + JS:
<div id="notifications"> <!-- 1 -->
<p>Jakieś powiadomienie</p>
</div>
<p>
<button onclick="clear()">Usuń powiadomienia</button> <!-- 2 -->
</p>
<script>
function clear() { // 3
document.querySelector( '#notifications' ).innerHTML = '';
}
</script>
Mamy kontener z powiadomieniami (1), a pod spodem knefel, który ma je usuwać (2). Podczepiona jest do niego przy pomocy atrybutu [onclick]
funkcja clear()
(3), która ustawia własność innerHTML
kontenera na pusty ciąg znaków. Ot, całkiem prosty kawałek kodu.
Tyle że nie działa. I, co gorsza, w konsoli nie ma absolutnie żadnego błędu. Co tu się zatem stanęło?
Trochę historii
Zanim przeskoczę do mrocznych tajemnic [onclick]
a, wypadałoby przypomnieć pewną antyczną JS-ową instrukcję – with()
. Służyła ona do tworzenia bloków kodu, które działały niejako “w kontekście” jakiegoś obiektu. Najprościej wyjaśnić to na przykładzie:
const obj = { // 1
a: 'test' // 2
};
with ( obj ) { // 3
console.log( a ); // 'test
console.log( obj.a === a ); // true
}
Tworzymy sobie obiekt obj
(1), który ma jedną własność – a
(2). Następnie tworzymy blok with()
dla tego obiektu (3). Dzięki temu w środku tego bloku możemy odwoływać się do własności obj
bez podawania nazwy obiektu. Innymi słowy, zamiast obj.a
możemy napisać po prostu samo a
.
Osobiście nie widzę w tym nic szczególnie przydatnego, a dodatkowo potrafi to wprowadzić niemały chaos w kodzie i trudne do wykrycia błędy:
const obj = { // 1
a: 'lol'
};
const a = 'SUPER IMPORTANT VARIABLE'; // 2
with ( obj ) { // 3
console.log( a ); // 'lol
console.log( obj.a === a ); // true
}
W tym przykładzie znowu mamy obj
z jedną własnością a
(1), ale mamy także zmienną a
(2), która – na potrzeby budowania dramatyzmu – zawiera niezwykle ważną informację. Tworzymy sobie blok with()
dla obiektu obj
(3) i… tracimy dostęp do zmiennej a
. Własności obj
skutecznie przesłaniają inne istniejące zmienne o nazwach takich samych jak własności obiektu obj
. A że wszystko jest zgodnie z zasadami języka (ale niekoniecznie logiki), to przeglądarka/inne środowisko uruchomieniowe JS-a nie wyświetli nam żadnego przydatnego błędu.
Na całe szczęście tryb ścisły nie pozwala używać with()
, dzięki czemu – w modularnym świecie – instrukcja ta jest spotykana naprawdę rzadko.
Tryb ścisły w świecie ESM ma spore znaczenie, ponieważ kod JS wczytany jako moduł jest uruchamiany w trybie ścisłym.
Mroczny sekret [onclick]
Wróćmy zatem po tej historycznej dygresji do naszego [onclick]
a – co dokładnie się dzieje w chwili naciśnięcia przycisku z problematycznego przykładu?
Otóż okazuje się, że istnieje metoda document.clear()
, która… nic nie robi, ale też nie rzuca błędu. Ot, pozostałość po antycznych czasach, która nie została usunięta, by nie psuć Sieci. Jeśli nieco zmodyfikujemy kod, przekonamy się, że faktycznie, ta metoda jest odpalana po kliknięciu przycisku:
<div id="notifications">
<p>Jakieś powiadomienie</p>
</div>
<p>
<button onclick="console.log( document.clear === clear );clear()">Usuń powiadomienia</button>
</p>
<script>
function clear() {
document.querySelector( '#notifications' ).innerHTML = '';
}
</script>
Po kliknięciu przycisku, w konsoli pojawi się true
. Czyli nasza funkcja clear()
została nadpisana przez metodę document
u o tej samej nazwie. Innymi słowy, kod wewnątrz [onclick]
a jest mniej więcej równoznaczny z poniższym:
with ( document ) {
clear();
}
A co na to specka?
Nie byłbym sobą, gdybym nie poszperał w specyfikacji, żeby znaleźć wyjaśnienie dla tego zachowania. Atrybuty HTML dla event listenerów są opisane w specyfikacji HTML. Jest tam zamieszczony sposób parsowania ich zawartości do faktycznego kodu JS. Bez zbytniego zagłębiania się w szczegóły techniczne, jednym z etapów jest ustalenie scope (zakresu). W tym celu wołana jest “funkcja” NewObjectEnvironment()
ze specyfikacji ECMAScript. Piszę to w cudzysłowie, ponieważ nie jest to faktycznie istniejąca funkcja, a po prostu opis pewnej abstrakcyjnej operacji, którą musi wykonać silnik JS-a. Operacja NewObjectEnvironment
zwraca tzw. Object Environment Record (Rejestr Środowiska Obiektu). Dokładnie taki sam, jaki tworzy with()
.
Żeby było ciekawiej, jak dokładnie spojrzy się w specyfikację HTML, to zobaczyć można, że tworzone są tam trzy rejestry: dla document
, dla samego elementu, na którym zachodzi zdarzenie, oraz dla formularza, jeśli takowy jest przodkiem elementu z atrybutem [on…]
. Całość oczywiście jest dodatkowo uruchamiana w globalnym scope, co sprawia, że można tworzyć naprawdę przerażające potworki:
<form action="./">
<button type="button" onclick="reset();clear();console.log( innerHTML, innerWidth );">Ooops…</button>
</form>
reset()
pochodzi z formularza (HTMLFormElement#reset()
),clear()
to już wspominane wcześniejdocument.clear()
,innerHTML
to własność samego przycisku,innerWidth
to z kolei własność globalnego obiektuwindow
.
Zatem jeśli jeszcze nie używasz addEventListener()
, to mam dla Ciebie kolejny argument, by w końcu to zacząć robić!
Komentarze
Przejdź do komentarzy bezpośrednio na Githubie.
Dawne komentarze
Ten blog wcześniej korzystał z systemu komentarzy Disqus. Jednakże pożegnaliśmy się i postanowiłem, że zaimportuję do nowej wersji stare komentarze z niego. Cóż, jego system eksportu na wiele nie pozwala…
Bardzo dobry artykuł
Bardzo ciekawy artykuł. Jeszcze jedna cegiełka do arsenału, który może pomóc w debugowaniu kodu początkujących programistów.