Una raccolta di snippet ingannevoli e divertenti scritti in JavaScript
JavaScript è un ottimo linguaggio. Ha una sintassi semplice, un grande ecosistema e, quello che conta veramente, una community fantastica.
Allo stesso tempo, sappiamo che JavaScript è un linguaggio abbastanza strano con delle parti cervellotiche. Alcune di queste possono rendere il nostro lavoro un inferno, alcune invece possono farci ridere a crepapelle.
L'idea per WTFJS è di Brian Leroux. Questo elenco è largamente ispirato al suo talk “WTFJS” at dotJS 2012:
Puoi installare questo manuale con npm. Lancia semplicemente:
$ npm install -g wtfjs
Ora dovresti essere in grado di eseguire wtfjs dalla riga di comando. Altrimenti puoi continuare tranquillamente a leggerlo qui.
Il codice sorgente lo puoi trovare qui: https://github.com/denysdovhan/wtfjs
Attualmente wtfjs è disponibile nelle seguenti lingue:
- 💪🏻 Motivazione
- ✍🏻 Notazione
- 👀 Esempi
[]è uguale a![]trueè diverso da![], ma anche diverso da[]- true è false
- baNaNa
NaNnon èNaN- È un fail
[]è truthy, ma nontruenullè falsy, ma nonfalsedocument.allè un object, ma è undefined- Il numero più piccolo rappresentabile è maggiore di zero
- function non è una function
- Sommare array
- "Trailing commas" in un array
- L'operatore di uguaglianza sugli array è un mostro
undefinedeNumberparseIntè bast**do- Math con
trueefalse - I commenti HTML sono validi anche in JavaScript
NaNènota number[]enullsono objects- Incrementare numeri magicamente
- La precisione di
0.1 + 0.2 - Patchare numeri
- Confrontare tre numeri
- Matematica spassosa
- Somma di RegExps
- Le stringhe non sono istanze di
String - Richiamare funzioni con le backticks
- Call call call
- Una proprietà chiamata
constructor - Un Object usato come key nelle property di un oggetto
- Accedere ai prototypes con
__proto__ `${{Object}}`- Destructuring con valori di default
- Puntini e lo spreading
- Labels
- Labels annidate
- Un
try..catchinsidioso - Si tratta di ereditarietà multipla?
- Un generator che produce se stesso
- Una classe di tipo class
- Oggetti non-coercible
- Arrow functions strambe
- Arrow functions non possono essere un costruttore
argumentse arrow functions- Uno strano return
- Concatenare assegnamenti su un object
- Accedere alle properties di un object con gli array
- Null e gli operatori relazionali
Number.toFixed()mostra numeri diversiMath.max()più piccolo diMath.min()- Confrontare
nullcon0 - Alcune ridichiarazioni di variabili
- Comportamento di default di Array.prototype.sort()
- resolve() non restituisce un'istanza di Promise
- 📚 Other resources
- 🎓 License
Just for fun
— “Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds
Lo scopo principale di questo elenco è quello di raccogliere alcuni esempi strambi e mostrarne il loro funzionamento, se possibile. Semplicemente per il fatto che è divertente imparare qualcosa che non sapevamo prima.
Se sei un principiante puoi utilizzare questi appunti per approfondire JavaScript. Spero che questi appunti ti motivino a leggerne le specifiche.
Se sei uno sviluppatore senior, considera questi esempi come un'ottimo punto di riferimento per tutte quelle stranezze e stramberie del tuo amato JavaScript.
Ad ogni modo, leggilo. Probabilmente imparerai qualcosa di nuovo.
// -> viene utilizzato per indicare il risultato di un'espressione. Ad esempio:
1 + 1; // -> 2// > significa il risultato di console.log o di un altro output. Ad esempio:
console.log("hello, world!"); // > hello, world!// è semplicemente un commento utilizzato per le spiegazioni. Esempio:
// Assegnare una funzione ad una costante
const foo = function() {};Array è uguale a not array:
[] == ![]; // -> trueL'opratore di abstract equality converte entrambi gli operandi prima di confrontarli, e diventano entrambi 0 per ragioni differenti. Gli Array sono truthy, quindi sulla destra, l'opposto di un valore truthy è false, che viene quindi forzato a diventare uno 0. Sul lato sinistro però l'array vuoto viene forzato a diventare un numero senza prima essere convertito in un valore booleano, e gli array vuoti vengono forzati a 0 a prescindere che siano truthy.
Qui possiamo vedere come viene semplificata l'espressione:
+[] == +![];
0 == +false;
0 == 0;
true;Vedi anche [] è truthy, ma non true.
Array è diverso da true, ma anche not Array è diverso da true;
Array è uguale a false, ma anche not Array è uguale a false:
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> truetrue == []; // -> false
true == ![]; // -> false
// Secondo le specifiche
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> falsefalse == []; // -> true
false == ![]; // -> true
// Secondo le specifiche
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> true
![]; // -> false
false == false; // -> true!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> trueConsidera questo, step-by-step:
// true è 'truthy' e rappresentato dal valore 1 (number), 'true' in formato stringa è NaN.
true == "true"; // -> false
false == "false"; // -> false
// 'false' non è la stringa vuota, quindi è un valore truthy
!!"false"; // -> true
!!"true"; // -> true"b" + "a" + +"a" + "a"; // -> 'baNaNa'Questo è un giochino old-school in JavaScript, rivisitato. L'originale è questo:
"foo" + +"bar"; // -> 'fooNaN'L'espressione viene valutata come 'foo' + (+'bar'), che converte 'bar' in "not a number".
NaN === NaN; // -> falseLe specifiche definiscono rigorosamente la logica dietro a questo comportamento:
- Se
Type(x)è diverso daType(y), return false.- Se
Type(x)è Number, allora
- Se
xè NaN, return false.- Se
yè NaN, return false.- … … …
Seguendo la definizione di NaN da quella dell'IEEE:
Sono possibili quattro relazioni mutuamente esclusive: less than, equal, greater than, e unordered. L'ultimo caso si presenta quando almeno un operando è NaN. Tutt i NaN se comparati risulteranno unordered, inclusa la comparazione con se stesso.
— “What is the rationale for all comparisons returning false for IEEE754 NaN values?” at StackOverflow
Non crederai ai tuoi occhi, ma...
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'Rompendo quell'ammasso di simboli in pezzettini, possiamo notare che il seguente pattern si ripete spesso:
![] + []; // -> 'false'
![]; // -> falseQuindi proviamo a sommare [] a false. Ma a causa di una serie di chiamate interne (binary + Operator -> ToPrimitive -> [[DefaultValue]]) otteniamo la conversione dell'operando a destra in una stringa:
![] + [].toString(); // 'false'Se pensiamo ad una stringa come un Array, possiamo accedere al suo primo elemento con [0]:
"false"[0]; // -> 'f'Il resto è ovvio, ma la i è complicata. La i in fail viene ottenuta generando la stringa 'falseundefined' e prendendo l'elemento all'indice ['10']
Un array è un valore truthy, ma non è uguale a true.
!![] // -> true
[] == true // -> falseEcco i link alle sezioni corrispondenti della specifica ECMA-262:
Nonostante il fatto che null sia un valore falsy, non è uguale a false.
!!null; // -> false
null == false; // -> falseAllo stesso modo, altri valori falsy, come 0 o '' sono uguali a false.
0 == false; // -> true
"" == false; // -> trueLa spiegazione è la stessa dell'esempio precedente. Ecco il link corrispondente:
⚠️ Questo fa parte delle Browser API e non funziona su Node.js⚠️
Nonostante il fatto che document.all sia un oggetto array-like e permette l'accesso al DOM della pagina, risponde alla funzione typeof con undefined.
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'Allo stesso modo, document.all è diverso da undefined.
document.all === undefined; // -> false
document.all === null; // -> falseMa contemporaneamente:
document.all == null; // -> true
document.allveniva utilizzato per accedere agli elementi del DOM, nelle vecchie versioni di IE. Nonostante non sia mai diventato uno standard, veniva ampiamente utilizzato in codice JS non proprio recentissimo. Quando vennero rilasciate le nuove APIs (comedocument.getElementById) questa API divenne obsoleta e il comitato dello standard dovette decidere cosa farne. A causa del suo uso spropositato l'API venne mantenuta ma venne introdotta una violazione intenzionale nelle speficiche di JavaScript. Il motivo per il quale risponde afalsequando si utilizza l'operatore di Strict Equality Comparison conundefined, mentretruequando si utilizza l'operatore di Abstract Equality Comparison è a causa della violazione intenzionale inserita nella specifica che la permette in modo esplicito.
— “Obsolete features - document.all” at WhatWG - HTML spec — “Chapter 4 - ToBoolean - Falsy values” at YDKJS - Types & Grammar
Number.MIN_VALUE è il numero più piccolo rappresentabile, che è maggiore di zero:
Number.MIN_VALUE > 0; // -> true
Number.MIN_VALUEè5e-324, ovvero il più piccolo numero positivo che può essere rappresentato con precisione float, cioè quello che si può ottenere il più vicino possibile allo zero. Definisce la migliore risoluzione che un tipo di dato float può fornire.Il numero più piccolo in assoluto è
Number.NEGATIVE_INFINITYnonostante non sia effettivamente un tipo numerico.— “Why is
0less thanNumber.MIN_VALUEin JavaScript?” at StackOverflow
⚠️ Un bug presente in V8 v5.5 o inferiore (Node.js <=7)⚠️
Tutti conoscerete la noiosa undefined is not a function, ma questa?
// Dichiara una classe che estende null
class Foo extends null {}
// -> [Function: Foo]
new Foo() instanceof null;
// > TypeError: function is not a function
// > at … … …Questo non è parte delle specifiche. È semplicemente un bug che ora è stato risolto, quindi non dovrebbero esserci problemi con questo in futuro.
E se provassimo a sommare due array?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'Viene svolta la concatenazione. il procedimento step-by-step è il seguente:
[1, 2, 3] +
[4, 5, 6][
// chiama toString()
(1, 2, 3)
].toString() +
[4, 5, 6].toString();
// concatenazione
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");Creiamo un array con 4 elementi vuoti. Nonostante ciò, si ottiene un array con 3 elementi, a causa delle "trailing commas":
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'Trailing commas (anche chiamate "final commas") sono utili quando si aggiungono nuovi elementi, parametri o proprietà in codice JavaScript. Se si vuole aggiungere una nuova proprietà si può semplicemente aggiungere una nuova riga senza modificare quella precedente, se quella linea presenta già una virgola alla fine. Questo rende i diffs dei sistemi di version-control più puliti e modificare il codice è leggermente meno problematico.
— Trailing commas at MDN
L'operatore di uguaglianza sugli array in JS è un mostro, come possiamo osservare sotto:
[] == '' // -> true
[] == 0 // -> true
[''] == '' // -> true
[0] == 0 // -> true
[0] == '' // -> false
[''] == 0 // -> true
[null] == '' // true
[null] == 0 // true
[undefined] == '' // true
[undefined] == 0 // true
[[]] == 0 // true
[[]] == '' // true
[[[[[[]]]]]] == '' // true
[[[[[[]]]]]] == 0 // true
[[[[[[ null ]]]]]] == 0 // true
[[[[[[ null ]]]]]] == '' // true
[[[[[[ undefined ]]]]]] == 0 // true
[[[[[[ undefined ]]]]]] == '' // trueGuarda attentamente gli esempi precedenti! Il comportamento viene spiegato nella sezione 7.2.13 Abstract Equality Comparison delle specifiche.
Se non passiamo argomenti al costruttore di Number, otteniamo 0. Il valore undefined viene assegnato di default quando non viene passato alcun valore, quindi possiamo aspettarci che Number senza parametri prenda undefined come valore del suo parametro. Invece quando inseriamo undefined, otteniamo NaN.
Number(); // -> 0
Number(undefined); // -> NaNIn base alle specifiche:
- Se non viene passato alcun parametro durante l'invocazione della funzione,
nviene valorizzato a+0. - Altrimenti,
nsarà il risultato diToNumber(value). - Nel caso di
undefined,ToNumber(undefined)deve restituireNaN.
Qui la sezione corrispondente:
parseInt è famoso per le sue stranezze:
parseInt("f*ck"); // -> NaN
parseInt("f*ck", 16); // -> 15💡 Spiegazione: Questo avviene perchè parseInt continuerà a svolgere il parsing carattere per carattere fino a che non trova un carattere che non riconosce. La f in 'f*ck' è la rappresentazione esadecimale di 15.
Svolgere il parsing di Infinity a integer è qualcosa di...
//
parseInt("Infinity", 10); // -> NaN
// ...
parseInt("Infinity", 18); // -> NaN...
parseInt("Infinity", 19); // -> 18
// ...
parseInt("Infinity", 23); // -> 18...
parseInt("Infinity", 24); // -> 151176378
// ...
parseInt("Infinity", 29); // -> 385849803
parseInt("Infinity", 30); // -> 13693557269
// ...
parseInt("Infinity", 34); // -> 28872273981
parseInt("Infinity", 35); // -> 1201203301724
parseInt("Infinity", 36); // -> 1461559270678...
parseInt("Infinity", 37); // -> NaNAttenzione anche quando si svolge il parsing di null:
parseInt(null, 24); // -> 23💡 Spiegazione:
Si sta convertendo
nullalla stringa"null"e provando poi a convertirla a sua volta. Per le radici da 0 a 23, non ci sono numerali per svolgere la conversione, quindi viene restituito NaN. A 24,"n", la 14-esima lettera, viene aggiunta al sistema di numerazione. A 31,"u", la 21-esima lettera, viene aggiunta e l'intera stringa può essere decodificata. A 37 non c'è più un valido insieme di numerazione che si può generare quindi viene restituitoNaN.— “parseInt(null, 24) === 23… wait, what?” at StackOverflow
Non dimentichiamoci del sistema di numerazione ottale:
parseInt("06"); // 6
parseInt("08"); // 8 se è presente il supporto a ECMAScript 5
parseInt("08"); // 0 se assente il supporto a ECMAScript 5💡 Spiegazione: Se la stringa in input inizia con "0", la radice è 8 (octal) o 10 (decimal). Quale radice viene scelta dipende dall'implementazione. ECMAScript 5 specifica l'utilizzo di 10 (decimal), Ma non è ancora supportata da tutti i browser. Per questo motivo è sempre meglio specificare una radice quando si utilizza parseInt.
parseInt converte sempre l'input in stringa:
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2
Number({ toString: () => 2, valueOf: () => 1 }); // -> 1Attenzione quando si svolge il parsin di valori in virgola mobile:
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5💡 Spiegazione: ParseInt prende una stringa come argomento e restituisce un intero in base alla radice specificata. ParseInt inoltre elimina tutto ciò che viene dopo e incluso il primo carattere non numerico nella stringa passata come parametro. 0.000001 Viene convertito nella stringa "0.000001" e parseInt restituisce 0. Quando 0.0000001 viene convertito in stringa viene interpretato come "1e-7" e quindi parseInt restituisce 1. 1/1999999 viene interpretato come 5.00000250000125e-7 e parseInt restituisce 5.
Facciamo un po' di calcoli:
true -
true +
// -> 2
(true + true) *
(true + true) -
true; // -> 3Hmmm... 🤔
Possiamo forzare dei valori a numeri utilizzando il costruttore di Number. È abbastanza ovvio che true venga forzato a 1:
Number(true); // -> 1L'operatore unario + prova a convertire il suo valore in un numero. Può convertire la rappresentazione testuale di interie e float, così come i valori non testuali true, false, e null. Se non riesce a svolgere il parsing di un particolare valore, restuituirà NaN. Questo significa che possiamo forzare facilmente true a 1:
+true; // -> 1Quando svolgiamo addizioni o moltiplicazioni, viene invocato il metodo ToNumber. In base alla specifica questo metodo restituisce:
Se
parametroè true, restituisci 1. Separametroè false, restituisci +0.
È questo il motivo per il quale possiamo sommare valori booleani e ottenere risultati corretti.
Sezioni corrispondenti:
Non ci crederai, ma <!-- (ovvero un commento in HTML) è un commento valido in JavaScript.
// commento valido
<!-- anche questoStupito? Commenti HTML-like sono stati pensati per permettere ai browser che non capivano il tag <script> di degradare in modo soft. Questi browser, ad esempio Netscape 1.x non sono più diffusi. Quindi non c'è proprio più alcun motivo per inserire commenti HTML nei tag script
Dato che Node.js è basato sull'engine V8, i commenti HTML-like sono supportati anche dal runtime di the Node.js. Inoltre sono parte delle specifiche:
Il tipo di NaN è 'number':
typeof NaN; // -> 'number'Spiegazione di come funzionano gli operatori typeof e instanceof:
typeof []; // -> 'object'
typeof null; // -> 'object'
// però
null instanceof Object; // falseIl comportamento dell'operatore typeof è definito nella seguente sezione delle specifiche:
Secondo le specifiche, l'operatore typeof restituisce una stringa in base alla Table 35: typeof Operator Results. Per null, gli oggetti ordinari, esotici standard e non standard che non implementano [[Call]], restituisce la stringa "object".
Comunque si può anche controllare il tipo di un oggetto utilizzando il metodo toString.
Object.prototype.toString.call([]);
// -> '[object Array]'
Object.prototype.toString.call(new Date());
// -> '[object Date]'
Object.prototype.toString.call(null);
// -> '[object Null]'999999999999999; // -> 999999999999999
9999999999999999; // -> 10000000000000000
10000000000000000; // -> 10000000000000000
10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002Questo è causato dallo standard IEEE 754-2008 per l'aritmetica binaria dei numeri in virgola mobile. A questa grandezze numeriche, arrotonda al numero pari più vicino. Leggi di più qui:
- 6.1.6 The Number Type
- IEEE 754 on Wikipedia
Un giochino ben noto. La somma di 0.1 e 0.2 è completamente sbagliata:
0.1 +
0.2(
// -> 0.30000000000000004
0.1 + 0.2
) ===
0.3; // -> falseLa risposta alla domanda ”La matematica in virgola mobile è completamente rotta? ” su StackOverflow:
Le costanti
0.2e0.3nel programma saranno approssimazioni del loro vero valore. Il valoredoublepiù vicino a0.2è più grande del numero razionale0.2ma ildoublepiù vicino a0.3è più piccolo del numero razionale0.3. La somma di0.1e0.2risulta essere più grande del numero razionale0.3e quindi risultando diverso dalla costante presente nel codice.
Questo problema è talmente noto che esiste anche il sito web 0.30000000000000004.com. Capita in tutti i linguaggi di programmazione che svolgono calcoli in virgola mobile, non solo JavaScript.
Possiamo aggiungere metodi nostri agli oggetti wrapper come Number o String.
Number.prototype.isOne = function() {
return Number(this) === 1;
};
(1.0).isOne(); // -> true
(1).isOne(); // -> true
(2.0)
.isOne()(
// -> false
7
)
.isOne(); // -> falseOvviamente possiamo estendere l'oggetto Number così come ogni altro oggetto in JavaScript. Non è comunque una pratica consigliata se il metodo definito non è parte delle specifiche. Ecco la lista delle proprietà dell'oggetto Number:
1 < 2 < 3; // -> true
3 > 2 > 1; // -> falsePerchè funziona in questo modo? Beh, il problema è nella prima parte dell'espressione. Ecco come funziona:
1 < 2 < 3; // 1 < 2 -> true
true < 3; // true -> 1
1 < 3; // -> true
3 > 2 > 1; // 3 > 2 -> true
true > 1; // true -> 1
1 > 1; // -> falsePossiamo correggerlo con l'operatore Greater than or equal (>=):
3 > 2 >= 1; // trueLeggi più a riguardo degli operatori relazionali nelle specifiche:
Spesso il risultato delle operazioni aritmetiche in JavaScript risulta essere abbastanza strano. Consideriamo questi esempi:
3 - 1 // -> 2
3 + 1 // -> 4
'3' - 1 // -> 2
'3' + 1 // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'
'222' - -'111' // -> 333
[4] * [4] // -> 16
[] * [] // -> 0
[4, 4] * [4, 4] // NaNCosa succede nei primi quattro esempi? Ecco una piccola tabella per comprendere la somma in JavaScript:
Number + Number -> addition
Boolean + Number -> addition
Boolean + Boolean -> addition
Number + String -> concatenation
String + Boolean -> concatenation
String + String -> concatenation
E per quanto riguarda gli altri esempi? I metodi ToPrimitive e ToString vengono chiamati implicitamente per [] e {} prima della somma. Leggi di più riguardo a questo processo nelle specifiche:
- 12.8.3 The Addition Operator (
+) - 7.1.1 ToPrimitive(
input[,PreferredType]) - 7.1.12 ToString(
argument)
In particolare l'eccezione è in {} + []. Il motivo per cui differisce da [] + {} è che, senza parentesi, viene interpretato come un blocco di codice seguito dall'operatore unario +, convertendo [] in un numero. Come viene spiegato di seguito:
{
// qui un blocco di codice
}
+[]; // -> 0Per ottenere lo stesso risultato di [] + {} possiamo racchiuderlo tra parentesi.
({} + []); // -> [object Object]Sapevi che si possono sommare numero in questo modo?
// Patch a toString method
RegExp.prototype.toString =
function() {
return this.source;
} /
7 /
-/5/; // -> 2"str"; // -> 'str'
typeof "str"; // -> 'string'
"str" instanceof String; // -> falseIl costruttore di String restituisce una stringa:
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> trueProviamo con new:
new String("str") == "str"; // -> true
typeof new String("str"); // -> 'object'Object? eh?
new String("str"); // -> [String: 'str']Più informazioni sul costruttore di String nelle specifiche:
Dichiariamo una funzione che logga tutti i parametri nella console:
function f(...args) {
return args;
}Senza dubbio, saprai che possiamo richiamarla nel modo seguente:
f(1, 2, 3); // -> [ 1, 2, 3 ]Ma sapevi di poter chiamare qualsiasi funzione con le backticks?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`;
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]Beh, questa non è per niente magia se hai familiarità con i Tagged template literals. Nell'esempio precedente, la funzione f f è un tag per i template literal. I tag prima dei template literals permettono di svolgere il parsing dei template con una funzione. Il primo parametro di una "funzione tag" contiene un array di stringhe. I parametri restanti sono relativi alle espressioni. Ad esempio:
function template(strings, ...keys) {
// fai qualcosa con strings and keys…
}Questa è la magia dietro famosa libreria chiamata 💅 styled-components, molto popolare tra la community di React.
Link alle specifiche:
Trovato da @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2]);Attenzione, ti può mettere in crisi il cervello! Prova ad eseguire questo codice a mente: stiamo applicando il metodo call usando il metodo apply.
Leggi di più:
- 19.2.3.3 Function.prototype.call(
thisArg, ...args) - **19.2.3.1 ** Function.prototype.apply(
thisArg,argArray)
const c = "constructor";
c[c][c]('console.log("WTF?")')(); // > WTF?Consideriamo questo esempio passo passo:
// Dichiariamo una costante che è la stringa 'constructor'
const c = "constructor";
// c è una stringa
c; // -> 'constructor'
// Otteniamo il costruttore di string
c[c]; // -> [Function: String]
// Otteniamo il costruttore di constructor
c[c][c]; // -> [Function: Function]
// chiamiamo la funzione costruttore e gli passiamo
// il corpo di una nuova funzione come parametro
c[c][c]('console.log("WTF?")'); // -> [Function: anonymous]
// Chiamiamo la funzione anonima risultante
// Il risultato è loggare sulla console la stringa 'WTF?'
c[c][c]('console.log("WTF?")')(); // > WTF?Object.prototype.constructor restituisce un riferimento al costruttore di Object che ha creato l'oggetto. Con le stringhe è String, nel caso dei numeri è Number e così via.
{ [{}]: {} } // -> { '[object Object]': {} }Perchè funziona così? Qui stiamo utilizzando le Computed property name. Quando passiamo un oggetto tra parentesi quadre, forza la conversione di quell'oggetto a stringa, quindi otteniamo la proprietà '[object Object]' e il valore {}.
Possiamo realizzare un "brackets hell" in questo modo:
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}
// structure:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }Leggi di più a riguardo degli object literals qui:
Come sappiamo, i tipi primitivi non hanno prototipi. Però, se proviamo ad ottenere il valore di __proto__ per i tipi primitivi, otteniamo questo:
(1).__proto__.__proto__.__proto__; // -> nullQuesto accade perchè quando qualcosa non ha un prototype, verrà inserito in un oggetto wrapper con un metodo ToObject. Quindi, passo passo:
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__; // -> nullQui più informazioni riguardo a __proto__:
Quale è il risultato dell'espressione qui sotto?
`${{ Object }}`;La risposta è:
// -> '[object Object]'Abbiamo definito un oggetto con una proprietà Object usando la Shorthand property notation:
{
Object: Object;
}Quindi abbiamo passato questo oggetto al template literal, seguirà la chiamata al metodo toString per quell'oggetto. Ecco perchè otteniamo la stringa '[object Object]'.
Considera l'esempio seguente:
let x,
{ x: y = 1 } = { x };
y;L'esempio precedente è un ottima domanda per un colloquio di lavoro. Quale è il valore di y? La risposta è:
// -> 1let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4Con l'esempio precedente:
- Dichiariamo
xsenza alcun valore, quindi risultaundefined. - Quindi inseriamo il valore di
xall'interno della proprietàxdell'oggetto. - Quindi estraiamo il valore di
xusando il destructuring e lo assegniamo ay. Se il valore non è definito, allora utilizziamo1come valore di default. - Restituiamo il valore di
y.
- Object initializer su MDN
Si possono realizzare esempi interessanti utilizzando l'operatore di spreading e gli array. Considera questo:
[...[..."..."]].length; // -> 3Perchè 3? Quando utilizziamo l'operatore di spread, viene chiamato il metodo @@iterator, e l'iteratore che viene restituito viene utilizzato per ottenere i valori sui quali iterare. L'iteratore di default per le stringhe separa la stringa in caratteri. Dopo lo spreading, vengono inseriti questi valori in un array. Quindi viene svolto nuovamente lo spread sull'array e il risultato viene nuovamente inserito al suo interno.
La stringa '...' è composta da tre caratteri ., quindi la dimensione dell'array risultante è 3.
Ora, passo passo:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3Chiaramente, possiamo svolgere questo procedimento di spread e wrap quante volte vogliamo:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// and so on …Sono in pochi i programmatori che sono a conoscenza delle Labels in JavaScript. Sono abbastanza interessanti:
foo: {
console.log("first");
break foo;
console.log("second");
}
// > first
// -> undefinedL'istruzione etichettata viene utilizzata con le istruzioni di break o continue. Possiamo usare un'etichetta per identificare costrutto iterativo, e usare le istruzioni break o continue per indicare se il programma deve interrompere l'iterazione o continuarla.
Nell'esempio precedente, identifichiamo l'etichetta foo. Dopo che console.log('first'); viene eseguita l'esecuzione viene fermata.
Approfondisci le etichette in JavaScript:
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5Simile agli esempi precedenti, clicca sui seguenti link:
Cosa restituisce questa espressione? 2 o 3?
(() => {
try {
return 2;
} finally {
return 3;
}
})();La risposta è 3. Sorpreso?
Dai uno sguardo all'esempio sottostante:
new class F extends (String, Array) {}(); // -> F []Si tratta di ereditarietà multipla? Negativo.
La parte interessante è il valore della clausola ((String, Array)) di extends. L'operatore di grouping restituisce sempre il suo ultimo parametro, quindi (String, Array) è semplicemente Array. Questo significa che abbiamo creato una classe che estende Array.
Guarda questo esempio di generator che produce se stesso:
(function* f() {
yield f;
})().next();
// -> { value: [GeneratorFunction: f], done: false }Come possiamo notare, il valore restituito è un oggetto con value uguale a f. In quel caso, possiamo fare una cosa del genere:
(function* f() {
yield f;
})()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()
.value()
.next();
// -> { value: [GeneratorFunction: f], done: false }
// and così via
// …Per capirne il suo funzionamento, leggi queste sezioni delle specifiche:
Considera questa sintassi offuscata in gioco:
typeof new class {
class() {}
}(); // -> 'object'Sembra la dichiarazione di una classe all'interno di un'altra classe. Dovrebbe essere un errore, invece otteniamo 'object'.
Da ECMAScript 5, possiamo usare le keywords come property names. Quindi immaginalo come nel seguente esempio:
const foo = {
class: function() {}
};ES6 ha standardizzato la definizione compatta per i metodi. Inoltre, le classi possono essere anonime. Quindi se togliamo la parte con : function, otteniamo:
class {
class() {}
}Il risultato di una default class è sempre un oggetto semplice. E il tuo typeof dovrebbe restituire 'object'.
Leggi di più qui:
Con i ben noti, esiste un modo per evitare la type-coercion. Guarda un po':
function nonCoercible(val) {
if (val == null) {
throw TypeError("nonCoercible should not be called with null or undefined");
}
const res = Object(val);
res[Symbol.toPrimitive] = () => {
throw TypeError("Trying to coerce non-coercible object");
};
return res;
}Adesso possiamo utilizzarla in questo modo:
// objects
const foo = nonCoercible({ foo: "foo" });
foo * 10; // -> TypeError: Trying to coerce non-coercible object
foo + "evil"; // -> TypeError: Trying to coerce non-coercible object
// strings
const bar = nonCoercible("bar");
bar + "1"; // -> TypeError: Trying to coerce non-coercible object
bar.toString() + 1; // -> bar1
bar === "bar"; // -> false
bar.toString() === "bar"; // -> true
bar == "bar"; // -> TypeError: Trying to coerce non-coercible object
// numbers
const baz = nonCoercible(1);
baz == 1; // -> TypeError: Trying to coerce non-coercible object
baz === 1; // -> false
baz.valueOf() === 1; // -> trueConsidera l'esempio sottostante:
let f = () => 10;
f(); // -> 10Okay, va bene, ma guarda questo:
let f = () => {};
f(); // -> undefinedPotresti aspettarti {} anzichè undefined. Questo è perchè le parentesi graffe fanno parte della sintassi per le arrow functions, quindi f restituirà undefined. È comunque possibile restituire l'oggetto {} direttamente da una arrow function, racchiudendo il valore di ritorno tra parentesi.
let f = () => ({});
f(); // -> {}Considera l'esempio sottostante:
let f = function() {
this.a = 1;
};
new f(); // -> f { 'a': 1 }Ora, prova a fare la stessa cosa con una arrow function:
let f = () => {
this.a = 1;
};
new f(); // -> TypeError: f is not a constructorLe arrow function non possono essere utilizzate come costruttore e lanceranno un errore se usate con new. Dato che hanno un this lessicale, e non hanno la proprietà prototype, non avrebbe molto senso.
Considera l'esempio sottostante:
let f = function() {
return arguments;
};
f("a"); // -> { '0': 'a' }Ora, prova a fare la stessa cosa con una arrow function:
let f = () => arguments;
f("a"); // -> Uncaught ReferenceError: arguments is not definedLe arrow functions sono una versione alleggerita delle funzioni tradizionali con un focus sull'essere concise e con un this lessicale. Allo stesso tempo le arrow function non forniscono un binding per l'oggetto arguments. Un'alternativa valida per ottenere lo stesso risultato è utilizzare i rest parameters:
let f = (...args) => args;
f("a");- Arrow functions at MDN.
anche l'istruzione return può essere complicata. Considera questo:
(function() {
return
{
b: 10;
}
})(); // -> undefinedreturn e l'espressione da restituire devono essere sulla stessa linea:
(function() {
return {
b: 10
};
})(); // -> { b: 10 }Questo a causa di un concetto chiamato Automatic Semicolon Insertion, che inserisce automagicamente punti e virgola dopo la maggior parte degli a capo. Nel primo esempio, c'è un punto e virgola inserito tra l'istruzione return e l'oggetto, quindi la funzione restituisce undefined e l'oggetto non viene mai valutato.
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
foo.x; // -> undefined
foo; // -> {n: 2}
bar; // -> {n: 1, x: {n: 2}}Da destra a sinistra, {n: 2} viene assegnato a foo, e il risultato di questo assegnamento {n: 2} viene assegnato a foo.x, ecco perchè bar è {n: 1, x: {n: 2}} in quanto bar è un riferimento a foo. Ma perchè foo.x è undefined mentre bar.x non lo è?
Foo e bar referenziano lo stesso oggetto {n: 1}, e gli lvalues vengono risolti prima dell'assegnamento. foo = {n: 2} sta creando un nuovo oggetto, quindi foo viene aggiornato per referenziare il nuovo oggetto. Il trick qui è in foo.x = ... in quanto il lvalue è stato risolto precedentemente e referenzia ancora il vecchio oggetto foo = {n: 1} e lo aggiorna inserendo il valore x. Dopo questa catena di assegnamenti, bar continua a referenziare il vecchio oggetto foo, ma foo referenzia il nuovo oggetto {n: 2}, dove x non esiste.
È equivalente a:
var foo = { n: 1 };
var bar = foo;
foo = { n: 2 }; // -> {n: 2}
bar.x = foo; // -> {n: 1, x: {n: 2}}
// bar.x point to the address of the new foo object
// it's not equivalent to: bar.x = {n: 2}var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1E per quanto concerne gli array pseudo-multidimensionali?
var map = {};
var x = 1;
var y = 2;
var z = 3;
map[[x, y, z]] = true;
map[[x + 10, y, z]] = true;
map["1,2,3"]; // -> true
map["11,2,3"]; // -> trueL'operatore parentesi quadre [] converte l'espressione usando il metodo toString. Convertire un array di un solo elemento in una stringa è come convertire l'elemento contenuto nell'array in stringa.
["property"].toString(); // -> 'property'null > 0; // false
null == 0; // false
null >= 0; // truePer farla breve, se null che è minore di 0 è false, allora null >= 0 è true. Leggi la spiegazione approfondita per questo qui.
Number.toFixed() può comportarsi in modo bizzarro in certi browser. Guarda l'esempio seguente:
(0.7875).toFixed(3);
// Firefox: -> 0.787
// Chrome: -> 0.787
// IE11: -> 0.788
(0.7876).toFixed(3);
// Firefox: -> 0.788
// Chrome: -> 0.788
// IE11: -> 0.788L'istinto potrebbe farci pensare che IE11 sia corretto e Firefox/Chrome sbaglino, la realtà è che Firefox/Chrome stanno rispettando gli standard per i numeri in virgola mobile (IEEE-754 Floating Point), mentre IE11 sta evitando di rispettarli (quello che probabilmente è) uno sforzo per restituire dei risultati più chiari.
Possiamo vedere perchè questo accade con un semplice test:
// Confermare lo strano risultato dell'arrotondamento per difetto di 5
(0.7875).toFixed(3); // -> 0.787
// Sembra essere 5 quando si estende il
// limite a 64-bit (double-precision) di precisione
(0.7875).toFixed(14); // -> 0.78750000000000
// Ma se si supera il limite?
(0.7875).toFixed(20); // -> 0.78749999999999997780I numeri floating point non sono memorizzati come una sequenza di cifre decimali, ma attraverso un metodo più elaborato che produce delle piccole inacuratezze the solitamente vengono eliminate dalle chiamate a toString o simili, ma queste imprecisioni rimangono comunque presenti internamente.
In questo caso, il "5" alla fine era un numero infinitesimamente più piccolo del vero 5. Arrotondandolo ad una precisione ragionevole verrà mostrato come 5... ma internamente non è un 5.
IE11, invece, mostrerà il valore dato in input con degli zeri in coda, anche nel caso di toFixed(20), in quanto sembra forzare l'arrotondamento del valore per evitare problematiche causate dai limiti hardware.
Guarda il riferimento a NOTE 2 sulla definizione per toFixed nelle specifiche ECMA-262.
Math.min(1, 4, 7, 2); // -> 1
Math.max(1, 4, 7, 2); // -> 7
Math.min(); // -> Infinity
Math.max(); // -> -Infinity
Math.min() > Math.max(); // -> true- Perchè Math.max() è più piccolo di Math.min()? by Charlie Harvey
La seguente espressione sembra introdurre una contraddizione:
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> trueCome può null non essere uguale a, o maggiore di 0, se null >= 0 è effettivamente true? (Funziona anche con "inferiore a" nello stesso modo.)
Il modo in cui queste tre espressioni vengono valutate sono tutti diversi ed è per questo che viene prodotto questo comportamento un po' inaspettato.
Per prima cosa analizziamo il comportamento dell'operatore di abstract equality comparison, null == 0.
Solitamente, se l'operatore non riesce a confrontare i suoi operanti in modo opportuno, li converte in numeri e compara questi ultimo. Quindi ci si può aspettare il seguente comportamento:
// This is not what happens
(null == 0 + null) == +0;
0 == 0;
true;Invece, secondo una lettura attenta delle specifiche, la conversione a numero non avviene per l'operando che ha valore null o undefined. Quindi, se abbiamo null da un lato del simbolo uguale, l'altro lato deve essere null o undefined per fare in modo che venga restituito true. Dato che non è questo il caso, verrà restituito false.
Ora analizziamo l'operatore di comparazione null > 0. Qui l'algoritmo, a differenza dell'operatore di abstract equality, convertirà null in un numero. Quindi il comportamento sarà il seguente:
null > 0
+null = +0
0 > 0
falseInfine, analizziamo l'operatore relazionale null >= 0. Si può obiettare che questa espressione dovrebbe essere il risultato di null > 0 || null == 0; se fosse così, allora il risultato dell'espressione dovrebbe essere false. Invece l'operatore >= funziona in un modo completamente diverso, dove praticamente prende l'opposto dell'operatore <. Dato che l'esempio con l'operatore "maggiore di" produce lo stesso valore dell'operatore "minore di", l'espressione verrà valutata nel modo seguente:
null >= 0;
!(null < 0);
!(+null < +0);
!(0 < 0);
!false;
true;JS permette la ridichiarazione di variabili:
a;
a;
// È valida anche questa
a, a;Funziona anche in modalità strict:
var a, a, a;
var a;
var a;Tutte le definizione sono state unite in una sola.
Supponiamo di voler ordinare un array di numeri.
[ 10, 1, 3 ].sort() // -> [ 1, 10, 3 ]
L'ordinamento di default viene realizzato convertendo gli elementi in stringhe, quindi confrontando i loro valore in UTF-16.
Passa una comparefn se vuoi ordinare qualcosa che non è una stringa.
[ 10, 1, 3 ].sort((a, b) => a - b) // -> [ 1, 3, 10 ]
const theObject = {
a: 7
};
const thePromise = new Promise((resolve, reject) => {
resolve(theObject);
}); // -> Instance object di Promise
thePromise.then(value => {
console.log(value === theObject); // -> true
console.log(value); // -> { a: 7 }
});Il value che viene risolto da thePromise è esattamente theObject.
E se inserissimo un'altra Promise all'interno della funzione resolve?
const theObject = new Promise((resolve, reject) => {
resolve(7);
}); // -> Promise instance object
const thePromise = new Promise((resolve, reject) => {
resolve(theObject);
}); // -> Promise instance object
thePromise.then(value => {
console.log(value === theObject); // -> false
console.log(value); // -> 7
});Questa funzione appiattisce livelli annidati di oggetti promise-like (ad esempio una promise che risolve a una promise che risolve a qualcosa) in un singolo livello.
La specifica è ECMAScript 25.6.1.3.2 Promise Resolve Functions. But it is not quite human-friendly.
- wtfjs.com — una raccolta di irregolarità e stranezze davvero speciali con un pizzico di momenti dolorosamente controintuitivi per il linguaggio del web.
- Wat — A lightning talk by Gary Bernhardt from CodeMash 2012
- What the... JavaScript? — Il talk di Kyle Simpsons alla Forward 2 che prova a "estrarre le stramberie” da JavaScript. Il suo desiderio è aiutare a scrivere un codice più pulito, elegante e leggibile, ispirare le persone a contribuire alla community open source.
