Klasa `Object` w JavaScript
Ten artykuł wyjaśnia klasę Object w JavaScript.
Ten artykuł wyjaśnia klasę Object w JavaScript, zawierając praktyczne przykłady.
YouTube Video
Klasa Object w JavaScript
Object to wbudowany obiekt, który jest bazą dla wszystkich obiektów w JavaScript. Wiele podstawowych funkcji języka, takich jak zarządzanie właściwościami, dziedziczenie (łańcuch prototypów), enumeracja, klonowanie i zamrażanie, jest dostarczanych poprzez zachowanie Object.
Object.create
Istnieje wiele sposobów tworzenia obiektów i warto wybrać odpowiedni w zależności od celu.
Literał obiektu (najczęstszy sposób)
Poniższy kod pokazuje najprostszy i najbardziej czytelny sposób tworzenia obiektu.
1// Create an object using object literal
2const user = {
3 name: "Alice",
4 age: 30,
5 greet() {
6 return `Hello, I'm ${this.name}`;
7 }
8};
9
10console.log(user.greet()); // "Hello, I'm Alice"
- W tym przykładzie właściwości i metody są definiowane za pomocą literałów. Jest to proste i zazwyczaj zapewnia lepszą wydajność.
Konstruktor new Object()
Konstruktor Object jest rzadko używany, ale warto zrozumieć jego działanie.
1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
new Object()zwraca pusty obiekt, ale literał{}jest krótszy i częściej używany.
Określanie prototypu za pomocą Object.create
Object.create służy do tworzenia obiektu z określonym prototypem.
1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
Object.createjest idealne do projektowania obiektów opartych na dziedziczeniu, umożliwiając precyzyjną kontrolę nad łańcuchem prototypów.
Atrybuty właściwości i deskryptory
Właściwości mają atrybuty takie jak 'value', 'writable', 'enumerable' i 'configurable', które można szczegółowo kontrolować za pomocą Object.defineProperty.
Podstawowy przykład użycia defineProperty
Następnie przedstawiono przykład definiowania właściwości nieenumerowalnych i tylko do odczytu za pomocą defineProperty.
1// Define a non-enumerable read-only property
2const person = { name: "Carol" };
3
4Object.defineProperty(person, "id", {
5 value: 12345,
6 writable: false,
7 enumerable: false,
8 configurable: false
9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
- Użycie
definePropertypozwala precyzyjnie kontrolować zachowanie właściwości, takie jak enumeracja, nadpisywanie i usuwanie.
Właściwości dostępowe (getter / setter)
Dzięki akcesorom można dodawać własną logikę do odczytu i zapisu właściwości.
1// Use getter and setter to manage internal state
2const data = {
3 _value: 1,
4 get value() {
5 return this._value;
6 },
7 set value(v) {
8 if (typeof v === "number" && v > 0) {
9 this._value = v;
10 } else {
11 throw new Error("value must be a positive number");
12 }
13 }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
- Dzięki
getterisetterdostęp do właściwości można traktować jak zewnętrzne API oraz dodać walidację lub skutki uboczne.
Prototyp i dziedziczenie (prototype / __proto__ / Object.getPrototypeOf)
Dziedziczenie w JavaScript opiera się na łańcuchu prototypów, a nie na klasach. Obiekty mogą odwoływać się do innych obiektów jako swoich prototypów.
Object.getPrototypeOf i Object.setPrototypeOf
Poniższy przykład pokazuje, jak sprawdzać i ustawiać prototypy.
1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
Object.getPrototypeOfpobiera prototyp obiektu.Object.setPrototypeOfzmienia prototyp istniejącego obiektu, ale należy go używać ostrożnie, ponieważ może to wpłynąć na wydajność.
Ważne wbudowane metody
Jasno wyjaśnimy najczęściej używane i najważniejsze metody wybrane spośród metod instancji dostarczonych przez Object.prototype, a także metody statyczne będące własnością Object.
hasOwnProperty, isPrototypeOf, toString, valueOf
hasOwnProperty, isPrototypeOf, toString i valueOf definiują podstawowe zachowanie obiektów.
1// Demonstrate prototype methods
2const base = { greet() { return "hello"; } };
3const child = Object.create(base);
4const a = { x: 1 };
5
6console.log(a.hasOwnProperty("x")); // true
7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
8
9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
hasOwnPropertyto podstawowa metoda sprawdzająca, czy właściwość należy bezpośrednio do danego obiektu.isPrototypeOfsprawdza, czy obiekt docelowy ma dany obiekt jako swój prototyp.
Object.keys, Object.values, Object.entries
Object.keys, Object.values i Object.entries zwracają listy własnych, enumerowalnych właściwości obiektu. Są one przydatne do iteracji i transformacji.
1// Keys, values and entries
2const item = { id: 1, name: "Widget", price: 9.99 };
3
4// ["id", "name", "price"]
5console.log(Object.keys(item));
6
7// [1, "Widget", 9.99]
8console.log(Object.values(item));
9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));- Są często używane do iteracji i przekształcania obiektów.
Object.assign
Object.assign służy do płytkiego kopiowania i łączenia obiektów. Należy pamiętać, że prototypy oraz właściwości dostępowe nie są kopiowane.
1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
- W przypadku zagnieżdżonych obiektów kopiowane są tylko referencje, dlatego do głębokiego klonowania potrzebna jest inna implementacja.
Object.freeze, Object.seal, Object.preventExtensions
Object.freeze, Object.seal i Object.preventExtensions kontrolują mutowalność obiektów.
1// Freeze vs seal vs preventExtensions
2const obj = { a: 1 };
3Object.freeze(obj);
4obj.a = 2; // fails silently or throws in strict mode
5delete obj.a; // fails
6
7const obj2 = { b: 2 };
8Object.seal(obj2);
9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
freezeto najbardziej restrykcyjna metoda — uniemożliwia jakiekolwiek zmiany właściwości obiektu.sealuniemożliwia dodawanie lub usuwanie właściwości, ale pozwala na zmianę wartości istniejących właściwości.preventExtensionsjedynie zapobiega dodawaniu nowych właściwości; istniejące właściwości nadal można zmieniać lub usuwać.
Enumerowalność obiektu, kolejność oraz for...in / for...of
for...in wylicza nazwy enumerowalnych właściwości, ale uwzględnia też właściwości z łańcucha prototypów, dlatego często stosuje się go razem z hasOwnProperty. Łączenie Object.keys() z for...of jest bezpieczniejsze i wyraźniej określa Twoje intencje.
1// Safe enumeration
2const obj = Object.create({ inherited: true });
3obj.own = 1;
4
5for (const key in obj) {
6 if (obj.hasOwnProperty(key)) {
7 console.log("own prop:", key);
8 } else {
9 console.log("inherited prop:", key);
10 }
11}
12
13for (const key of Object.keys(obj)) {
14 console.log("key via Object.keys:", key);
15}- Zasady enumeracji właściwości są określone w specyfikacji ECMAScript — w niektórych przypadkach kolejność jest gwarantowana, w innych nie. Zasadniczo klucze interpretowane jako liczby są uporządkowane rosnąco, podczas gdy inne klucze zachowują kolejność wstawiania.
Klonowanie i kopiowanie głębokie
Istnieją dwa typy kopiowania obiektów: płytkie kopiowanie za pomocą Object.assign lub składni rozproszenia oraz głębokie kopiowanie za pomocą metod rekurencyjnych. Ważne jest, aby stosować je odpowiednio w zależności od sytuacji.
Płytkie kopiowanie (operator spread / Object.assign)
1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
- Przy płytkim kopiowaniu zagnieżdżone obiekty współdzielą referencje, więc zmiany w oryginale wpływają na kopię.
Proste głębokie kopiowanie (z zastrzeżeniami)
Sztuczka z wykorzystaniem JSON to szybki sposób na głębokie kopiowanie, ale ma wady, np. utratę funkcji, Date, referencji cyklicznych czy wartości undefined. Aby wykonać prawdziwe głębokie klonowanie, należy użyć dedykowanej biblioteki.
1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
- Metody oparte na JSON są wygodne do szybkiego przetwarzania prostych danych, ale w typowych przypadkach ich działanie może być błędne.
Mixiny i kompozycja obiektów
Zamiast wielokrotnego dziedziczenia często stosuje się wzorzec kompozycji zachowań za pomocą mixinów.
1// Simple mixin function
2const canEat = {
3 eat() { return `${this.name} eats`; }
4};
5const canWalk = {
6 walk() { return `${this.name} walks`; }
7};
8
9function createPerson(name) {
10 const person = { name };
11 return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
- Mixiny są elastyczne, ale należy uważać na kolizje nazw właściwości i trudność w testowaniu.
Typowe pułapki i najlepsze praktyki
Oto kilka typowych pułapek i najlepszych praktyk.
-
Mutowalność Obiekty są domyślnie mutowalne. W aplikacjach z zarządzaniem stanem rozważ użycie niezmiennych struktur danych z wykorzystaniem
Object.freezelub biblioteki niezmiennych danych. -
Zanieczyszczenie prototypu Scalanie zewnętrznych danych do obiektu za pomocą
Object.assignlub pętli może prowadzić do nieoczekiwanych skutków ubocznych z powodu specjalnych właściwości takich jak__proto__czyconstructor, stwarzając zagrożenia bezpieczeństwa. Filtrować dane użytkownika przed scalaniem ich bezpośrednio z obiektem. -
Pułapki
for...infor...inwylicza również właściwości z prototypu, dlatego należy sprawdzać przezhasOwnProperty. UżywanieObject.keysjest bardziej przejrzyste. -
Błędne stosowanie płytkiego kopiowania Rozważ, czy potrzebujesz głębokiego kopiowania, aby zmiany w zagnieżdżonych obiektach nie wpływały na oryginał.
Praktyczny przykład: Wzorzec niemutowalnej aktualizacji obiektu
Wzorce zwracające nowy obiekt bez bezpośredniej modyfikacji stanu są powszechnie stosowane w React i podobnych bibliotekach.
1// Immutable update example
2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
3
4// Toggle todo done immutably
5function toggleTodo(state, todoId) {
6 return {
7 ...state,
8 todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
9 };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
- Ten kod jest przykładem tworzenia nowego obiektu stanu bez bezpośredniej modyfikacji oryginalnego obiektu
state.toggleTodokopiuje tablicętodosi zwraca nowy obiekt, w którym tylko wybrany element jest zmieniony, dzięki czemu oryginalny obiektstatepozostaje niezmieniony. - Niemutowalne aktualizacje ograniczają skutki uboczne i ułatwiają zarządzanie stanem.
Praktyczny przykład: Bezpieczne scalanie (uwaga na zanieczyszczenie prototypu)
Podczas scalania zewnętrznego JSON pomiń __proto__, aby zapobiec zanieczyszczeniu prototypu.
1// Safe merge ignoring __proto__ keys
2function safeMerge(target, source) {
3 for (const key of Object.keys(source)) {
4 if (key === "__proto__" || key === "constructor") continue;
5 target[key] = source[key];
6 }
7 return target;
8}
9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
- Tego rodzaju ochrona jest ważna również w bibliotekach i frameworkach.
Wydajność — na co zwracać uwagę
Pod kątem wydajności warto zwrócić uwagę na następujące kwestie:.
- Unikaj częstych zmian prototypów (
Object.setPrototypeOf) oraz dynamicznego dodawania/usuwania właściwości, bo utrudnia to optymalizację przez silnik. - Podczas generowania wielu małych obiektów optymalizacja staje się bardziej skuteczna, jeśli używasz obiektów o jednolitej strukturze (tym samym zestawie właściwości).
- Głębokie kopiowanie jest kosztowne. Ogranicz ich użycie lub rozważ aktualizacje na podstawie różnic (diff-based).
Podsumowanie
Object to centralny element JavaScript — zapewnia wiele funkcji, takich jak tworzenie obiektów, kontrola właściwości, dziedziczenie, kopiowanie i zarządzanie mutowalnością. Ważne jest, aby rozumieć takie API jak Object.defineProperty, Object.assign oraz Object.freeze i projektować z rozwagą, aby uniknąć pułapek, takich jak zanieczyszczenie prototypu czy płytkie kopiowanie.
Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.