Zwracanie obiektu - wyjaśnienie różnic

0

Pierwszy raz spotykam się z czymś tak dziwnym.. Może najpierw wrzucę minimalny kod:

/*function a1()
{
  return
    {
      'asd': 2
    };
}*/

function a2()
{
  a =
    {
      'asd': 2
    };
  return a;
}

//alert(JSON.stringify(a1()));
alert(JSON.stringify(a2()));

I teraz pytanie, dlaczego po odkomentowaniu funkcji a1 pojawia się błąd:

invalid label
'asd': 2

zadzwiwiająca funkcja a2 działa...

dla mnie jest to jedne wielkie WTF, ale może jest jakieś logiczne wyjaśnienie...

1

Bardzo proste. Po prostu ta funkcja najpierw wywołuje return; , a następnie masz blok kodu, w którym robisz niedozwoloną operację (nie ma takiego operatora jak ":"). Działa jak przerzucisz { do linijki wyżej. Nie pokazujesz interpreterowi, że stwierdzenie w tej linii będzie kontynuowane. W takich przypadkach należy się zastanowić jak parser ma działać, pod tym względem JS jest zdecydowanie prostszy niż taki C++.

0

czyli wymuszają na mnie zapis którego nie cierpie :P

1

I właśnie z tego powodu w JavaScripcie istnieje "jedyny słuszny" styl kodowania jeśli chodzi o klamry :). Nie powinno się ich stawiać od nowej linii!

A czemu tak się dzieje? Zjarek do końca tego nie wyjaśnił, bo wcale nie masz w kodzie "return;" -- nie masz tam średnika.

Dla ciekawskich -- szczegółówe wyjaśnienie...

Gramatyka JavaScriptu mówi jasno, że gdy chcesz coś zwrócić za pomocą instrukcji return coś, to pomiędzy return a coś NIE WOLNO wstawiać znaku nowej linii. Zerknij do specyfikacji:
http://es5.github.com/#x12.9

Masz tam tylko dwie produkcje z return:

  1. return ; -- czyli zwrócenie niczego (czyli zwrócenie undefined).
  2. return [no LineTerminator here] Expression ; -- zwrócenie wartości wyrażenia. Za wyrażenie może robić liczba (5), ciąg znaków ('abc'), literał obiektowy { ads: 2} itp. itd.).

Ty chciałeś wybrać opcję #2. Ale tam wyraźnie stoi napisane: no line terminator here (znak nowej linii nie jest w tym miejscu dozwolony). Czyli: jeśli chcemy coś zwrócić, nie wolno wstawiać po return znaku nowej linii!

I teraz co robi parser JS?

Próbuje korzystać z opcji #1, w której jest tylko samo return i średnik. Ale... w Twoim kodzie nie ma średnika. Jest znak nowej linii, a po nim klamra otwierająca {. Nie istnieje taka reguła w JavaScripcie, więc mamy błąd składni (klamra otwierająca nie jest dozwolona w tym miejscu).

JavaScript uruchamia więc procedurę automatycznego wstawiania średnika: http://es5.github.com/#x7.9

Jeśli chcesz poczytać specyfikację, to podpowiem tylko, że za "offending token" robi klamra otwierająca. Jeśli nie, to powiem w skrócie, co robi JavaScript: widząc, że po return jest fragment kodu, który wg gramatyki języka nie powinien tam być, i że zaczyna się on od znaku nowej linii, wstawia zaraz za return średnik. I sprawdza, czy teraz sytuacja się nie poprawiła.

Ta reguła jest troszkę zamotana. Można ją wyjaśnić też tak, że jeśli gdzieś potrzebny jest średnik, a zamiast niego mamy znak nowej linii, to ten średnik JavaScript sobie automatycznie wstawia.

Okazuje się, że wstawienie średnika za return daje nam return;, czyli poprawną instrukcję języka, zwracającą undefined (pierwszą z dwóch reguł przytoczonych wyżej).

OK, linię z return mamy odchaczoną. Parsujemy dalszą część kodu:

{
  'asd': 2
};

