Strona główna - poczytaj o JavaScript, jQuery, CSS i HTML5

Front-Trends 2010

Zagadki rozwiązane – czyli o scope, zaokrąglaniu liczb i tak dalej…

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";
Do globalnych zmiennych możesz również odwoływać się poprzez window['nazwa']. Np.:

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:

Proces ten opisał szerzej Rafał Kukawski w komentarzach.
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);
Funkcja toFixed pomaga zaokrąglić liczbę zmiennoprzecinkową do tylu cyfr, ile wynosi wartość argumentu, z jakim wywołaliśmy toFixed.

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

1

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:

alert('a' in this);
alert('b' in this);
alert('c' in this);
var a = 1;
switch(a){
	case 1:
     		var b = 2;
	break;
	case 2:
     		var c = 3;
  	break;
}

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).

2

@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

pio
3

Dzieki Rafal za szersze wyjasnienie, jestes w przypisach ;-). Pozdrawiam

Dodaj komentarz

Dozwolone tagi: <blockquote>, <code>, <strong>