La classe `Object` in JavaScript
Questo articolo spiega la classe Object in JavaScript.
Questo articolo spiega la classe Object in JavaScript, includendo esempi pratici.
YouTube Video
La classe Object in JavaScript
Object è un oggetto integrato che funge da base per tutti gli oggetti JavaScript. Molte delle caratteristiche principali del linguaggio, come la gestione delle proprietà, l'ereditarietà (catena di prototipi), l'enumerazione, la clonazione e il congelamento, sono fornite attraverso il comportamento di Object.
Object.create
Esistono diversi modi per creare oggetti, e dovresti usarli in modo appropriato a seconda dello scopo.
Letterale oggetto (il più comune)
Il codice seguente mostra il modo più semplice e leggibile per creare un oggetto.
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"
- In questo esempio, proprietà e metodi sono definiti usando letterali. È semplice e generalmente offre prestazioni superiori.
Costruttore new Object()
Il costruttore Object viene usato raramente, ma è utile comprenderne il comportamento.
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()restituisce un oggetto vuoto, ma il letterale{}è più breve e più comune.
Specificare il prototipo con Object.create
Object.create viene usato per creare un oggetto con un prototipo specificato.
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.createè ideale per la progettazione di oggetti basata su ereditarietà, consentendo di controllare con precisione la catena dei prototipi.
Attributi e descrittori delle proprietà
Le proprietà hanno attributi come 'value', 'writable', 'enumerable' e 'configurable', che possono essere gestiti in dettaglio con Object.defineProperty.
Esempio di base dell’uso di defineProperty
Segue un esempio di definizione di proprietà non enumerabili e di sola lettura usando 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
- L'utilizzo di
definePropertyconsente di controllare con precisione il comportamento delle proprietà, come enumerazione, riscrittura ed eliminazione.
Proprietà accessor (getter / setter)
Con gli accessor, puoi inserire logica nelle letture e scritture delle proprietà.
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
- Con
getteresetter, puoi trattare l’accesso alle proprietà come una API esterna e aggiungere validazione o effetti collaterali.
Prototipo ed ereditarietà (prototype / __proto__ / Object.getPrototypeOf)
L’ereditarietà in JavaScript si basa sulla catena dei prototipi, non sulle classi. Gli oggetti possono riferirsi ad altri oggetti come loro prototipi.
Object.getPrototypeOf e Object.setPrototypeOf
Il seguente esempio mostra come ispezionare e impostare i prototipi.
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.getPrototypeOfrecupera il prototipo di un oggetto.Object.setPrototypeOfcambia il prototipo di un oggetto esistente, ma dovresti usarlo con cautela perché può influire sulle prestazioni.
Metodi integrati importanti
Spiegheremo chiaramente i metodi più comunemente usati e importanti selezionati tra i metodi di istanza forniti da Object.prototype, così come i metodi statici appartenenti a Object.
hasOwnProperty, isPrototypeOf, toString, valueOf
hasOwnProperty, isPrototypeOf, toString, e valueOf definiscono il comportamento di base degli oggetti.
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
hasOwnPropertyè un metodo essenziale per verificare se una proprietà è direttamente sull’oggetto.isPrototypeOfverifica se l'oggetto target ha se stesso come prototipo.
Object.keys, Object.values, Object.entries
Object.keys, Object.values e Object.entries restituiscono elenchi delle proprietà enumerabili proprie di un oggetto. Sono utili per l’iterazione e la trasformazione.
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));- Questi sono spesso usati per iterare e trasformare oggetti.
Object.assign
Object.assign viene usato per la copia superficiale e la fusione. Nota che prototipi e proprietà accessor non vengono copiati.
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)
- Per oggetti nidificati, vengono copiate solo le referenze, quindi è necessaria un’implementazione diversa per la clonazione profonda.
Object.freeze, Object.seal, Object.preventExtensions
Object.freeze, Object.seal e Object.preventExtensions controllano la mutabilità degli oggetti.
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
freezeè il più severo; impedisce qualsiasi modifica alle proprietà dell’oggetto.sealimpedisce di aggiungere o eliminare proprietà, ma permette di modificare i valori delle proprietà esistenti.preventExtensionsimpedisce solo l'aggiunta di nuove proprietà; le proprietà esistenti possono ancora essere modificate o eliminate.
Enumerabilità e ordine degli oggetti, e for...in / for...of
for...in enumera i nomi delle proprietà enumerabili, ma include anche le proprietà della catena dei prototipi, quindi spesso viene usato insieme a hasOwnProperty. Combinare Object.keys() con for...of è più sicuro e rende le tue intenzioni più chiare.
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}- Le regole per l’enumerazione delle proprietà sono definite nella specifica ECMAScript, e ci sono casi in cui l’ordine di enumerazione è garantito e altri in cui non lo è. Come regola generale, le chiavi interpretate come numeri sono ordinate in ordine crescente, mentre le altre seguono l'ordine di inserimento.
Clonazione e copia profonda
Esistono due tipi di copia degli oggetti: la copia superficiale utilizzando Object.assign o la sintassi spread, e la copia profonda utilizzando metodi ricorsivi. È importante utilizzarle in modo appropriato a seconda della situazione.
Copia superficiale (sintassi 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
- Con la copia superficiale, gli oggetti nidificati condividono le referenze, quindi le modifiche all’oggetto originale possono influenzare la copia.
Copia profonda semplice (con avvertenze)
Usare il trucco JSON è un modo veloce per creare una copia profonda, ma presenta svantaggi come la perdita di funzioni, Date, riferimenti circolari e valori undefined. Per una vera clonazione profonda, è necessario usare una libreria dedicata.
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
- I metodi basati su JSON sono convenienti per gestire rapidamente dati semplici, ma nei casi comuni il loro comportamento può non funzionare correttamente.
Mixin e composizione di oggetti
Al posto dell’ereditarietà multipla, viene spesso usato il pattern della composizione dei comportamenti tramite mixin.
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"
- I mixin sono flessibili, ma bisogna fare attenzione alle collisioni di nomi delle proprietà e alla testabilità.
Errori comuni e migliori pratiche
Ecco alcuni errori comuni e migliori pratiche.
-
Mutabilità Gli oggetti sono mutabili per impostazione predefinita. Nelle applicazioni con gestione dello stato, considera strutture dati immutabili utilizzando
Object.freezeo una libreria immutabile. -
Inquinamento del prototipo Unire dati esterni in un oggetto direttamente con
Object.assigno cicli può causare effetti collaterali inaspettati con proprietà speciali come__proto__oconstructor, creando rischi di sicurezza. Filtra l’input dell’utente prima di unirlo direttamente. -
Insidie di
for...infor...inenumera anche le proprietà del prototipo, quindi verifica conhasOwnProperty. UsareObject.keysè più chiaro. -
Uso improprio della copia superficiale Considera se è necessaria una copia profonda per evitare che modifiche agli oggetti nidificati influenzino l’oggetto originale.
Esempio pratico: pattern di aggiornamento oggetto immutabile
Pattern che restituiscono un nuovo oggetto senza modificare direttamente lo stato sono comunemente usati in React e librerie simili.
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
- Questo codice è un esempio di creazione di un nuovo oggetto stato senza modificare direttamente l'oggetto
stateoriginale. La funzionetoggleTodocopia l'arraytodose restituisce un nuovo oggetto con solo l'elemento interessato modificato, così lo stato originale rimane invariato. - Gli aggiornamenti immutabili riducono gli effetti collaterali e facilitano la gestione dello stato.
Esempio pratico: fusione sicura (attenzione all’inquinamento del prototipo)
Quando si unisce JSON esterno, ignora __proto__ per evitare l’inquinamento del prototipo.
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
- Questo tipo di protezione è importante anche in librerie e framework.
Considerazioni sulle prestazioni
Per quanto riguarda le prestazioni, considera i seguenti punti:.
- Evita cambiamenti frequenti del prototipo (
Object.setPrototypeOf) o aggiunte/rimozioni dinamiche di proprietà, poiché ostacolano l’ottimizzazione del motore. - Quando si generano molti oggetti piccoli, l’ottimizzazione diventa più efficace se si utilizzano oggetti con una struttura uniforme (lo stesso insieme di proprietà).
- La copia profonda è costosa. Riduci al minimo il loro uso o considera l’uso di aggiornamenti basati su differenze.
Riepilogo
Object è centrale in JavaScript, offrendo una varietà di funzionalità come creazione di oggetti, controllo delle proprietà, ereditarietà, copia e gestione della mutabilità. È importante comprendere API come Object.defineProperty, Object.assign e Object.freeze, e progettare con attenzione per evitare insidie come l'inquinamento del prototipo e la copia superficiale.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.