Ten zaczynający się od klamry otwierającej ({) kod dla większości z nas wygląda na literał obiektowy reprezentujący obiekt, który ma własność asd o wartości 2.

I w normalnych przypadkach dla JavaScriptu to też jest literał obiektowy! W uproszczeniu: klamra otwierająca, która NIE jest pierwszym nie-białym znakiem w instrukcji, oznacza początek literału obiektowego. W poniższych przypadkach mamy więc do czynienia z literałami obiektowymi:

return { // literał obiektowy, bo przed '{' mamy 'return '
  asd: 3;
};

callback({ option: true }); // literał obiektowy, bo przed '{' mamy 'callback('

for (var prop in { a: 1, b: 2}) {  // literał obiektowy, bo przed '{' mamy 'for (var prop in '
}

if ({} === {}) { // literał obiektowy, bo przed '{' mamy 'if ('
}

To pokrywa wszystkie normalne sytuacje. Zawsze, gdy literał ma się nam do czegoś przydać, przed nim musi znajdować się coś znaczącego, np. instrukcja if, return (ale bez znaku nowej linii!) czy wywołanie funkcji.

W naszym przypadku, ponieważ wcześniej za return został automatycznie wstawiony średnik, mamy nową instrukcję, zaczynającą się od razu od klamry otwierającej. Wtedy jest inaczej: klamra otwierająca to nie początek literału obiektowego, tylko początek bloku kodu!

Bloków kodu to po prostu ciąg instrukcji zawartych pomiędzy znakami { oraz }:

{
  instrukcja();
  a = b + c;
  inna_instrukcja();
}

Blok kodu można wstawić ot tak, wewnątrz innego bloku kodu, ale zwykle wstawiamy bloki np. po instrukcji if, gdy chcemy warunkowo wykonać kilka instrukcji:

if (n === 0) {
  instrukcja();
  a = b + c;
  inna_instrukcja();
}

Równie dobrze możemy napisać jednak tak:

przed_blokiem_kodu();
n = 2;
{
  var i = 5;
  pierwsza_instrukcja_w_bloku();
  druga_instrukcja_w_bloku();
}
n++;

W JavaScripcie jednak nic nam to nie daje, bo (inaczej niż w C++) nie ma tu tzw. blokowego zakresu ważności zmiennych i zmienna i jest zdefiniowana również przed blokiem kodu (i za nim też).

Niemniej jednak, możemy tak zrobić. Nawisy klamrowe z naszej kłopotliwej instrukcji return stanowią własnie blok kodu.

Przytoczmy ten fragment jeszcze raz:

{
  'asd': 2
};

No dobra. Ale co to za instrukcja 'asd': 2 ? Czyli cośtam, zaraz za tym dwókropek, i dwójka?

No cóż...

JavaScript, jak wiele innych języków programowania, posiada etykiety (ang. label). Baardzo rzadko się z nich korzysta.

W innych językach, najczęściej używano etykiet razem z instrukcją skoku -- goto:

// niepoprawny JS
var i = 0;
etykieta: 
  write(i);
  i++;
  if (i < 10) goto etykieta;

Definiowało się tam etykietę (tutaj nazwaną po prosty etykieta) i w razie czego, można było tam skoczyć za pomocą goto. JavaScript nie ma jednak goto. Etykiet można w JS-ie użyć przy instrukcjach continue i break, ale w to się nie wgłębiajmy.

Etykietę można sobie nazwać prawie dowolnie, ale musi być to prawidłowy identyfikator JavaScript -- czyli nazwa nie rozpoczynająca się od cyfry i tak dalej, i tak dalej. Z grubsza, nazwa etykiety musi spełniać te same warunki co nazwa zmiennej.

Identyfikator etykieta to poprawna nazwa etykiety. Identyfikator asd też jest poprawny!

Mógłbyś napisać więc:

{
  asd: 2
};

i byłoby dobrze!

Czyli w sumie, gdyby Twój kod wyglądał tak:

return
{
  asd: 2;
};

nie byłoby błędu składni! Kod oznaczałby jednak coś innego, niż myślałeś. Za return wstawiony byłby średnik, więc instrukcja return zwracałaby undefined. Za nią znajdowąłby się blok instrukcji. Blok zawierałby etykietę asd oraz instrukcję wyrażenia, która byłaby po prostu dwójką (2). Taką bezużyteczną dwójką. W JavaScripcie, i w wielu innych językach, możesz sobie napisać po prostu:

2 + 2;

i spowoduje to obliczenie wyniku dwa plus dwa, ale ten wynik nie zostanie nigdzie zapisany, ani nigdzie wypisany. Bezużyteczne, ale przydaje się w nieco innych sytuacjach. Równie dobrze jak 2 + 2 możesz sobie napisać samo 2.

I taka dwójka stałaby w Twoim kodzie obok etykiety asd, umieszczonej w bloku instrukcji zaraz za return ;).

