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
NaN
non èNaN
- È un fail
[]
è truthy, ma nontrue
null
è falsy, ma nonfalse
document.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
undefined
eNumber
parseInt
è bast**do- Math con
true
efalse
- I commenti HTML sono validi anche in JavaScript
NaN
ènota number[]
enull
sono 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..catch
insidioso - 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
arguments
e 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
null
con0
- 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:
[] == ![]; // -> true
L'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 == ![]; // -> true
true == []; // -> false
true == ![]; // -> false
// Secondo le specifiche
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> 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"; // -> true
Considera 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; // -> false
Le 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'
![]; // -> false
Quindi 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 // -> false
Ecco 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; // -> false
Allo stesso modo, altri valori falsy, come 0
o ''
sono uguali a false
.
0 == false; // -> true
"" == false; // -> true
La 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; // -> false
Ma contemporaneamente:
document.all == null; // -> true
document.all
veniva 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 afalse
quando si utilizza l'operatore di Strict Equality Comparison conundefined
, mentretrue
quando 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_INFINITY
nonostante non sia effettivamente un tipo numerico.— “Why is
0
less thanNumber.MIN_VALUE
in 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 ]]]]]] == '' // true
Guarda 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); // -> NaN
In base alle specifiche:
- Se non viene passato alcun parametro durante l'invocazione della funzione,
n
viene valorizzato a+0
. - Altrimenti,
n
sarà 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); // -> NaN
Attenzione anche quando si svolge il parsing di null
:
parseInt(null, 24); // -> 23
💡 Spiegazione:
Si sta convertendo
null
alla 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 }); // -> 1
Attenzione 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; // -> 3
Hmmm... 🤔
Possiamo forzare dei valori a numeri utilizzando il costruttore di Number
. È abbastanza ovvio che true
venga forzato a 1
:
Number(true); // -> 1
L'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; // -> 1
Quando 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 questo
Stupito? 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; // false
Il 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; // -> 10000000000000002
Questo è 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; // -> false
La risposta alla domanda ”La matematica in virgola mobile è completamente rotta? ” su StackOverflow:
Le costanti
0.2
e0.3
nel programma saranno approssimazioni del loro vero valore. Il valoredouble
più vicino a0.2
è più grande del numero razionale0.2
ma ildouble
più vicino a0.3
è più piccolo del numero razionale0.3
. La somma di0.1
e0.2
risulta essere più grande del numero razionale0.3
e 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(); // -> false
Ovviamente 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; // -> false
Perchè 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; // -> false
Possiamo correggerlo con l'operatore Greater than or equal (>=
):
3 > 2 >= 1; // true
Leggi 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] // NaN
Cosa 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
}
+[]; // -> 0
Per 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; // -> false
Il costruttore di String
restituisce una stringa:
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> true
Proviamo 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__; // -> null
Questo 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__; // -> null
Qui 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 è:
// -> 1
let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
Con l'esempio precedente:
- Dichiariamo
x
senza alcun valore, quindi risultaundefined
. - Quindi inseriamo il valore di
x
all'interno della proprietàx
dell'oggetto. - Quindi estraiamo il valore di
x
usando il destructuring e lo assegniamo ay
. Se il valore non è definito, allora utilizziamo1
come 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; // -> 3
Perchè 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 // -> 3
Chiaramente, 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
// -> undefined
L'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; // -> 5
Simile 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; // -> true
Considera l'esempio sottostante:
let f = () => 10;
f(); // -> 10
Okay, va bene, ma guarda questo:
let f = () => {};
f(); // -> undefined
Potresti 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 constructor
Le 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 defined
Le 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;
}
})(); // -> undefined
return
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]; // -> 1
E 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"]; // -> true
L'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; // true
Per 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.788
L'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.78749999999999997780
I 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; // -> true
Come 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
false
Infine, 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.