Vacuità
Non consiglio di provare a definire o utilizzare una funzione che calcola se un valore in tutto il mondo è vuoto. Cosa significa veramente essere "vuoti"? Se ho let human = { name: 'bob', stomach: 'empty' }
, dovrebbe isEmpty(human)
tornare true
? Se ho let reg = new RegExp('');
, dovrebbe isEmpty(reg)
tornare true
? Che dire isEmpty([ null, null, null, null ])
- questo elenco contiene solo il vuoto, quindi l'elenco stesso è vuoto? Voglio presentare qui alcune note su "vacuità" (una parola intenzionalmente oscura, per evitare associazioni preesistenti) in javascript - e voglio sostenere che "vacuità" nei valori di javascript non dovrebbe mai essere trattata genericamente.
Truthiness / Falsiness
Per decidere come determinare la "vacuità" dei valori, dobbiamo soddisfare il senso innato e intrinseco di javascript sul fatto che i valori siano "veritieri" o "falsi". Naturalmente, null
e undefined
sono entrambi "falsi". Meno naturalmente, il numero 0
(e nessun altro numero tranne NaN
) è anche "falsa". Almeno naturalmente: ''
è falso, ma []
e {}
(e new Set()
, e new Map()
) sono sinceri, anche se sembrano tutti ugualmente vacui!
Null vs Undefined
C'è anche qualche discussione riguardante il null
vs undefined
: abbiamo davvero bisogno di entrambi per esprimere vacuità nei nostri programmi? Personalmente evito di far apparire nel mio codice le lettere u, n, d, e, f, i, n, e, d in questo ordine. Uso sempre null
per indicare "vacuità". Ancora una volta, tuttavia, dobbiamo soddisfare il senso intrinseco di JavaScript di come null
e undefined
differire:
- Cercare di accedere a una proprietà inesistente dà
undefined
- Se si omette un parametro quando si chiama una funzione, il parametro riceve
undefined
:
let f = a => a;
console.log(f('hi'));
console.log(f());
- I parametri con valori predefiniti ricevono il valore predefinito solo se indicato
undefined
, non null
:
let f = (v='hello') => v;
console.log(f(null));
console.log(f(undefined));
Vacuosità non generica
Credo che la vacuità non debba mai essere trattata in modo generico. Dovremmo invece sempre avere il rigore di ottenere maggiori informazioni sui nostri dati prima di determinare se sono vacui - lo faccio principalmente controllando il tipo di dati con cui ho a che fare:
let isType = (value, Cls) => {
try {
return Object.getPrototypeOf(value).constructor === Cls;
} catch(err) {
return false;
}
};
Si noti che questa funzione ignora il polimorfismo: si aspetta value
che sia un'istanza diretta Cls
e non un'istanza di una sottoclasse di Cls
. Evito instanceof
per due motivi principali:
([] instanceof Object) === true
("Una matrice è un oggetto")
('' instanceof String) === false
("Una stringa non è una stringa")
Si noti che Object.getPrototypeOf
viene utilizzato per evitare un caso come let v = { constructor: String };
La isType
funzione restituisce ancora correttamente per isType(v, String)
(false) eisType(v, Object)
(true).
Nel complesso, consiglio di utilizzare questa isType
funzione insieme a questi suggerimenti:
- Ridurre al minimo la quantità di valori di elaborazione del codice di tipo sconosciuto. Ad esempio, per
let v = JSON.parse(someRawValue);
, la nostra v
variabile è ora di tipo sconosciuto. Il prima possibile, dovremmo limitare le nostre possibilità. Il modo migliore per farlo può essere quello di richiedere un tipo particolare: ad esempio if (!isType(v, Array)) throw new Error('Expected Array');
- questo è un modo davvero rapido ed espressivo per rimuovere la natura generica di v
, e assicurarsi che sia sempre un Array
. A volte, tuttavia, dobbiamo consentire v
di essere di più tipi. In questi casi, dovremmo creare blocchi di codice dove v
non è più generico, il più presto possibile:
if (isType(v, String)) {
/* v isn't generic in this block - It's a String! */
} else if (isType(v, Number)) {
/* v isn't generic in this block - It's a Number! */
} else if (isType(v, Array)) {
/* v isn't generic in this block - it's an Array! */
} else {
throw new Error('Expected String, Number, or Array');
}
- Utilizzare sempre "whitelist" per la convalida. Se richiedi che un valore sia, ad esempio, una stringa, un numero o una matrice, controlla le 3 possibilità "bianche" e genera un errore se nessuna delle 3 è soddisfatta. Dovremmo essere in grado di vedere che il controllo per la possibilità di "nero" non è molto utile: Dire che scriviamo
if (v === null) throw new Error('Null value rejected');
- questo è grande per garantire che null
i valori non fanno fino in fondo, ma se un valore lo fa fare attraverso, sappiamo ancora poco niente al riguardo. Un valore v
che supera questo controllo null è ancora MOLTO generico - è tutt'altro chenull
! Le blacklist difficilmente dissipano la genericità.
A meno che non sia un valore null
, non considerare mai "un valore vacuo". Invece, considera "una X che è vacua". In sostanza, non considerare mai di fare qualcosa del genere if (isEmpty(val)) { /* ... */ }
- non importa come isEmpty
sia implementata quella funzione (non voglio saperlo ...), non è significativo! Ed è troppo generico! La vacuità dovrebbe essere calcolata solo con la conoscenza del val
tipo. I controlli di vacuità dovrebbero apparire così:
- "Una stringa, senza caratteri":
if (isType(val, String) && val.length === 0) ...
- "Un oggetto, con 0 oggetti di scena":
if (isType(val, Object) && Object.entries(val).length === 0) ...
- "Un numero, uguale o inferiore a zero":
if (isType(val, Number) && val <= 0) ...
"Una matrice, senza elementi": if (isType(val, Array) && val.length === 0) ...
L'unica eccezione è quando null
viene utilizzato per indicare determinate funzionalità. In questo caso è significativo dire: "Un valore vacuo":if (val === null) ...