Ale Ty nie masz etykiety asd. Masz 'asd', z apostofami. Apostrofy nie mogą występować w nazwie etykiety, podobnie jak nie mogą być częścią nazwy zmiennej.

JavaScript wykrywa więc nieprawidłową etykietę! I stąd błąd "invalid label" :)

0

dzięki za szczegółowe wyjaśnienie :) w takim razie cieszę się, że używam koncepcji: { 'asd': ...}, a nie {asd: ...}, bo błędu szukałbym pewnie dużo dłużej :)

Nie orientujesz się może kiedy wejdzie nowa wersja js? :)

1

@krwq:
Mając już tę wiedzę o wstawianiu średników, nie używałbym na Twoim miejscu apostrofów przy każdej nazwie własności obiektu. Wygodniej jest bez tego. Natomiast apostrofu czy cudzysłowu możesz użyć, gdybyś chciał jako nazwę własności wybrać słowo zastrzeżone (np. 'for', czy 'return' ;)), czego bez apostrofów nie zrobisz.

Ale fakt faktem -- użycie apostrofu tutaj spowodowało wczesny błąd i pomogło Ci zobaczyć, że coś jest nie tak! Nie kojarzę jednak innych sytuacji, w których to by Ci pomogło, więc z tą wiedzą możesz przestawi się na wygodniejszy sposób.

Specyfikacja nowej wersji JS nie jest jeszcze ukończona. Zawiera duuuuużo bajerów, które twórcy języka i programiści wymyślali przez ostatnie lata. Obecnie dogadują się, co ma się znaleźć w języku (i w jakiej postaci), a co nie. W zeszłym tygodniu np. ustalono, że będzie można definiować funkcje za pomocą strzałek, np. tak:

var podniesDoKwadratu = (x) -> x * x;

To definiuje funkcję podniesDoKwadratu() o oczywistym działaniu (nawet nie trzeba tam podawać return -- zwracana jest wartość ostatniego wyrażenia!).

Zanim ukończą specyfikację nowej wersji i zanim wejdzie ona do przeglądarek, w szczególności IE :P, pewnie minie trochę lat...

Już jakiś czas temu jednak całkowicie ukończono "nową" wersję JavaScriptu, formalnie zwaną ECMAScriptem 5 (powiedzmym że obecnie ECMAScript, to to samo co JavaScript). Właśnie do specyfikacji ECMAScriptu 5 linkowałem w poprzednim poście! ( http://es5.github.com/ )

Ta wersja hula już sobie w różnych przeglądarkach, choć takie IE8 nie obsługuje jej w pełni.

Jest wiele przydatnych funkcji, np. tablice mają metody forEach(), map(), filter(), indexOf() (wreszcie!)... Jest też wbudowana funkcja Array.isArray(), którą wcześniej trzeba było sobie napisać samemu. Object.keys(o) zwraca tablicę własności obiektu o, funkcje mają przydatną funkcję bind() (którą tez trzeba było pisać samemu). Stringi dostały wreszcie trim(). Superprzydatne jest Object.create(proto), który tworzy nowy, pusty obiekt z prototypem proto -- do tej pory trzeba było kombinować z operatorem new i udawać, że pisze się w Javie ;). Object.create() jest dużo potężniejsze i pozwala np. na sprawienie, by jakaś własność obiektu była stała (=nie da się jej zmienić) lub na zdefiniowanie własnych akcesorów. Czyli możemy sprawić, że przypisanie obiekt.foo = 5 nie tyle ustawi wartość pola foo, tylko wywoła jakąś funkcję, którą zdefiniowaliśmy!

