Pora przedstawić rozwiązania opublikowanych kilka dni temu Javasciptowych zagadek. Może być ciekawie, tym bardziej, że nikt nie dostrzegł (przynajmniej w komentarzach) jednej z najciekawszych właściwości języka, choć nie do końca pozytywnej.
Scope!
Pierwsza zagadka:
alert("i" in window); // true
var i = "foobar";
var bar = ‘foo’;
alert(window['bar']);
opiera się na założeniu, że obiekt window jest globalny, będąc “najwyżej” w hierarchii obiektów JSowych, dla których tworzy swoistą przestrzeń nazw. Każda zdefiniowana zmienna w globalnym scope (czyli poza ciałem jakichkolwiek funkcji czy obiektów) należy domyślnie do obiektu window!
Jeśli mowa o globalnym scope, to zauważmy, że naturalnie this będzie w takim przypadku odnosić się do window. Proces przypisania dzieje się “w locie”, podczas uruchamiania strony. To mniej więcej tak, jakbyśmy na początku skryptu napisali:
var this = new window();
Tak więc analogicznie zadziała coś podobnego:
alert("i" in this); // true
var i = "foobar";
Pamiętajmy też, że kontekst zmienia się w zależności od zainicjowanego obiektu. To logiczne. O ile więc coś takiego będzie zachowywać się bez zmian:
var i = "foobar";
function foobar() {
alert("i" in this); // true
}
foobar();
tak:
var i = "foobar";
function foobar() {
alert("i" in this); // false
}
var obj = new foobar();
zwróci false, ponieważ this będzie odnosić się do utworzonego obiektu funkcji foobar w piątej linijce.
Mimo że do tej pory wszystko brzmiało dość logicznie, należy jednak zauważyć kilka odstępstw od powyższych reguł. Na przykład:
alert("foo" in window); // true
alert("bar" in window); // false(!)
var foo = "foo";
bar = "bar";
Składowe obiektu window (przestrzeni nazw) są, co ważne, preinicjowane na podstawie słowa kluczowego var. Warto również dodać, że nie mamy dostępu do wartości globalnych zmiennych przed ich zdeklarowaniem w kodzie:
alert('foo' in window); // true
alert(window['foo']); // undefined
var foo = "foo";
alert(window['foo']); // foo
Żeby dostrzec lepiej różnicę w inicjowaniu obiektu window spójrzmy na trzy przykłady:
alert("foobar" in window); // true
alert(foobar); // undefined
window.onload = function() {
alert(foobar); // foobar
};
var foobar = "foobar";
window.alert('this is FOOOOBAR!'); // this is FOOOOBAR!'
alert('just another poor foobar!'); // just another poor foobar!
function foobar() {
alert('foobar');
}
window.foobar(); // foobar
Ciekawe, prawda?
Druga zagadka także opiera się na motywie scope, czyli zakresie zmiennych. Przypomnijmy:
alert("i" in window); // false
(function() {
i = "foobar";
})();
Podana konstrukcja zwraca false. Dlaczego? Otóż sytuację powinna rozjaśnić inna, która znaczy dokładnie to samo:
alert("i" in window); // false
function foobar() {
i = "foobar";
};
foobar();
Silnik JavaScript, inicjując window, poszukując tym samym zmiennych globalnych nie natrafia na żadną z nich, ponieważ wszystkie są otoczone klamrami funkcji.
Przejdźmy do trzeciego punktu:
var i;
alert(i);
Sprawa jest prosta — jeśli zmienna nie ma wartości, domyślnie ustawiane jest undefined. Nawiązując do pierwszego zadania, sprawdźmy dla utrwalenia zachowanie poniższego kodu:
alert("i" in window); // true
var i;
Zaokrągalnie liczb w JavaScript
Czwarte zadanko to jest to, co tygryski lubią najbardziej. To o nim pisałem we wstępie:
alert(0.3 === 0.1 + 0.2); // false(!)
Zwraca ono false. Dlaczego? Wszystkiemu winne jest pojedyncze typowanie — nieważne, czy liczba jest zmiennoprzecinkowa, czy całkowita — obie w JavaScript będą tego samego typu — Number!
alert(typeof 0.2); // number
alert(typeof 100); // number
Dalej… zaokrąglanie w JS działa na podstawie IEEE 754, co nie jest najlepsze, jeśli chodzi o cyfry po przecinku:
Liczbę pojedynczej precyzji w formacie “IEEE—754″ zapisujemy za pomocą trzydziestu dwóch bitów. Pierwszym bitem jest bit znaku S (sign). Jeśli liczba zapisana w kodzie dziesiętnym jest ujemna, oznacza to, iż S przyjmie wartość 1. Jeśli liczba dziesiętna jest dodatnia – zero. Dalej następuje 8 bitów kodujących wykładnik potęgi 2 (cecha), oraz 23 bity rozwinięcia binarnego (mantysa), przy czym pomija się wiodący, niezerowy bit. Daje to około 7–8 dziesiętnych miejsc znaczących i zakres od około ±1.18•10—38 do około ±3.4•1038. Zakres taki może wydawać się wystarczający w prostych obliczeniach, lecz jego użycie nastręcza trudności, gdy istnieje potrzeba stosowania niektórych stałych fizycznych (jak np. stała Plancka), często też może prowadzić do występowania błędów przepełnienia podczas obliczeń pośrednich, jeśli ich wynik wykroczy poza reprezentowany zakres.
Rozwiązaniem problemu może być na przykład:
alert(Number((0.1 + 0.2).toFixed(1)) === 0.3);
Warto spojrzeć na inne implementacje rozwiązania problemu. Zdaje się, że właśnie słyszę śmiech dżawowców, pomieszany z szeptanym BigDecimal, mistrzu!…
Piątka to natomiast typowa podpucha — zwraca true.
This is JavaScript!
Co do szóstego zadania:
function foobar(i) {
return function(i) {
alert(i);
};
};
foobar(false)(true); // true
tak naprawdę najpierw do funkcji foobar leci zmienna o wartości false. Z takim oto argumentem funkcja foobar zwraca inną, anonimową funkcję, która pobiera sobie drugi argument — true.
Siódemka to gra na przeczekanie:
function foobar(i) {
return (function(i) {
return i;
})();
};
alert(foobar(false)); // false
Zwracany jest false. Dużej filozofii tu nie ma.
Argumenty i referencje
Ósemka:
function foobar(foo, bar) {
arguments[1] = 'foo';
alert(bar);
}
foobar('foo', 'bar');
Warto wiedzieć, że w ciele funkcji pod zmienną arguments mamy tablicę prezentującą argumenty wywołania funkcji. Pod arguments[0] jest pierwszy argument (zmienna foo) i tak dalej. Okazuje się, że tablicowa zmienna arguments[1] działa na zasadzie referencji. Zmieniając jej wartość, zmieniamy też wartość zmiennej bar.
Call on me
Zadanie dziewiąte:
function foobar() {
alert(this);
}
foobar.call(0 === false); // false
Otóż każda funkcja w swoich składowych zawiera m. in. wbudowaną metodę call. Pozwala ona uruchomić funkcję, na której została wywołana. Pierwszy argument, jaki przyjmuje, to kontekst, do jakiego odnosić ma się zmienna this w ciele wywoływanej funkcji. W tym wypadku w foobar pod zmienną this będzie obiekt Boolean o wartości false. Wszystko dlatego, że prawdą jest 0 == false, jednak nieprawdą już, że 0 === false. Sprawdźmy, czy to prawda:
function foobar() {
alert(this.constructor); Boolean [...]
}
foobar.call(0 === false);
Obiektowe prototypy
Przedostatnie zadanie wyglądało następująco:
function foobar() {};
var obj = new foobar();
foobar.prototype.test = function() { alert('test'); };
obj.test(); // test
Jak wiemy, dzięki prototype możemy rozszerzać pseudoklasy oraz utworzone obiekty o nowe składowe.
Prawdę tę prezentuje również ostatnie zadanie:
function foobar() {
this.test = function() { alert('test1'); }
};
var obj = new foobar();
foobar.prototype.test = function() { alert('test2'); };
obj.test(); // test1
Z tym, że — jak widzimy — prototype nie potrafi nadpisać istniejących już składowych w utworzonych obiektach.
I jak? Zdane?


