Polimorfizm w obiektach JavaScript – o co chodzi i jak to wygląda w praktyce?
Polimorfizm brzmi „mądrze”, ale w programowaniu oznacza bardzo praktyczną rzecz:
możesz traktować różne obiekty w ten sam sposób, bo mają wspólny „interfejs” (np. metodę o tej samej nazwie),
a mimo to zachowują się inaczej. W JavaScripcie polimorfizm spotkasz bardzo często, nawet jeśli nie używasz klas.
1) Najkrótsza definicja
Polimorfizm = jeden sposób użycia (ta sama metoda), wiele zachowań (różne implementacje).
Przykład: wywołujesz obj.render() na różnych obiektach i każdy renderuje po swojemu.
2) Polimorfizm w JS bez interfejsów (duck typing)
JavaScript nie wymaga formalnych interfejsów. Liczy się to, że obiekt „wygląda jak trzeba”.
Jeśli przekazujesz do funkcji obiekt, który ma metodę area(), to funkcja może z niej skorzystać —
nieważne, czy to koło, prostokąt czy coś jeszcze.
const Circle = r => ({
area() { return Math.PI * r * r; }
});
const Rectangle = (w, h) => ({
area() { return w * h; }
});
function printArea(shape) {
console.log(shape.area()); // działa dla różnych obiektów, byle miały area()
}
printArea(Circle(2));
printArea(Rectangle(3, 4));
Taki styl nazywa się często duck typing: „jeśli coś kwacze jak kaczka, to traktuj to jak kaczkę”.
Czyli: jeśli obiekt ma metodę area(), to traktujemy go jako „coś, co ma pole”.
3) Polimorfizm w klasach: nadpisywanie metod (override)
Jeśli używasz klas i dziedziczenia, polimorfizm najczęściej objawia się przez
nadpisywanie (override) metod. Ta sama metoda (speak()) istnieje w klasie bazowej,
ale klasy pochodne implementują ją inaczej.
class Animal {
speak() { return "…"; }
}
class Dog extends Animal {
speak() { return "Hau!"; }
}
class Cat extends Animal {
speak() { return "Miau!"; }
}
const animals = [new Dog(), new Cat(), new Animal()];
console.log(animals.map(a => a.speak())); // ["Hau!", "Miau!", "…"]
Ważne: kod wywołujący speak() nie musi wiedzieć, czy ma do czynienia z psem czy kotem.
Wywołuje metodę w ten sam sposób, a wynik zależy od konkretnego obiektu.
4) Polimorfizm bez dziedziczenia: kompozycja (często lepsza w JS)
W JavaScripcie wiele projektów woli kompozycję (składanie obiektów) zamiast rozbudowanego dziedziczenia.
Zamiast robić extends, doklejasz obiektom wspólne zachowania.
Nadal masz polimorfizm: różne obiekty udostępniają tę samą metodę, np. save().
const withSave = state => ({
save() { console.log("Zapisuję:", state); }
});
const User = name => ({ name, ...withSave({ name }) });
const Document = title => ({ title, ...withSave({ title }) });
function saveAll(items) {
items.forEach(i => i.save());
}
saveAll([User("Ala"), Document("Umowa")]);
Zwróć uwagę: saveAll nie interesuje się typem obiektu. Ważne jest tylko to, że każdy element ma metodę save().
5) Dlaczego polimorfizm jest przydatny?
- Mniej ifów: zamiast
if (typ === ...)wywołujesz metodę i tyle. - Łatwiejsze rozszerzanie: dodajesz nowy typ obiektu z tą samą metodą, a reszta kodu działa bez zmian.
- Czystszy kod: logika „jak coś zrobić” siedzi w obiekcie, a nie w setce warunków.
- Lepsze testowanie: możesz łatwo podmieniać obiekty (np. mocki) byle miały ten sam interfejs.
6) Typowa pułapka: „interfejs” tylko umowny
Ponieważ JS nie wymusza interfejsów, łatwo przekazać obiekt, który nie ma wymaganej metody.
Dlatego w praktyce często:
- pilnuje się konwencji (np. każdy „renderer” ma
render()), - dodaje się walidację lub sensowne błędy,
- albo używa TypeScript, który potrafi interfejsy sprawdzać.
function renderAll(items) {
items.forEach(i => {
if (typeof i.render !== "function") {
throw new Error("Obiekt nie ma metody render()");
}
i.render();
});
}