Poniżej definiujemy gadatliwca, który komentuje głośno za każdym razem, gdy ustawiamy mu wlasnosc:

var gadatliwiec = Object.create(null, {
  wlasnosc: {
    get: function() {
      return this._wlasnosc;
    },
    set: function(newValue) {
      alert("Ustawiam na " + newValue);
      this._wlasnosc = newValue;
    }
  }
});
gadatliwiec.wlasnosc = 5;
alert("Odczytano: " + gadatliwiec.wlasnosc);

To już działa w Chromie, Safari czy Firefoxie!

Podobnie przydatne może to być przy odczycie (tym razem, deskryptor własności dlugosc dodam już po zwykłej inicjalizacji obiektu)...

var odcinekNaProstej = {
  poczatek: 0,
  koniec: 10
};

Object.defineProperty(odcinekNaProstej, "dlugosc", {
  get: function() {
    return Math.abs(this.koniec - this.poczatek);
  }
});
alert(odcinekNaProstej.dlugosc); // 10 -- poprawnie!

// a teraz zmienmy poczatek na 2
odcinekNaProstej.poczatek = 2;

alert(odcinekNaProstej.dlugosc); // 8, czyli się 'zaktualizowalo' -- super!

W IE8 to niestety nadal nie działa :(. Ale np. w NodeJS, gdzie jest silnik V8 (ten co w Chrome), można tego używać rutynowo. Dobra wiadomość: tabele kompatybilności mówią, że działa toto w IE9. Sam mam na kompie jeszcze ósemkę, więc jeszcze nie sprawdziłem.

2

Nie myślałeś czasem o pisaniu jakiś e-booków o javascripcie albo jakiegoś bloga z takimi opisami? Znasz tyle bajerów, że spokojnie mógłbyś.

0

Myślałem, ale na razie nie mam czasu. Zbieram materiały. Tu, na forum, piszę jednak dość nieregularnie i nawet nie czytam swoich postów -- opracowanie ich, przeredagowanie etc. zajęłoby mi drugie, trzecie i czwarte tyle czasu.

Nie lubię fałszywej skromności, więc powiem, że wiem, że mam do tego wystarczający poziom. Poziom bloggerów jest zresztą bardzo różny i najczęściej ograniczają się do powtarzania tego, co ktoś inny wyczaił na listach dyskusyjnych lub napisał na zagranicznym blogu. Do takiego "tłumaczenia" artykułów nic nie mam i chwalę za wykonaną pracę, o ile autor się przyznaje do swojego trybu pracy, ale sam mam inne ambicje. Wśród bloggerów frontendowych jest paru dobrych kolesi, drobna część z nich jest też naprawdę dobra w JavaScripcie ;), ale supermena jeszcze nie spotkałem. Jest natomiast trochę gości, którzy blogów nie piszą, a poziomem od tych, co piszą (nawet topowych!) zupełnie nie odstają! (czasem są od nich nieco gorsi, czasem lepsi)

Co do "bajerów", to jakiś czas przestałem się ich uczyć i ich znajomość zaczęła wynikać z coraz lepszego zrozumienia specyfikacji. Niestety dla sceny JS-owej, aktualnie, jeśli czytujesz specyfikację ECMA-262 i rozumiesz jakąś tam znaczącą jej część, to już jesteś w top iluś tam najlepszych kmiotków w Polsce. Za "kmiotka" uważam kolesia piszącego w JS. Nam, "kmiotkom", daleko do poziomu ludzi piszących interpretery JS czy tworzących gramatyki formalne (nie wiem, ilu takich ludzi jest w Polsce). Do nauki mamy jeszcze, w każdym razie, całkiem sporo ;). Popieram stwierdzenie, że im więcej się wie, tym wie się o istnieniu coraz większej gamy rzeczy, których się jeszcze nie wie :P.

0
bswierczynski napisał(a)

Myślałem, ale na razie nie mam czasu.

Poprosić kogoś, kto ma dostęp do bazy 4p, żeby zrobił zrzut wszystkich Twoich postów o długości powiedzmy 3k+ znaków, potem co parę dni publikować jeden jako nowy wpis ;D

1 użytkowników online, w tym zalogowanych: 0, gości: 1