niedziela, 18 grudnia 2011

Quine w JavaScript

Quine - czyli program wypisujący własny kod źródłowy.
!function f(){
  console.log(['!', f, '();'].join(''));
}();

niedziela, 21 sierpnia 2011

Własny EventListener

Kolejny raz w mojej pracy zaszła potrzeba zaimplementowania EventListener`a. Kiedy widzę, że muszę napisać drugi raz ten sam kod zawsze refaktoryzuję ten istniejący tak aby wspólną jego część przenieść do jednego niezależnego miejsca w kodzie.
Ewentualne zmiany kodu w więcej niż jednym miejscu nie wróżą niczego dobrego. Pozwólmy aby nasza dalsza praca nad kodem była miła i przyjemna. JavaScript doskonale nam w tym pomoże.

var EventListener = function(enames){
  var events = {};
  for( var i = 0; i < enames.length; ++i ){
    events[enames[i]] = [];
  }
  return {
    add: function(ename, fn){
      var that = this;
      var i = events[ename].push(fn) - 1;
      return {
        detach: function(){
          delete events[ename][i];
          return {
            attach: function(){
              return that.add(ename, fn);
            }
          };
        }
      };
    },
    remove: function(ename, fn){
      var that = this;
      delete events[ename][
        events[ename].indexOf(fn)
      ];
      return {
        attach: function(){
          return that.add(ename, fn);
        }
      };
    },
    invoke: function(ename){
      var arg = [].slice.call(arguments, 1);
      var res = [];
      for( var i in events[ename] ){
        if( events[ename].hasOwnProperty(i) ){
          res.push(events[ename][i].apply(this, arg));
        }
      }
      return res;
    },
    toString: function(){
      return '[EventListener: '+ enames.join(', ') +']';
    }
  };
};

Klasa EventListener pozwala nam na utworzenie własnego nasłuchiwacza zdarzeń. Dzięki niej w wystarczy, że w jednym miejscu aplikacji stworzymy kod odpowiedzialny za wykrycie jakiegoś zdarzenia, a w wielu innych dowolnych miejscach tylko podłączamy się do niego. Innych zalet powyższego wzorca projektowego chyba nie muszę tłumaczyć.
Obiekt zwracany przez metodę add posiada metodę detach, która pozwala odpiąć dodane zdarzenie. Ta z kolei zwraca obiekt zawietający metodę attach, która pozwala na powrotne podpięcie zdarzenia itd.
Podobnie jest z metodą remove naszej klasy. Z tą różnicą, że ta w pierwszej kolejności zwraca obiekt z metodą attach.
Metoda invoke oczywiście służy do wywołania funkcji podpiętych pod zadane zdarzenie. Pierwszym argumentem tej metody jest nazwa wywołanego zdarzenia. Kolejne argumenty są dowolne i opcjonalne. Będą one przekazane do każdej z podpiętych do zdarzenia funkcji.

czwartek, 18 sierpnia 2011

Object implicit

Object.prototype.implicit = function(obj){
  for( var i in obj ){
    if( obj.hasOwnProperty(i) && typeof this[i] === 'undefined' ){
      this[i] = obj[i];
    }
  }
  return this;
};

Całkiem przydatna funkcja umożliwiająca łączenie dwóch obiektów. Działa podobnie do już opisanej przeze mnie funkcji update. Różnica polega na tym, że metoda implicit nie nadpisuje już istniejących właściwości obiektu.

Animation, czyli jak sprawnie animować elementy strony

Poprzednio pisałem na temat animacji. Tym razem rozbudowałem tę funkcję o dodatkowe przydatne możiwości.

var Animation = function(fn, params){
  params = params || {};
  params = params.implicit({
    frames: 10,
    fn: function(){},
    t: 30,
    transition: null,
    at: 0
  });
  var frame = Math.round(params.at * params.frames);
  var progress;
  
  var run = (function(){
    return params.transition
      ? function(p){
          progress = p;
          fn.call(res, params.transition(progress), params);
        }
      : function(p){
          progress = p;
          fn.call(res, progress, params);
        };
  })();
  var iid = setInterval(function(){
    run( (++frame)/params.frames );
    if( frame >= params.frames ){
      clearInterval(iid);
      params.fn.call(res, params);
    }
  }, params.t);
  run( frame/params.frames );
  
  var res = {
    stop: function(execEndFn){
      execEndFn = execEndFn || false;
      clearInterval(iid);
      if( execEndFn ){
        params.fn.call(res, params);
      }
      return this.progress();
    },
    progress: function(){
      return progress;
    },
    toString: function(){
      return '[Animation: '+ Math.round(progress * 1e2) +'%]';
    }
  };
  return res;
};

Przede wszystkim funkcja (klasa) nie zwraca już identyfikatora zwracanego przez funkcję setInterval. Zamiast tego zwraca obiekt z dwiema (nie licząc metody toString nadpisującej tę domyślną dla obiektu) metodami.
Pierwsza metoda stop umożliwia zatrzymanie animacji. Wywołanie jej z pierwszym parametrem równym true spowoduje wywołanie również funkcji, wywoływanej w chwili normalnego zakończenia animacji. Przez normalne zakończenie rozumiemy odtworzenie animacji do końca.
Kolejna metoda zwracanego obiektu pozwala pobrać aktualny stan animacji z zakresu <0; 1>.
Dodatkowo do opisanych już wcześniej parametrów dodałem dwa następujące.
Transition - pozwala przekazać funkcję przejścia. Dzięki temu możemy w prosty sposób zrealizować np. łagodne rozpoczęcie i/lub zakończenie animacji.
Kolejny nowy parametr at jest nie mniej ciekawy. Pozwala bowiem on na rozpoczęcie animacji od zadanego etapu. Animacja z niezerową wartością dla tego parametru będzie trwała już krócej czyli nie będzie rozciągnięta na zadaną ilość klatek (frames) a zostanie ucięta, co pozwala na niesamowicie proste zaimplementowanie animacji, która może zostać w dowolnej chwili przerwana i odtworzona wstecz do punktu wyjściowego.

Na koniec dodam, że "klasa" korzysta z funkcji implicit, w celu ustawienia domyślnych parametrów wywołania funkcji.

niedziela, 14 sierpnia 2011

Object update

Object.prototype.update = function(obj){
  for( var i in obj ){
    if( obj.hasOwnProperty(i) ){
      this[i] = obj[i];
    }
  }
  return this;
};

Funkcja update pozwala w prosty sposób rozbudować lub nadpisać dowolne właściwości obiektu.
Ponieważ funkcja operuje na referencji do obiektu, oraz zwraca tę referencję możemy z niej skorzystać na dwa sposoby.

Pierwszy z nich polega na utworzeniu obiektu i nadpisaniu/rozszerzeniu jego metod/wartości.
var obj = {
  a: 1,
  b: 2
};

obj.update({
  b: 3,
  c: 4
});

console.log(obj);

Drugi sposób to wywołanie metody update na naszym obiekcie w chwili jego utworzenia i zwrócenie na jego miejsce nowego obiektu.
var obj = {
  a: 1,
  b: 2
}.update({
  b: 3,
  c: 4
});

console.log(obj);

startInterval czyli natychmiastowy setInterval

var startInterval = function(fn, delay){
  var arg = [].slice.call(arguments, 2),
      that = this;  
  var newFn = function(){
    fn.apply(that, arg);
  };
  newFn();
  return setInterval(newFn, delay);
};

Powyżej zamieszczam funkcję startInterval. Działa dokładnie tak jak setInterval (z wyjątkiem możliwości przekazania pierwszego parametru jako String) z tą różnicą, że pierwsze wywołanie funkcji następuje natychmiastowo.
Funkcja jest wygodną alternatywą dla setInterval. Dzięki niej nie trzeba w kodzie tworzyć dodatkowych nazw dla funkcji, aby wywołać ją, oraz przekazać jako parametr do setInterval.

niedziela, 12 czerwca 2011

Domyślne parametry funkcji

Funkcja i domyślne jej parametry w JavaScript? JavaScript nigdy nie oferował mechanizmu nadawania domyślnych wartości niezdefiniowanym parametrom przy jej wywołaniu.

Można sobie z tą niedogodnością poradzić w całkiem prosty sposób i wiele osób tak to właśnie robi. Poniżej zamieszczam kawałek kodu.

var fn = function(a, b, c){
  a = a || 25;
  b = b || [2, 5];
  c = c || 'domyslny';
  alert(
    'liczba: ' + a + '\n' +
    'tablica: ' + b + '\n' +
    'string: ' + c
  );
};

fn(5, [1,2,3], 'test');
fn(5, [1,2,3]);
fn(5);
fn();

Działa całkiem nieźle. Co jednak jeśli wywołamy funkcję w poniższy sposób?

fn(0);

Funkcja wyświetla 25. Przecież wysłaliśmy 0 (zero). Dzieje się tak, ponieważ poniższa konstrukcja traktuje liczbę 0 jako wartość false, dlatego do zmiennej a przypisywana jest alternatywna wartość 25.

a = a || 25;

Skoro znamy już problemy wynikające z powyższych konstrukcji przejdźmy do rozwiązania problemu w inny, bardziej elegancki sposób.
Poniżej zamieszczam funkcję która pozwala ze zwykłej funkcji stworzyć funkcję przyjmującą domyślne parametry. Kod zawiera również funkcję testową oraz kilka jej wywołań.

Function.prototype.params = function(){
  var that = this,
      arg = [].splice.apply(arguments, [0, arguments.length]);
  return function(){
    return that.apply(this, [].splice.apply(arguments, [0]).concat(
        arg.slice(arguments.length)
      )
    );
  };
};

var test = function(a, b){
  alert(
    a + '\n' +
    b
  );
}.params(11, 22);

test(1, 2);
test(1);
test();

Stworzenie funkcji z domyślnymi parametrami jest teraz dziecinnie proste. Wystarczy zadeklarować dowolną funkcję a zaraz za nią wywołać naszą metodę z argumentami, które mają być domyślne.
Warto pamiętać o tym, że powyższe udogodnienie będzie spowalniać działanie skryptu. Każde wywołanie zwróconej funkcji powoduje wywołanie kilku dodatkowych instrukcji. Nadużywanie tego udogodnienia może okazać się nierozsądne, w szczególności w przypadku prostych, wielokrotnie powtarzanych funkcji.

wtorek, 7 czerwca 2011

JavaScript i nazwy

Po pewnym czasie programowania w tym specyficznym języku daje się zauważyć w napisanym przeze mnie kodzie ciekawą prawidłowość. Otóż nigdy (z niewielkimi wyjątkami) nie nazywam w kodzie czegoś czego używam tylko raz.
Specyfika tego języka umożliwia takie pisanie kodu i bardzo chętnie tak właśnie go piszę.
W JavaScripcie każdy obiekt, tablica, funkcja, liczba itd. może mieć przypisaną nazwę, ale może jej również nie posiadać (funkcja anonimowa, dana natychmiastowa).
Każda dodatkowa zmienna w kodzie to możliwość kolizji nazw i potencjalnych problemów, dlatego uważam, że trzymanie się powyższej zasady pozwala na całkiem przyjemne i bezproblemowe rozbudowywanie kodu.

(function(str){
  alert(str);
})([
  'JavaScript pozwala na',
  {
    x: [
      'budowanie calkiem',
      (function(x){
        return 'zlozonych ' + x[''] + ' ' + x.a;
      })({
        '': 'struktur',
        a: 'danych'
      }),
      'bez deklaracji',
      'jakiejkolwiek'
    ]
  }.x.join(' '),
  'zmiennej w kodzie.'
].join(' '));

Powyżej kawałek kodu, którym starałem się wyrazić to co mam na myśli pisząc tego posta.

poniedziałek, 6 czerwca 2011

Animacja jednostkowa

Animacja jednostkowa to funkcja którą napisałem, po to aby ułatwić sobie tworzenie animacji w swoich aplikacjach.
Nazywam ją tak, ponieważ umożliwia mi proste zbudowanie animacji dowolnej wielkości (właściwości obiektu) w prosty i elegancki sposób poprzez wywołanie mojej funkcji z parametrem - liczbą z zakresu od 0 do 1 włącznie, która określa aktualny stan animacji.
Poniżej zamieszczam kod tytułowej funkcji a zaraz za nim krótki opis sposobu używania.

var animation = function(fn, params){
  params = params || {};
  params = {
    frames: params.frames || 10,
    fn: params.fn || null,
    t: params.t || 30
  };
  var frame = 0;
  fn(0);
  var iid = setInterval(function(){
    fn( (++frame)/params.frames );
    if( frame >= (params.frames) ){
      clearInterval(iid);
      params.fn();
    }
  }, params.t);
  return iid;
};

Jak widać funkcja przyjmuje dwa parametry: wskaźnik do funkcji oraz obiekt opcjonalnych parametrów.
fn - to wskaźnik do głównej funkcji wykonawczej, która będzie decydowała o tym co będziemy animować.
Zmienna params przyjmuje 3 opcjonalne parametry:
- frames - ilość klatek animacji,
- t - interwał pomiędzy kolejnymi klatkami animacji,
- fn - funkcja, która zostanie wywołana zaraz po zakończeniu działania animacji.
Funkcja zwraca wartość Number zwracaną przez wbudowaną funkcję setInterval. Pozwala to na wcześniejsze zakończenie działania animacji.

// prosty kod pokazujacy sposob uzycia funkcji
animation(function(x){
  alert(x);
}, {
  frames: 10,
  fn: function(){
    alert('koniec');
  }
});

Jak widać funkcja jest niczym więcej jak wzbogaceniem funkcji setInterval o dodatkowe funkcjonalności.
Mimo wszystko potrafi bardzo uprościć pisany kod.
Do jej głównych zalet można zaliczyć:
- zwracany bieżący stan animacji mieszczący się zawsze w przedziale domkniętym <0; 1>,
- pierwsze wywołanie funkcji fn(0) jest wykonywane synchronicznie, w przeciwieństwie do zastosowania samej funkcji setInterval, gdzie na pierwsze wywołanie zadanej funkcji skrypt musi odczekać zadany czas. Dzięki temu unikniemy krótkotrwałych stanów nieustalonych przebiegu animacji,
- i wreszcie możliwość dodania wskaźnika do funkcji wywoływanej na zakończenie animacji - prosty sposób na wykrycie końca animacji bez żadnych dodatkowych instrukcji warunkowych.

środa, 13 kwietnia 2011

Inline scripts

Proste skrypty JavaScript, wklejane do paska adresu które mogą ułatwić naszą codzienną pracę.

Liczenie średniej z zestawu danych.
javascript:d={s:0,l:0};while(d.i=parseInt(prompt('podaj liczbe do sredniej (zero konczy ciag danych)',''),10)){d.s+=d.i;++d.l;}alert('srednia z '+d.l+' danych wynosi: '+(d.s/d.l));

Zaznaczenie wszystkich pól checkbox na stronie. Naturalnie odznaczenie można zrealizować przez zamianę true na false.
javascript:p=document.getElementsByTagName('input');for(i=0;i<p.length;++i){if(p[i].type==='checkbox'){p[i].checked=true;}};alert('ok');

sobota, 2 kwietnia 2011

Problem z funkcjami setTimeout oraz setInterval w IE

W tym poście opiszę jak rozwiązać problem z przekazywaniem argumentów w funkcji setTimeout w przeglądarce Internet Explorer, który pojawił się w trakcie mojej pracy nad kodem JavaScript.
Problem polega na tym, że przeglądarka IE nie przekazuje argumentów do funkcji, gdy funkcja jest wywoływana z opóźnieniem (setTimeout) lub w interwałach czasowych (setInterval).

// przydatne funkcje
Array.prototype.clone = function(){
 return this.slice(0);
};
Object.prototype.toArray = function(){
 var arr = [];
 for( var i in this ){
  if( this.hasOwnProperty(i) ){
   arr.push(this[i]);
  }
 }
 return arr;
};
var argToArray = function(arg){
 var arr = [];
 for( var i = 0; i < arg.length; ++i ){
  if( arg.hasOwnProperty(i) ){
   arr.push(arg[i]);
  }
 }
 return arr;
};


// safeTimeout - odpowiednik funkcji setTimeout wykorzystujacy ta funkcje
// oraz dzialajacy prawidlowo rowniez w IE
var safeTimeout = function(fn, t){
 if( typeof(fn) == typeof(function(){}) ){
  var arg = argToArray(arguments).slice(2);
  return setTimeout(function(){
   fn.apply(this, arg);
  }, t);
 }
 return setTimeout(fn, t);
};

// safeInterval - odpowiednik funkcji setInterval
var safeInterval = function(fn, t){
 if( typeof(fn) == typeof(function(){}) ){
  var arg = argToArray(arguments).slice(2);
  return setInterval(function(){
   fn.apply(this, arg);
  }, t);
 }
 return setInterval(fn, t);
};
// funkcja testowa sprawdzajaca poprawnosc powyzszych funkcji
var testFunction = function(a, b){
 alert('1. ' + a + '\n2. ' + b);
};

// uruchomienie setTimeout w IE nie da oczekiwanego rezultatu (screen powyzej)
// parametry nie sa przekazywane do wywolywanej funkcji
setTimeout(testFunction, 1, 'pierwszy argument', 'drugi argument');

// wywolanie funkcji naprawiajacej ten problem przynosi oczekiwany rezultat
safeTimeout(testFunction, 1, 'pierwszy argument', 'drugi argument');

// test funkcji dla pierwszego parametru bedacego String`iem
safeTimeout('alert(\'evaluation test\')', 1);

// uruchomienie funkcji czyszczacej interwal czasowy z opoznieniem 5 sekund
safeTimeout(function(iid){
  clearInterval(iid);
 },
 5e3,
 // uruchomienie funcki testowej w interwalach co 1 sekunde
 safeInterval(testFunction, 1e3, 'pierwszy argument', 'drugi argument')
);