Uma lista de exemplos engraçados e truques com JavaScript
JavaScript é uma excelente linguagem. Ela tem uma sintaxe simples, um ecossistema grande e, o mais importante, uma grande comunidade.
Ao mesmo tempo, todos nós sabemos que o JavaScript é uma linguagem engraçada com várias partes complicadas. Algumas delas porem rapidamente transformar seu trabalho em um inferno, e outras podem nos fazer gargalhar.
A ideia original para o WTFJS é do Brian Leroux. Essa lista é inspirada por sua talk “WTFJS” no dotJS 2012:
Você pode instalar esse manual usando o npm
. É só rodar o comando:
$ npm install -g wtfjs
Você poderá rodar wtfjs
na sua linha de comando. Esse comando vai abrir o manual na sua $PAGER
selecionada ou você pode continuar lendo aqui mesmo.
O código-fonte está disponível aqui https://github.com/denysdovhan/wtfjs.
Atualmente, temos essas traduções disponíveis de wtfjs:
- 💪🏻 Motivação
- ✍🏻 Notação
- 👀 Exemplos
[]
é igual a![]
true
não é igual a![]
, nem igual a[]
também- true é false
- baNaNa
NaN
não é umNaN
- É uma falha
[]
é verdadeiro, mas nãotrue
null
é falso, mas nãofalse
document.all
é um objeto (object), mas é indefinido (undefined)- Valor mínimo é maior que zero
- function não é uma function
- Somando arrays
- Vírgulas finais em arrays
- Igualdade entre arrays é um monstro
undefined
eNumber
parseInt
é um vilão- Matemática com
true
efalse
- Comentários HTML são válidos no JavaScript
NaN
nãoé um número[]
enull
são objetos- Aumentando números magicamente
- Precisão de
0.1 + 0.2
- Patching numbers
- Comparação de três números
- Matemática engraçada
- Soma de RegExps
- Strings não são instâncias
String
- Chamando funções com backticks
- Call call call
- Uma propriedade
constructor
- Objeto como uma chave de uma propriedade de objeto
- Acessando protótipos com
__proto__
`${{Object}}`
- Desestruturação com valores padrão
- Pontos e dispersão
- Rótulos
- Rótulos aninhados
try..catch
traidor- Isto é herança múltipla?
- Um gerador que produz a si mesmo
- Uma classe de classe
- Objetos não coercíveis
- Arrow functions traiçoeiras
- Arrow functions não podem ser construtores
arguments
e arrow functions- Retorno traiçoeiro
- Encadeamento atribuições em um objeto
- Acessando propriedades de objetos usando arrays
- Null e Operadores Relacionais
Number.toFixed()
mostra números diferentesMath.max()
menor queMath.min()
- Comparando
null
com0
- Redeclaração da mesma variável Same variable
- Comportamento padrão Array.prototype.sort()
- 📚 Outros recursos
- 🎓 Licença
Just for fun
— “Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds
O objetivo dessa lista era coletar alguns exemplos malucos e explicar como eles funcionam, se possível. Apenas porque é legal aprender algo que nós não conhecemos.
Se você é um iniciante, você poderá utilizar esses pontos para se aprofundar no JavaScript. Eu espero que esses pontos te motivem em gastar um pouco mais de tempo lendo as especificações.
Se você já é um desenvolvedor profissional, você pode considerar esses exemplos como uma excelente referência para todos as peculiaridades e pontos inesperados do nosso amado JavaScript.
Em todo caso, leia. Você provavelmemte irá aprender algo novo.
// ->
é utilizado para mostrar o resultado de uma expressão. Por exemplo:
1 + 1; // -> 2
// >
significa o resultado de console.log
ou qualquer outra saída. Por exemplo:
console.log("hello, world!"); // -> hello, world!
//
são apenas comentários para as explicações. Exemplo:
// Atribuindo uma função para a constante foo
const foo = function() {};
Array é igual a not array:
[] == ![]; // -> true
O operador abstrato de igualdade converte os dois lados em números para compará-los, e os dois lados se tornam 0
por razões diferentes. Arrays são verdadeiros (truthy), então na direita, o oposto de um valor verdadeiro é false
, o que é coagido para 0
. Na esquerda, todavia, um array vazio é coagido para um número sem se tornar um booleano (boolean) primeiro, e arrays vazios sempre forçados para 0
, apesar de serem verdadeiros.
Aqui está uma simplificação dessa expressão:
+[] == +![];
0 == +false;
0 == 0;
true;
Veja também []
is truthy, but not true
.
Array não é igual a true
, mas not Array também não é igual a true
'
Array é igual a false
, not Array é igual a false
também:
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
true == []; // -> false
true == ![]; // -> false
// De acordo com a especificação
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> true
false == ![]; // -> true
// De acordo com a especificação
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> true
![]; // -> false
false == false; // -> true
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
Considere esse passo-a-passo:
// true é 'truthy' e representado pelo valor 1 (number), 'true' como string é NaN.
true == "true"; // -> false
false == "false"; // -> false
// 'false' não é uma string vazia, então ele é um valor verdadeiro (truthy)
!!"false"; // -> true
!!"true"; // -> true
"b" + "a" + +"a" + "a"; // -> 'baNaNa'
Essa é uma piada antiga no JavaScript, mas remasterizada. Aqui está a forma original:
"foo" + +"bar"; // -> 'fooNaN'
A expressão é avaliada como 'foo' + (+'bar')
, o que converte bar
para um "não número" (NaN - Not a Number).
NaN === NaN; // -> false
A especificação define estritamente a lógica por trás desse comportamento:
- Se
Type(x)
é diferente deType(y)
, retorne false.- Se
Type(x)
é um Number, então
- Se
x
é um NaN, retorne false.- Se
y
é um NaN, retorne false.- … … …
Seguindo a definição de NaN
do IEEE:
Quatro relações de exclusões mútuas são possíveis: menor que (less than), igual (equal), maior que (greater than), e não ordenado (unordered). O último caso surge quando, pelo menos, um operador é um NaN. Todo NaN deve comprarar não ordenado (unordered) com tudo, incluindo a si mesmo.
— “What is the rationale for all comparisons returning false for IEEE754 NaN values?” no StackOverflow
Você não vai acreditar, mas ...
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'
Quando nós quebramos esses símbolos em pedaços, percebemos que o esse padrão se repete com frequência:
![] + []; // -> 'false'
![]; // -> false
Então nós tentamos adicionar []
para false
. Mas devido a um número interno de chamadas de função (binary + Operator
-> ToPrimitive
-> [[DefaultValue]]
) nós acabamos convertendo o operador da direita para uma string:
![] + [].toString(); // 'false'
Pensando em uma string como um array nós conseguimos acessar seu primeiro caractere usando [0]
:
"false"[0]; // -> 'f'
O resto é óbvio, mas o i
é ardiloso. O i
em fail
é pego através da geração da string 'falseundefined'
e pegando o element no índice ['10']
Um array é um valor verdadeiro (truthy), porém, não é igual a true
.
!![] // -> true
[] == true // -> false
Aqui estão links das seções correspondentes especificação do ECMA-262:
Apesar do fato que null
é um valor falso (falsy), ele não é igual a false
.
!!null; // -> false
null == false; // -> false
Ao mesmo tempo, outro valor falso (falsy), como 0
ou ''
são iguais a false
.
0 == false; // -> true
"" == false; // -> true
A explicação é a mesma dos exemplos anteriores. Aqui está o link correspondente:
⚠️ Esta é a parte da API Browser e não irá funcionar em um ambiente com Node.js - apenas em navegadores⚠️
Apesar de document.allser um objeto parecido com um array, ele dá acesso aos nós do DOM na página, e responde como
undefinedna função
typeof`.
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'
Ao mesmo tempo, document.all
não é igual a undefined
.
document.all === undefined; // -> false
document.all === null; // -> false
Mas ao mesmo tempo:
document.all == null; // -> true
document.all
é usado como uma maneira de acessar todos os elementos do DOM, em particular com versões legadas do IE. Mesmo nunca tendo se tornado um padrão, foi amplamente usado nas eras antigas do JS. Quando o padrão progrediu com novas APIs (comodocument.getElementById
) essa API (document.all) se tornou obsoleta e o comitê padrão teve que decidir o que fazer com ela. Por conta do amplo uso eles decidiram deixar a API mas introduziram uma violação intencional da especificação do JavaScript. A razão que ele retorna comofalse
quando usamos o Comparador Estrito de Igualdade comundefined
etrue
quando usamos o Comparador Abstrado de Igualdade é devido a essa violação intencional que explicitamente permite isso.— “Obsolete features - document.all” em WhatWG - HTML spec
— “Chapter 4 - ToBoolean - Falsy values” em YDKJS - Types & Grammar
Number.MIN_VALUE
é o menor número, que ainda é maior que zero:
Number.MIN_VALUE > 0; // -> true
Number.MIN_VALUE
é igual a5e-324
, ou seja, o menor número positivo que pode ser representado com precisão float; ou seja, o mais próximo possível de zero. Isso define a melhor resolução que pontos flutuantes (floats) podem fornecer.Agora, o menor valor geral é
Number.NEGATIVE_INFINITY
, embora ele não seja realmente numérico em um senso estrito.— “Why is
0
less thanNumber.MIN_VALUE
in JavaScript?” no StackOverflow
⚠️ Um bug presenta na V8 v5.5 or anterior (Node.js <=7)⚠️
Todos vocês conhecem a chatice de undefined is not a function, mas e quanto a isso?
// Declare uma classe que extende de null
class Foo extends null {}
// -> [Function: Foo]
new Foo() instanceof null;
// > TypeError: function is not a function
// > at … … …
Isto não é parte da especificação. É apenas um bug que já foi arrumado, então isso não deverá ser um problema no futuro.
E se você tentar somar dois arrays?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
A concatenação ocorre. Passo-a-passo, ela ocorre mais ou menos assim:
[1, 2, 3] +
[4, 5, 6][
// call toString()
(1, 2, 3)
].toString() +
[4, 5, 6].toString();
// concatenation
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");
Você criou um array com 4 elementos vazios. Apesar disso, você terá um array com três elementos, por conta das vírgulas finais (trailing commas):
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'
Trailing commas (também chamadas de "final commas", ou em português, "vírgulas finais") são úteis quando você adiciona novos elementos, parâmetros ou propriedades em um código JS. Caso se você quer adicionar uma nova propriedade, você pode simplesmente adicionar uma nova linha sem modificar a anterior se ela já utiliza uma trailling comma. Isso faz com que os diffs no versionamento de código sejam mais limpos, e também a edição do código menos problemática.
— Trailing commas no MDN
Igualdade de arrays é um monstro no JS, como você pode ver abaixo:
[] == '' // -> 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
Você deve observar bem cautelosamente os exemplos acima! O comportamento é descrito na seção 7.2.13 Abstract Equality Comparison da especificação.
Se nós não passarmos nenhum argumento em um construtor Number
, nós teremos 0
como retorno. O valor undefined
é atribuído em argumentos formais quando não não existem argumentos, então você deve esperar que Number
sem argumentos receba undefined
como um valor dos seus parâmetros. Todavia, quando passamos undefined
, o retorno será NaN
.
Number(); // -> 0
Number(undefined); // -> NaN
De acordo com a especificação:
- Se nenhum argumento for passado na chamada da função,
n
será+0
. - Se não,
n
será ?ToNumber(value)
. - Em caso de
undefined
,ToNumber(undefined)
deve retornarNaN
.
Aqui está a seção correspondente:
parseInt
é famoso por suas peculiaridades:
parseInt("f*ck"); // -> NaN
parseInt("f*ck", 16); // -> 15
💡 Explicação:
Isso acontece porque parseInt
vai continuar parseando caractere por caractere até que ele atinja um caractere desconhecido. O f
em f*ck
é o dígito hexadecimal 15
.
Se você parsear Infinity
para um inteiro…
//
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
Tenha cuidado quando parsear um null
também:
parseInt(null, 24); // -> 23
💡 Explicação:
Ele converte
null
para uma string"null"
e tenta fazer o parse. Para raízes de 0 a 23, não existem numerais que ele possa converter, então ele retorna NaN. Em 24,"n"
, a 14ª letra, é adicionada ao sistema numérico. Em 31,"u"
, a 21ª letra, é adicionada e a string inteira poderá ser decodificada. Em 37 onde não existe mais nenhum numeral válido definido que poderá ser gerado, o retorno éNaN
.— “parseInt(null, 24) === 23… wait, what?” no StackOverflow
Não se esqueça dos octals:
parseInt("06"); // 6
parseInt("08"); // 8 se suporta ECMAScript 5
parseInt("08"); // 0 se não suporta ECMAScript 5
💡 Explicação:
Se uma string de entrada começa com "0", a raiz é oito (octal) ou 10 (decimal). A raiz que é escolhida dependerá da implementação. O ECMAScript 5 define que a 10 (decimal) é utilizada, mas nem todos os navegadores suportam isso ainda. Por essa razão sempre deixe explícito qual será a raiz utilizada quando você usar o parseInt
.
parseInt
sempre converte a entrada para string:
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2
Number({ toString: () => 2, valueOf: () => 1 }); // -> 1
Tenha cuidado quando tentar fazer o parse de valores floating ponts (pontos flutuantes)
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
💡 Explicação:
ParseInt
recebe uma string como argumento e retorna um inteiro da raiz específica. ParseInt
também remove tudo depois e incluindo o primeiro non-digit (não dígito) no parâmetro como string. 0.000001
é convertido para a string "0.000001"
e o parseInt
retorna 0
. Quando 0.0000001
é convertido para uma string ele é tratado como "1e-7"
e, portanto, parseInt
retorna 1
. 1/1999999
é interpretado como 5.00000250000125e-7
e o parseInt
retorna 5
.
Vamos fazer algumas contas:
true +
true(
// -> 2
true + true
) *
(true + true) -
true; // -> 3
Hmmm… 🤔
Podemos forçar valores números com o construtor Number
. É bem óbvio que true
será forçado para 1
:
Number(true); // -> 1
O operador unário soma (i++) tenta converter o valor para um número. Ele pode converter representações de inteiros e flutuantes em strings, bem como os valores que não são stings, como true
, false
e null
. Se ele não conseguir parsear um valor particular, então será avaliado como NaN
. Isso significa que nós podemos forçar true
para 1
facilmente:
+true; // -> 1
Quando você realiza uma adição ou uma multiplicacão, o método ToNumber
é invocado.
De acordo com a especificação, esse método retorna:
Se
argument
é true, o retorno será 1. Seargumento
éfalse
, o retorno será 0.
Por isso podemos adicionar valores booleanos (boolean) como números regulares e obtermos os resultados corretos.
Seções correspondentes:
Você ficará impressionado, mas <!--
(sintaxe de comentários do HTML) são comentários válidos no JavaScript.
// comentário válido
<!-- comentário válido também
Impressionado? Comentários HTML se destinavam a permitir que navegadores que não interpretavam a tag <script>
fossem degradados normalmente. Esses browsers, e.x. Netscape 1.x, não são mais populares. Portanto, não precisamos mais colocar comentários HTML em suas tags script.
Como o Node.js é baseado na V8, comentários HTML são suportados pela runtime do Node.js também. Além disso, eles fazem parte da especificação:
O tipo NaN
é um 'number'
:
typeof NaN; // -> 'number'
Explicações de como os operadores typeof
e instanceof
funcionam:
typeof []; // -> 'object'
typeof null; // -> 'object'
// contudo
null instanceof Object; // false
O comportamento do operador typeof
é definido nessa seção da especificação:
De acordo com a especificação, o operador typeof
retorna uma string de acordo com a Table 35: typeof
Operator Results. Para null
, objetos exóticos comuns e exóticos não padronizados, que não implementam [[Call]]
, o retorno será a string "object"
.
Todavia, você poderá verificar o tipo de um objeto usando o método 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
Isso é causado pelo padrão IEEE 754-2008 para Binary Floating-Point Arithmetic (Aritmética de binários de ponto flutuante). Nessa escala, ele arredonda para o número par mais próximo. Leia mais:
- 6.1.6 The Number Type
- IEEE 754 na Wikipedia
Uma piada bastante conhecida. Uma adição de 0.1
e 0.2
é mortalmente precisa:
0.1 +
0.2(
// -> 0.30000000000000004
0.1 + 0.2
) ===
0.3; // -> false
A responsta para a pergunta ”Is floating point math broken?” no StackOverflow:
As constantes
0.2
and0.3
no seu programa serão também aproximações dos seus valores verdadeiros. Isso ocorre quando odouble
mais próximo de0.2
é maior que o número racional0.2
, mas odouble
mais próximo de0.3
é menor que o número racional0.3
. A soma de0.1
e0.2
acaba sendo maior que o número racional0.3
, e, portanto, discorda da constante em seu código.
Esse problema é tão conhecido que existe um website chamado 0.30000000000000004.com. Isso ocorre em todas as linguagens que utilizam floating-point math (matemática de ponto flutuante), não apenas no JavaScript.
Você pode adicionar seus próprios métodos em objetos como Number
ou String
.
Number.prototype.isOne = function() {
return Number(this) === 1;
};
(1.0).isOne(); // -> true
(1).isOne(); // -> true
(2.0)
.isOne()(
// -> false
7
)
.isOne(); // -> false
Obviamente você pode extender o objeto Number
como qualquer outro no JavaScript, contudo, não é recomendado se o comportamento do método definido não for parte da especificação. Aqui está a lista de propriedades do Number
:
1 < 2 < 3; // -> true
3 > 2 > 1; // -> false
Por que isso funciona assim? Bem, o problema está na primeira parte da expressão. Aqui está como isso funciona:
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
Nós podemos resolver isso com o operador Maior ou igual que (>=
);
3 > 2 >= 1; // true
Leia mais sobre os operadores Relacionais na especificação:
Geralmente os resultados de operações aritméticas em JavaScript podem ser inespererados. Considere esses exemplos:
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
O que está acontecendo com os primeiros quatro exemplos? Aqui está uma tabela para entender a soma no JavaScript:
Number + Number -> adição
Boolean + Number -> adição
Boolean + Boolean -> adição
Number + String -> concatenação
String + Boolean -> concatenação
String + String -> concatenação
E quanto aos outros exempos? Os métodos ToPrimitive
e ToString
estão sendo chamados implicitamente por []
e {}
antes da adição. Leia mais sobre o processo de evaluação na especificação:
- 12.8.3 The Addition Operator (
+
) - 7.1.1 ToPrimitive(
input
[,PreferredType
]) - 7.1.12 ToString(
argument
)
Você sabia que você pode somar números dessa forma?
// Adicione um método toString
RegExp.prototype.toString =
function() {
return this.source;
} /
7 /
-/5/; // -> 2
"str"; // -> 'str'
typeof "str"; // -> 'string'
"str" instanceof String; // -> false
O construtor String
retorna uma string:
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> true
Vamos tentar com um new
:
new String("str") == "str"; // -> true
typeof new String("str"); // -> 'object'
Objeto? O que é isso?
new String("str"); // -> [String: 'str']
Mais informações sobre o construtor String na especificação:
Vamos declarar uma função que irá logar todos os parâmetros no console:
function f(...args) {
return args;
}
Sem dúvida, você sabe que uma função pode ser chamada assim:
f(1, 2, 3); // -> [ 1, 2, 3 ]
Mas você sabia que você pode chamar qualquer função usando backticks (crases)?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`;
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]
Bom, isso não é uma mágica se você está familiarizado com Tagged template literals. No exemplo acima, a função f
é uma tag para template literal. Tags antes do template literal permitem que você faça o parse do template literals com uma função. O primeiro argumento de uma função tag contém um array de valores em string. O restante dos argumentos são relacionados às expressões. Exemplo:
function template(strings, ...keys) {
// faça algo com as strings e as chaves...
}
Esta é a mágica por trás de uma lib famosa, chamada 💅 styled-components - que é bem conhecida na comunidade React.
Link para a especificação:
Achado por @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
Atenção, isso vai explodir sua mente! Tente reproduzir esse código na sua cabeça: estamos aplicando o método call
usando o método apply
. Leia mais:
- 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?
Vamos considerar esse exemplo passo-a-passo:
// Declare uma nova constante, com um valor 'constructor' em string
const c = "constructor";
// c é uma string
c; // -> 'constructor'
// Recuperando o construtor da string
c[c]; // -> [Function: String]
// Recuperando o construtor do construtor
c[c][c]; // -> [Function: Function]
// Chame a função e passe o corpo
// de uma nova função como argumento
c[c][c]('console.log("WTF?")'); // -> [Function: anonymous]
// E então chame essa função anônima
// O Resultado será um log no console com a string 'WTF?'
c[c][c]('console.log("WTF?")')(); // > WTF?
Um Object.prototype.constructor
retorna a referência da função construtora Object
que criou a instância do objeto. Em casos com strings ele é String
, em casos com números ele é um Number
, e assim por diante.
{ [{}]: {} } // -> { '[object Object]': {} }
Por que isso funciona assim? Aqui estamos usando uma Computed property name. Quando você passa um objeto dentro desses colchetes ({ }
), ele força o objeto para uma string, e então temos a chave '[object Object]'
com o valor {}
.
Nós podemos fazer o "brackets hell" dessa forma:
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}
// estrutura:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }
Leia mais sobre object literals aqui:
Como nós sabemos, os primitivos não tem protótipos. Contudo, se nós tentarmos recuperar o valor de um __proto__
para primitivos, teremos o seguinte retorno:
(1).__proto__.__proto__.__proto__; // -> null
Isso acontece porque quando algo não possui um protótipo, ele será envolvido em um objeto usando o método ToObject
. Então, seguindo essa linha:
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__; // -> null
Aqui temos mais informações sobre __proto__
:
Qual é o resultado da expressão abaixo?
`${{ Object }}`;
A resposta é:
// -> '[object Object]'
Nós definimos um objeto com a propriedade Object
usando a Shorthand property notation (notação curta de propriedade).
{
Object: Object;
}
Então nós passamos esse objeto para o template literal, e o método toString
chama por aquele objeto. Por isso temos a string '[object Object]'
.
Considere o exemplo:
let x,
{ x: y = 1 } = { x };
y;
O exemplo acima é uma excelente tarefa para uma entrevista. Qual é o valor de y
? A resposta é:
// -> 1
let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
Com o exemplo acima:
- Nós declaramos
x
sem nenhum valor, então ele éundefined
. - Então nós empacotamos o valor de
x
dentro da propriedadex
do objeto. - Depois nós extraímos o valor de
x
usando a desestruturação e queremos atribuí-lo paray
. Se o valor não for definido, então usaremos1
como default. - Retornarmos o valor de
y
.
- Object initializer no MDN
Exemplos interessantes podem ser compostos com spreading (dispersão) de arrays. Considere o seguinte:
[...[..."..."]].length; // -> 3
Por que 3
? Quando utilizamos o spread operator, o método @@iterator
é chamado, e o iterator retornado é utilizado para obter os valores para ser iterado. O iterador padrão para string dispersa uma string em caracteres. Depois de dispersar, nós empacotamos esses valores dentro de um array. E então dispersamos esse array novamente e empacotamos de volta em um array.
Uma string de '...'
consistem em três caracteres de .
, então o tamanho do array resultante será 3
.
Agora, detalhadamente:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3
Obviamente, nós podemos dispersar e envolver elementos de um array quantas vezes quisermos:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// e assim vai ...
Poucos programadores conhecem sobre rótulos no JavaScript, e eles são interessantes:
foo: {
console.log("first");
break foo;
console.log("second");
}
// > first
// -> undefined
A sentença rotulada é utilizada com os comandos break
ou continue
. Você pode utilizar um rótulo para identificar um laço de repetição, e então usar os comandos break
ou continue
para indicar quando um programa deverá interromper ou continuar a execução de um loop.
No exemplo acima, nós identificamos o rótulo foo
. Depois disso, é executado o that console.log('first');
e depois interrompemos a execução.
Leia mais sobre rótulos no JavaScript:
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
Parecido com os exemplos anteriores, acesse esses links:
O que a seguinte expressão irá retornar? 2
ou 3
?
(() => {
try {
return 2;
} finally {
return 3;
}
})();
A resposta é 3
. Surpreso?
Observe o exemplo abaixo:
new class F extends (String, Array) {}(); // -> F []
Isto é uma herança múltipla? Não.
A parte interessante é o valor da cláusula extends
((String, Array)
). O operador de agrupamento sempre retorna seu último argmento, então (String, Array)
é somente Array
.
Isso significa que nós criamos uma classe que extende um Array
.
Considere o exemplo de um gerador que produz a si mesmo:
(function* f() {
yield f;
})().next();
// -> { value: [GeneratorFunction: f], done: false }
Como você pode ver, o valor retornado é um objeto com seu value
igual a f
. Nesse caso, nós podemos fazer algo assim:
(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 }
// e assim por diante
// ...
Para entender porque isso funciona assim, leia essas seções da especificação:
Considere essa sintaxe ofuscada:
typeof new class {
class() {}
}(); // -> 'object'
Parece que estamos declarando uma classe dentro de outra. Isso deveria ser um erro, contudo, nós obtemos uma string com 'object'
.
Desde o ECMAScript 5, palavras reservadas são permitidas como nomes de propriedades. Pense nisso como esse exemplo simples de objeto:
const foo = {
class: function() {}
};
O ES6 padronizou o atalho para definição de métodos. Também, classes podem ser anônimas. Então, se removermos a parte : function
, teremos o seguinte resultado:
class {
class() {}
}
O resultado de uma classe padrão será sempre um objeto simples. E o seu tipo deverá ser 'object'
.
Leia mais aqui:
Com símbolos bem conhecidos, aqui está uma maneira de se livrar da coerção de tipo. Dê uma olhada:
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;
}
Agora podemos usar isso dessa forma:
// objetos
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
// números
const baz = nonCoercible(1);
baz == 1; // -> TypeError: Trying to coerce non-coercible object
baz === 1; // -> false
baz.valueOf() === 1; // -> true
Considere o exemplo abaixo:
let f = () => 10;
f(); // -> 10
Tá bom, legal, mas e agora isso:
let f = () => {};
f(); // -> undefined
Você provavelmente espera {}
ao invés de undefined
. Isso se dá porque os colchetes ({}
) são parte da sintaxe das arrow functions, então f
retornará indefinido. Contudo é possível retornar o objeto {}
diretamente da arrow function, fechando seu valor dentro das chaves (parênteses).
let f = () => ({});
f(); // -> {}
Considere o exemplo abaixo:
let f = function() {
this.a = 1;
};
new f(); // -> { 'a': 1 }
Agora, tente fazer o mesmo com uma arrow function:
let f = () => {
this.a = 1;
};
new f(); // -> TypeError: f is not a constructor
Arrow functions não podem ser utilizadas como construtores e irão devolver um erro quando utilizadas com new
. Porque elas possuem seu this
léxico, e elas não possuem uma propriedade prototype
, então isso não faria sentido.
Considere o exemplo abaixo:
let f = function() {
return arguments;
};
f("a"); // -> { '0': 'a' }
Agora tente o mesmo com uma arrow function:
let f = () => arguments;
f("a"); // -> Uncaught ReferenceError: arguments is not defined
Arrow functions são uma versão mais leve das funções regulares, com um foco em serem curtas e com o this
léxico. Ao mesmo, arrow functions não fornecem uma ligacão para o objeto argumentos
. Como uma alternativa, utilize os rest parameters
para ter o mesmo resultado:
let f = (...args) => args;
f("a");
- Arrow functions no MDN.
A sentença return
também é traiçoeira. Considere o seguinte:
(function() {
return
{
b: 10;
}
})(); // -> undefined
return
e a expressão retornada precisam estar na mesma linha:
(function() {
return {
b: 10
};
})(); // -> { b: 10 }
Isso se dá por causa do conceito chamado Automatic Semicolon Insertion (Inserção Automática de Ponto e vírgula), que magicamente insere o ponto e vírgula (;
) após a maioria das novas linhas. No primeiro exemplo, existe um ponto e vírgula entre a sentença return
e o objeto, então a função retorna undefined
e o objeto nunca é avaliado.
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 direita para a esquerda, {n: 2}
é atribuído para foo
, e o resultado dessa atribuição {n: 2}
é atribuído para foo.x
, e por isso bar
é {n: 1, x: {n: 2}}
, pois bar
é uma referência a foo
. Mas por que foo.x
é indefinido enquanto bar.x
não?
Foo e bar referenciam o mesmo objeto {n: 1}
, e l-values são resolvidos antes das atribuições. foo = {n: 2}
está criando um novo objeto, e então foo é atualizado para referenciar esse novo objeto. O truque aqui é que foo em foo.x = ...
como um l-value foi resolvido antes e continua referenciando o objeto antigo foo = {n: 1}
e o atualiza adicionando o valor de x. Depois desse encadeamento, bar continua referenciando o objeto antigo foo, mas foo referencia o novo objeto {n: 2}
, onde x não existe.
É equivalente a:
var foo = {n: 1};
var bar = foo;
foo = {n: 2} // -> {n: 2}
bar.x = foo // -> {n: 1, x: {n: 2}}
// bar.x aponta para o novo objeto foo
// e não é equivalente a: bar.x = {n: 2}
var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1
E quanto aos arrays pseudo-multidimensionais>
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
O operator de colchete []
converte a expressão passada usando toString
. A conversão de um array de um elemento em uma string é semelhante à conversão de um elemento contido em uma string:
["property"].toString(); // -> 'property'
null > 0; // false
null == 0; // false
null >= 0; // true
Em resumo, se null
é menos que 0
e false
, então null >= 0
é true
. Leia a explicação mais detalhada disso aqui.
Number.toFixed()
pode ter um comportamento estranho em navegadores diferentes. Veja esse exemplo:
(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
Enquanto seu primeiro instinto é achar que o IE11 está correto e Firefox/Chrome estão errados, a realidade é que Firefox/Chrome são mais obedientes em padrões de números (IEEE-754 Floating Point), enquanto o IE11 é desobediente na tentativa de dar resultados mais claros.
Você pode ver isso acontecendo com alguns testes rápidos:
// Confirme o resultado ímpar do arredondamento de 5 para baixo
(0.7875).toFixed(3); // -> 0.787
// Parece que é apenas 5 quando você expande para os
// limites da precisão de flutuação de 64 bits (precisão dupla)
(0.7875).toFixed(14); // -> 0.78750000000000
// Mas e se formos além do limite?
(0.7875).toFixed(20); // -> 0.78749999999999997780
Números de ponto flutuante não são salvos internamente como uma lista de dígitos decimais, mas com uma metodologia um pouco mais complicada que produz pequenas imprecisões que são usualmente arredondadas por toString
ou chamadas similares, mas estão presentes internamente.
Nese caso, aquele "5" no final era atualmente uma fração extremamente pequena abaixo de um 5 verdadeiro. Arredondá-lo a qualquer comprimento razoável o tornará um 5... mas na verdade não é um 5 internamente.
O IE11, no entanto, relatará a entrada de valor apenas com zeros anexados ao final, mesmo no caso toFixed(20), pois parece estar arredondando à força o valor para reduzir os problemas dos limites de hardware.
Veja por referência NOTE 2
na definição do toFixed
no 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
- Por que Math.max() é menor que Math.min()? by Charlie Harvey
As seguintes expressões parecem introduzir uma contradição:
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true
Como null
não pode ser igual nem maior que 0
, se null> = 0
é realmente true
? (Isso também funciona com menor que da mesma maneira.)
O jeito que essas três expressões são avaliadas são diferentes e são responsáveis por produzirem esse comportamento inesperado.
Primeiro, a comparação abstrata de igualdade null == 0
. Normalmente, se o operador não pode comparar os valores dos dois lados, ele converte ambos em números e compara os números. Então, você poderá esperar o seguinte comportamento:
// Isso não é o que acontece
(null == 0 + null) == +0;
0 == 0;
true;
Contudo, de acordo com a leitura da especificação, a conversão de números não pode acontecer em um lado que é null
ou undefined
. Portanto, se você tem null
em um lado do sinal de igual, o outro lado precisa ser null
ou undefined
para que essa expressão retorne true
. Como não é esse o caso, o retorno é false
.
Depois, a comparação relacional null > 0
. Aqui o algoritmo, diferentemente do operador abstrato de comparação, irá converter null
em um número. Portanto, temos o seguinte comportamento:
null > 0
+null = +0
0 > 0
false
Finalmente, a comparação relacional null >= 0
. Você pode argumentar que essa expressão deveria ser o resultado de null > 0 || null == 0
; se fosse esse o caso, então os resultados acima deveriam mostrar que isso também seria false
. Todavia, o operador >=
funciona de uma maneira diferente, onde basicamente ele se comporta de maneira oposta ao operador <
. Como nosso exemplo acima com o operador maior que também é válido para o operador menor que, isso significa que essa expressão é realmente avaliada da seguinte forma:
null >= 0;
!(null < 0);
!(+null < +0);
!(0 < 0);
!false;
true;
JS nos permite declarar variáveis das seguintes formas:
a;
a;
// This is also valid
a, a;
Funciona também no modo estrito:
var a, a, a;
var a;
var a;
Todas as definições são combinadas em uma definição.
Imagine que você precisa ordenar um array de números.
[ 10, 1, 3 ].sort() // -> [ 1, 10, 3 ]
A ordem padrão de ordenacão é feita na conversão dos elementos em texto, e depois comparando suas sequências de valores de unidades de código em UFT-16.
Passe comparefn
se você tentar ordenar algo que não seja string.
[ 10, 1, 3 ].sort((a, b) => a - b) // -> [ 1, 3, 10 ]
- wtfjs.com — uma coleção dessas várias irregularidades especiais, inconsistências e momentos dolorosos para cada linguagem da web.
- Wat — Uma excelente palestra de Gary Bernhardt no CodeMash 2012
- What the... JavaScript? — uma talk de Kyle Simpson para o Forward 2, que tenta “pull out the crazy” do JavaScript. Ele te ajuda a produzir código limpo, elegante, legível e inspirar a contribuir com a comunidade open source.