La classe `Object` in JavaScript

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 defineProperty consente 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 getter e setter, 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.getPrototypeOf recupera il prototipo di un oggetto.
  • Object.setPrototypeOf cambia 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.
  • isPrototypeOf verifica 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.
  • seal impedisce di aggiungere o eliminare proprietà, ma permette di modificare i valori delle proprietà esistenti.
  • preventExtensions impedisce 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.freeze o una libreria immutabile.

  • Inquinamento del prototipo Unire dati esterni in un oggetto direttamente con Object.assign o cicli può causare effetti collaterali inaspettati con proprietà speciali come __proto__ o constructor, creando rischi di sicurezza. Filtra l’input dell’utente prima di unirlo direttamente.

  • Insidie di for...in for...in enumera anche le proprietà del prototipo, quindi verifica con hasOwnProperty. Usare Object.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 state originale. La funzione toggleTodo copia l'array todos e 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.

YouTube Video