
Object.defineProperty v JavaScriptu
Jak pomocí Object.defineProperty definovat vlastnosti objektů s přesnou kontrolou nad jejich chováním.
Metoda Object.defineProperty umožňuje definovat novou vlastnost objektu nebo upravit existující s přesnou kontrolou nad jejím chováním. Na rozdíl od přímého přiřazení (obj.prop = hodnota) můžete nastavit, zda je vlastnost zapisovatelná, enumerovatelná nebo konfigurovatelná.
Základní syntaxe
Object.defineProperty(objekt, nazevVlastnosti, deskriptor);
Metoda vrací původní objekt s přidanou nebo upravenou vlastností.
Deskriptor vlastnosti
Deskriptor je objekt, který popisuje chování vlastnosti. Existují dva typy deskriptorů:
Datový deskriptor
Object.defineProperty(obj, "jmeno", {
value: "Jan", // hodnota vlastnosti
writable: true, // lze měnit hodnotu?
enumerable: true, // zobrazí se ve for...in?
configurable: true // lze smazat nebo změnit deskriptor?
});
Přístupový deskriptor (getter/setter)
Object.defineProperty(obj, "jmeno", {
get() { return this._jmeno; },
set(hodnota) { this._jmeno = hodnota; },
enumerable: true,
configurable: true
});
Pozor: Datový a přístupový deskriptor nelze kombinovat. Vlastnost má buď value/writable, nebo get/set.
Výchozí hodnoty
Při použití Object.defineProperty jsou výchozí hodnoty restriktivní:
// Toto vytvoří vlastnost, kterou nelze změnit ani smazat
Object.defineProperty(obj, "konstanta", {
value: 42
});
// Ekvivalent:
Object.defineProperty(obj, "konstanta", {
value: 42,
writable: false, // výchozí
enumerable: false, // výchozí
configurable: false // výchozí
});
Oproti tomu přímé přiřazení nastaví všechny příznaky na true:
obj.prop = 42;
// Ekvivalent:
Object.defineProperty(obj, "prop", {
value: 42,
writable: true,
enumerable: true,
configurable: true
});
Praktické příklady
Konstantní vlastnost
const config = {};
Object.defineProperty(config, "API_URL", {
value: "https://api.example.com",
writable: false,
enumerable: true,
configurable: false
});
config.API_URL = "https://jina.url"; // V strict mode vyhodí TypeError
console.log(config.API_URL); // "https://api.example.com"
Skrytá vlastnost
Vlastnost s enumerable: false se nezobrazí ve for...in, Object.keys() ani JSON.stringify():
const uzivatel = { jmeno: "Jan" };
Object.defineProperty(uzivatel, "_id", {
value: 12345,
enumerable: false
});
console.log(Object.keys(uzivatel)); // ["jmeno"]
console.log(uzivatel._id); // 12345
console.log(JSON.stringify(uzivatel)); // '{"jmeno":"Jan"}'
Computed property (getter)
const kruh = { polomer: 5 };
Object.defineProperty(kruh, "obsah", {
get() {
return Math.PI * this.polomer ** 2;
},
enumerable: true
});
console.log(kruh.obsah); // 78.54...
kruh.polomer = 10;
console.log(kruh.obsah); // 314.15...
Validace při přiřazení (setter)
const osoba = { _vek: 0 };
Object.defineProperty(osoba, "vek", {
get() {
return this._vek;
},
set(hodnota) {
if (typeof hodnota !== "number" || hodnota < 0) {
throw new Error("Věk musí být nezáporné číslo");
}
this._vek = hodnota;
},
enumerable: true
});
osoba.vek = 25; // OK
osoba.vek = -5; // Error: Věk musí být nezáporné číslo
Reálné příklady z praxe
Deprecation warning
Označení vlastnosti jako zastaralé — uživatel dostane varování, ale kód stále funguje:
function deprecate(obj, prop, novyNazev) {
const hodnota = obj[prop];
Object.defineProperty(obj, prop, {
get() {
console.warn(`${prop} je zastaralé, použijte ${novyNazev}`);
return hodnota;
},
enumerable: false
});
}
const config = { apiUrl: "https://api.example.com" };
config.API_URL = config.apiUrl; // stará verse
deprecate(config, "API_URL", "apiUrl");
config.API_URL; // Warning: API_URL je zastaralé, použijte apiUrl
Lazy loading (memoisace)
Hodnota se vypočítá až při prvním přístupu a pak se uloží:
function lazy(obj, prop, vypocet) {
Object.defineProperty(obj, prop, {
get() {
const hodnota = vypocet();
// Přepíše getter na prostou hodnotu
Object.defineProperty(obj, prop, {
value: hodnota,
writable: false,
enumerable: true
});
return hodnota;
},
configurable: true,
enumerable: true
});
}
const modul = {};
lazy(modul, "tezkéData", () => {
console.log("Načítám data...");
return { /* ... velký objekt ... */ };
});
// Nic se nenačítá, dokud nepřistoupíme
console.log(modul.tezkéData); // "Načítám data..." + vrátí objekt
console.log(modul.tezkéData); // Vrátí objekt bez logování (už je uložené)
Polyfill pro chybějící metodu
Přidání metody do prototypu s korektními příznaky:
// Polyfill pro Array.prototype.at (před ES2022)
if (!Array.prototype.at) {
Object.defineProperty(Array.prototype, "at", {
value: function(index) {
if (index < 0) index = this.length + index;
return this[index];
},
writable: true,
enumerable: false, // Nesmí se objevit ve for...in
configurable: true
});
}
[1, 2, 3].at(-1); // 3
Ochrana globálních objektů
Zabránění přepsání důležitých funkcí (např. v security kontextu):
// Zamknutí console.log proti přepsání
Object.defineProperty(console, "log", {
value: console.log,
writable: false,
configurable: false
});
console.log = function() {}; // TypeError v strict mode, tiše selže jinak
console.log("Stále funguje!"); // Funguje
Sledování změn (debugging)
Zjištění, kdo a kdy mění hodnotu:
function sleduj(obj, prop) {
let hodnota = obj[prop];
Object.defineProperty(obj, prop, {
get() { return hodnota; },
set(nova) {
console.log(`${prop}: ${hodnota} → ${nova}`);
console.trace(); // Zobrazí call stack
hodnota = nova;
}
});
}
const state = { count: 0 };
sleduj(state, "count");
state.count = 1; // "count: 0 → 1" + stack trace
state.count = 2; // "count: 1 → 2" + stack trace
Reaktivita ve Vue 2
Vue 2 používal Object.defineProperty jako základ svého reaktivního systému. Zjednodušená verse toho, co framework dělá interně:
function defineReactive(obj, key) {
let value = obj[key];
const subscribers = []; // komponenty závislé na této vlastnosti
Object.defineProperty(obj, key, {
get() {
// Při renderování komponenty ji zaregistruj jako závislost
if (currentlyRenderingComponent) {
subscribers.push(currentlyRenderingComponent);
}
return value;
},
set(newValue) {
value = newValue;
// Při změně překresli všechny závislé komponenty
subscribers.forEach(component => component.update());
}
});
}
// Vue 2 toto volá pro každou vlastnost v data()
function observe(obj) {
for (const key of Object.keys(obj)) {
defineReactive(obj, key);
}
}
Tento přístup měl známá omezení:
// Nefungovalo reaktivně — vlastnost neexistovala při observe()
this.novaVlastnost = "hodnota";
// Workaround
Vue.set(this, "novaVlastnost", "hodnota");
// Nefungovalo reaktivně — index pole není "vlastnost"
this.pole[0] = "nova";
// Workaround
Vue.set(this.pole, 0, "nova");
Vue 3 přešel na Proxy, který tyto problémy nemá — zachytí i nové vlastnosti a přístup přes index. Právě limity Object.defineProperty byly hlavním důvodem přechodu.
Definice více vlastností najednou
Pro definici více vlastností použijte Object.defineProperties:
const obj = {};
Object.defineProperties(obj, {
jmeno: {
value: "Jan",
writable: true,
enumerable: true
},
vek: {
value: 30,
writable: true,
enumerable: true
},
_tajne: {
value: "skryto",
enumerable: false
}
});
Zjištění deskriptoru
Deskriptor existující vlastnosti zjistíte pomocí Object.getOwnPropertyDescriptor:
const obj = { jmeno: "Jan" };
const deskriptor = Object.getOwnPropertyDescriptor(obj, "jmeno");
console.log(deskriptor);
// {
// value: "Jan",
// writable: true,
// enumerable: true,
// configurable: true
// }
Monkey-patching knihoven
Častý use case pro Object.defineProperty je úprava chování existujícího kódu — například knihovny třetí strany, kterou nemůžete přímo upravit:
// Přidání logování do existující metody knihovny
const puvodniFetch = window.fetch;
Object.defineProperty(window, "fetch", {
value: function(...args) {
console.log("Fetch volán s:", args[0]);
return puvodniFetch.apply(this, args);
},
writable: true,
configurable: true
});
Nebo sledování přístupu k vlastnosti:
// Zachycení, kdy někdo čte localStorage
const puvodniStorage = window.localStorage;
Object.defineProperty(window, "localStorage", {
get() {
console.log("Přístup k localStorage");
return puvodniStorage;
},
configurable: true
});
Pozor: Monkey-patching je mocný nástroj, ale snadno vede k těžko laditelným chybám. Používejte opatrně a pouze tam, kde není jiná možnost.
Srovnání s Proxy
Proxy je modernější alternativa (ES6+), která umožňuje zachytit jakoukoliv operaci nad objektem:
// Object.defineProperty — musíte definovat každou vlastnost zvlášť
const obj1 = {};
Object.defineProperty(obj1, "a", {
get() { console.log("čtení a"); return this._a; },
set(v) { console.log("zápis a"); this._a = v; }
});
// Proxy — zachytí všechny vlastnosti najednou
const obj2 = new Proxy({}, {
get(target, prop) {
console.log(`čtení ${prop}`);
return target[prop];
},
set(target, prop, value) {
console.log(`zápis ${prop}`);
target[prop] = value;
return true;
}
});
Kdy použít co
| Object.defineProperty | Proxy | |
|---|---|---|
| Známé vlastnosti | Ano | Ano |
| Dynamické vlastnosti | Ne (musíte znát název předem) | Ano (zachytí cokoliv) |
| Úprava existujícího objektu | Ano (in-place) | Ne (vytváří wrapper) |
| Zachycení delete, in, … | Ne | Ano |
| Podpora IE11 | Ano | Ne |
| Výkon | Rychlejší | Pomalejší (režie trapu) |
Proxy je lepší volba pro reaktivní systémy (Vue 3), validaci vstupů, nebo když potřebujete zachytit operace nad neznámými vlastnostmi. defineProperty je lepší pro úpravu konkrétních vlastností existujících objektů (monkey-patching) nebo když potřebujete podporu starších prohlížečů.
Jednodušší alternativy
Pro běžné případy existují jednodušší přístupy:
Getter/setter v objektovém literálu
const kruh = {
polomer: 5,
get obsah() {
return Math.PI * this.polomer ** 2;
}
};
Getter/setter ve třídě
class Kruh {
constructor(polomer) {
this.polomer = polomer;
}
get obsah() {
return Math.PI * this.polomer ** 2;
}
}
Object.freeze pro neměnnost
const config = Object.freeze({
API_URL: "https://api.example.com",
TIMEOUT: 5000
});
config.API_URL = "jina"; // Tiše selže (v strict mode TypeError)
config.NOVA = "x"; // Tiše selže
Kdy je to antipattern
Gettery a settery s Object.defineProperty umožňují vytvořit „magické” objekty — vypadají jako běžné vlastnosti, ale na pozadí spouští libovolný kód. To může být problém:
// Tohle vypadá nevinně...
user.email = "test@example.com"
// ...ale spustilo validaci, API call, toast notifikaci a analytics event
Problematické použití
- Skryté side-effecty — přiřazení vypadá triviálně, ale dělá netriviální věci
- Těžko debugovatelné — když
obj.x = 5nefunguje, je těžké zjistit proč - Porušení principu nejmenšího překvapení — kolega netuší, že jednoduchý assignment má vedlejší efekty
Kdy je to v pořádku
- Framework s jasnou konvencí — Vue, Svelte, MobX — všichni vědí, že reaktivní state „dělá věci”
- Dobře zdokumentované API — název funkce jasně říká, co objekt dělá (např.
createReactiveStore()) - Interní implementace — uživatel knihovny přímo s gettery nepracuje
- Computed properties bez side-effectů — getter jen počítá hodnotu z jiných vlastností
Alternativa — explicitní API
// Místo magie
params.limit = 50
// Explicitní metoda
params.set("limit", 50)
// nebo
updateParams({ limit: 50 })
Doporučení: Pro běžný aplikační kód preferujte explicitní metody. Pro knihovny a frameworky je „magie” akceptovatelná, pokud je konsistentní, zdokumentovaná a očekávatelná.
Kdy použít Object.defineProperty
Hlavní síla Object.defineProperty je v tom, že vytvoříte objekt, který vypadá běžně, ale chová se „magicky”:
// Uživatel vidí toto:
params.limit = 50
// Ale pod kapotou se děje:
// setter: validace → aktualisace URL → sync do storage
Typické legitimní použití:
- Monkey-patching — úprava chování knihoven třetích stran nebo globálních objektů
- Skryté vlastnosti — interní stav, který nemá být vidět v JSON nebo enumeraci
- Neměnné vlastnosti — konstanty, které nelze přepsat
- Polyfilly — přidání chybějících metod do prototypů (např.
Array.prototype.includes) - Dynamické gettery/settery — když názvy vlastností nejsou známé v době kompilace
- Reaktivní systémy — Vue 2 používal
definePropertypro sledování změn
Pro většinu běžného kódu jsou objektové literály s gettery/settery, třídy nebo Proxy čitelnější a flexibilnější.