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>

See the Pen [onclick] + with #1 by Comandeer (@Comandeer) on CodePen.

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>

See the Pen [onclick] + with #2 by Comandeer (@Comandeer) on CodePen.

Po kliknięciu przycisku, w konsoli pojawi się true. Czyli nasza funkcja clear() została nadpisana przez metodę documentu 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>

See the Pen [onclick] + with #3 by Comandeer (@Comandeer) on CodePen.

Zatem jeśli jeszcze nie używasz addEventListener(), to mam dla Ciebie kolejny argument, by w końcu to zacząć robić!