Komentarze
Witam,
widzę, że przedstawiłeś większość (jak nie wszystkie) ciekawostki związane z JavaScriptem. Przynajmniej nie przyszły mi do głowy inne, warte uwagi cechy tego języka.
Chciałem dorzucić swoje trzy grosze do niektórych zagadek, co powinno bardziej rozjaśnić sprawę tym bardziej początkującym JavaScriptowcom.
Zagadka 1:
Tak jak napisał Damian, wszystkie zmienne globalne stają się własnościami obiektu globalnego. W przypadku stron WWW jest to obiekt window.
Ale nadal nie tłumaczy to faktu, dlaczego wykonanie alert przed zadeklarowaniem zmiennej i tak daje true, że zmienna istnieje, choć jej deklaracja znajduje się później w kodzie. W JS działa to na takiej zasadzie, że zmienne są tworzone na samym początku aktualnego scope’u i każdej z nich przypisana jest początkowo wartość undefined. Wartość zmiennej zostanie przypisana dopiero w momencie wykonania deklaracji zmiennej (variable statement).
Rozważmy taki kod:
Wszystkie 3 zmienne istnieją, mimo iż case 2 nigdy nie zostanie wykonany i faktyczna deklaracja zmiennej ‘c’ nigdy nie nastąpi.
Powyższe tłumaczy też ‘false’ z drugiej zagadki, ponieważ mamy do czynienia z innym scrope’m. Druga zagadka prezentuje też pewną źle odbieraną technikę programowania. W JS powinno się pamiętać, żeby zawsze deklarować zmienne z użyciem var. Jeśli planujemy z innego scope’u przypisać jakąś wartość dla zmiennej globalnej, powinniśmy w globalnej przestrzeni nazw wcześniej zadeklarować tę zmienną (jako zmienną typu undefined). Uchroni to programistę przed próbą odczytu nieistniejącej zmiennej, co skutkować będzie błędem.
Zagadka 4:
Dodam tylko, że nie jest to wyłącznie problem języka JavaScript, ale wszystkich innych, które opierają swoje działanie o IEEE 754. Rozważano też wprowadzenie do Ecma-Scriptu drugiego typu numerycznego – decimal. W sieci było sporo dyskusji na ten temat, szczególnie dotyczyły faktów, że wprowadzenie drugiego typu może wprowadzić sporo zamieszania, oraz że obliczenia na typie dziesiętnym są dosyć powolne. Specyfikacja Ecma-Script 4 definiuje aż 5 typów reprezentacji liczb + stary typ Number, dla wstecznej kompatybilności. Ale przez całą wojnę związaną z rozwojem ES, cała praca nad językiem zaczyna się od nowa (ES 6-th edition).
@Rafał Kukawski: “W JS działa to na takiej zasadzie, że zmienne są tworzone na samym początku aktualnego scope’u (…)”
i tego w mordę nie wiedziałem, hehe.
pozdrawiam
Dzieki Rafal za szersze wyjasnienie, jestes w przypisach ;-). Pozdrawiam