Una lista de ejemplos divertidos y complejos de JavaScript
JavaScript es un lenguaje genial. Tiene una sintaxis simple, un gran ecosistema y lo que es mas importante, una genial comunidad.
Sabemos que Javascript es un lenguaje divertido pero que, al mismo tempo, tiene partes complejas. Mientras algunas de ellas pueden, de repente, convertir nuestro trabajo diario en un infierno, otras nos hacen partirnos de risa.
La idea original de WTFJS pertenece a Brian Leroux. Esta lista está fuertemente inspirada en su charla “WTFJS” at dotJS 2012:
Puedes instalar este manual usando npm
. Simplemente ejecuta:
$ npm install -g wtfjs
Ahora tienes que ser capaz de ejecutar wtfjs
en tu línea de comandos. Esto abrirá el manual en tu $PAGER
seleccionado. Por otra parte, también puedes continuar leyéndola aquí mismo.
El código está disponible aquí: https://github.com/denysdovhan/wtfjs
Actualmente, están disponibles las siguientes traducciones de wtfjs:
- 💪🏻 Motivación
- ✍🏻 Notación
- 📄 Notas de traducción
- 👀 Ejemplos
[]
es igual a![]
true
no es igual a![]
, pero tampoco igual a[]
- true es false
- baNaNa
NaN
no es unNaN
- Es un fail
[]
is truthy, but nottrue
null
es falsy, pero nofalse
document.all
es un objeto, pero es undefined- Valor mínimo (Minimal) es más grande que cero
- function no es una function
- Sumando arrays
- Comas finales en los array
- La igualdad de Array es un montruo
undefined
yNumber
parseInt
es un tipo malo- Matemáticas con
true
yfalse
- Los comentarios HTML son válidos en Javascript
NaN
esnota number[]
ynull
son objetos- Incrementando números magicamente
- Precision of
0.1 + 0.2
- Parcheando números
- Comparación de tres números
- Matemáticas divertidas
- Suma de RegExps
- Los Strings no son instancias de
String
- Llamando funciones con comillas de ejecución (backticks)
- Call call call
- Una propiedad
constructor
- Object como key de una propiedad de un object
- Accediendo a prototipos con
__proto__
`${{Object}}`
- Desestructuración con valores por defecto.
- Puntos y propagación
- Labels (etiquetas)
- Etiquetas anidadas
try..catch
malicioso- ¿Esto es herencia múltiple?
- Un generador que se produce a si mismo
- Una clase de clase
- Objetos no coercibles
- Funciones de flecha complejas
- Las funciones flecha no pueden ser un constructor
arguments
y las funciones de flecha- return complejo
- Accediendo a propiedades de objetos con arrays
- Null y los Operadores Relacionales
Number.toFixed()
muestra distintos númerosMath.max()
es menor queMath.min()
- Comparando
null
a0
- Misma redeclaración de variables
- 📚 Otros recursos
- 🎓 Licencia
Solo por diversión
— “Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds
El principal objetivo de esta lista es recoger algunos ejemplos locos y explicar como funcionan, si es que eso es posible. Solo porque es divertido aprender cosas que no sabiamos antes.
Si eres un principiante, puedes usar estos apuntes para entrar mas profundo en JavaScript. Espero que esatas notas te motiven a pasar mas tiempo leyendo la especificación.
Si eres un programador profesional, puedes considerar estos ejemplos como una buena referencia para todas las peculiaridades y comportamientos inesperados de nuestro querido JavaScript.
En cualquier caso, tan solo lee esto. Probablemente descubrirás algo nuevo.
// ->
es usado para mostrar el resultado de una expresión. Por ejemplo:
1 + 1; // -> 2
// >
significa el resultado de console.log
o alguna otra salida. Por ejemplo:
console.log("hello, world!"); // > hello, world!
//
es solo un comentario usado para aclaraciones. Ejemplo:
// Asignando una función a la constante foo
const foo = function() {};
** Los términos truthy
y falsy
no se traducirán al no encontrar en español equivalencia válida.
** El término coerced
se traducirá por coercido, que aunque la traducción mas usada sea coaccionado, coercido se parece mas al termino original.
Array es igual a no array:
[] == ![]; // -> true
El operador de igualdad abstracto convierte los dos lados a numeros para poder compararlos, y ambos lados se convierten en el numero 0
por distintas razones. Los Arrays son truthy, así que en la derecha, lo contrario a un valor truthy es false
, que es coercido a 0
. A la izquierda, sin embargo, un array vacío es coercido a un numero sin ser primero boolean, y los arrays vacios se coercen a 0
, a pesar de ser truthy.
Así es como esta expresión se simplifica:
+[] == +![];
0 == +false;
0 == 0;
true;
Ver también []
es truthy, pero no true
.
Array no es igual a true
, pero tampoco no array es igual a true
;
Array es igual a false
, no Array es también igual a false
:
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
true == []; // -> false
true == ![]; // -> false
// Según la especificación
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> true
false == ![]; // -> true
// Según la especificación
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> false
![]; // -> false
false == false; // -> true
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
Considera este paso-a-paso:
// true es 'truthy' y representado por el valor 1 (number), 'true' en formato string es NaN.
true == "true"; // -> false
false == "false"; // -> false
// 'false' no es un string vacío, por lo que es un valor truthy
!!"false"; // -> true
!!"true"; // -> true
"b" + "a" + +"a" + "a";
Esta es una broma old-school de Javascript pero remasterizada. Aquí esta la original:
"foo" + +"bar"; // -> 'fooNaN'
La expresión es evaluada como 'foo' + (+'bar')
, lo que convierte 'bar'
en un not a number (NaN).
NaN === NaN; // -> false
La especificación define estrictamente la logica detras de este comportamiento:
- Si
Type(x)
es diferente deType(y)
, return false.- Si
Type(x)
es Number:
- Si
x
es NaN, return false.- Si
y
es NaN, return false.- … … …
A continuación la definición de NaN
de la IEEE:
Four mutually exclusive relations are possible: less than, equal, greater than, and unordered. The last case arises when at least one operand is NaN. Every NaN shall compare unordered with everything, including itself.
— “What is the rationale for all comparisons returning false for IEEE754 NaN values?” at StackOverflow
Parece increíble pero …
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'
Descomponiendo esta amalgama de simbolos en bloques, nos damos cuenta que el siguiente patrón ocurre a menudo:
![] + []; // -> 'false'
![]; // -> false
So we try adding []
to false
. But due to a number of internal function calls (binary + Operator
-> ToPrimitive
-> [[DefaultValue]]
) we end up converting the right operand to a string:
![] + [].toString(); // 'false'
Pensando en un string como un array, podemos acceder a su primer caracter via [0]
:
"false"[0]; // -> 'f'
El resto es obvio, aunque la i
es mas difícil. La i
de fail
se obtiene de la generació del string 'falseundefined'
y cogiendola del elemento con el índice ['10']
Un array es un valor value, sin embargo, no es igual a true
.
!![] // -> true
[] == true // -> false
Aquí hay los links a la sección correspondiente de la especificación ECMA-262:
A pesar que null
es un valor falsy, no es igual a false
.
!!null; // -> false
null == false; // -> false
Al mismo tiempo, otros varlores falsy, como 0
o ''
son igual a false
.
0 == false; // -> true
"" == false; // -> true
La explicación es la misma que en el ejemplo anterior. Aquí está el link:
⚠️ Esto es parte de la API del navegador por lo tanto no funciona en un entorno Node.js⚠️
A pesar que document.all
es un objecto tipo array y que da acceso a los nodos del DOM de la página, responde a la función typeof
con undefined
.
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'
Al mismo tiempo, document.all
no es igual a undefined
.
document.all === undefined; // -> false
document.all === null; // -> false
Pero al mismo tiempo:
document.all == null; // -> true
document.all
solía ser una vía para acceder a los elementos del DOM, sobretodo con antiguas versiones de IE. Si bien nunca ha sido un estándar, se usó ampliamente en código JS antiguo. Mientras el estandar avanzó con nuevas APIs (comodocument.getElementById
) esta llamada API quedaba obsoleta y obsolete y el comité del estándar tenía que decidir que hacer con ella. Debido a su amplio uso, decidieron mantener la API pero introducir una violación intencionada de la especificación de JavaScript. La razón por la que responde afalse
cuando usa la Strict Equality Comparison conundefined
ytrue
cuando usa la Abstract Equality Comparison es devido a que la deliverada violación de la especificacion permite hacer esto.— “Obsolete features - document.all” at WhatWG - HTML spec — “Chapter 4 - ToBoolean - Falsy values” at YDKJS - Types & Grammar
Number.MIN_VALUE
es el valor más pequeño, que es mayor que cero
Number.MIN_VALUE > 0; // -> true
Number.MIN_VALUE
es5e-324
, es decir, el número positivo más pequeño que se puede representar con precisión flotante (float), es decir, el valor más cerca de cero al que se puede llegar. Define la mejor resolución que un float puede dar.Ahora el valor más pequeño en general es
Number.NEGATIVE_INFINITY
Aunque no es realmente numérico en sentido estricto.— “Why is
0
less thanNumber.MIN_VALUE
in JavaScript?” at StackOverflow
⚠️ Hay un bug en V8 v5.5 o versiones inferiores (Node.js <=7)⚠️
Todos conocemos el molesto undefined is not a function, pero que pasa con este?
// Se declara una clase que extiende a null
class Foo extends null {}
// -> [Function: Foo]
new Foo() instanceof null;
// > TypeError: function is not a function
// > at … … …
Esto no es una parte de la especificación. Es solo un error que ahora ha sido solventado, por lo que no debería haver problemas con él en el futuro.
Que passa si se prueba a somar dos arrays?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
Pasa la concatenación. Paso a paso:
[1, 2, 3] +
[4, 5, 6][
// llama a toString()
(1, 2, 3)
].toString() +
[4, 5, 6].toString();
// concatenación
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");
Creas un array con 4 elementos vacios. A pesar de todo, obtendras un array con tres elementos debido a las comas finales:
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'
Comas finales Pueden ser utiles cuando se añaden nuevos elementos, parametros o propiedades en JavaScript. Para añadir una nueva propiedad, simplemente se puede añadir una linia nueva sin modificar la anterior si esta usa una coma final. Esto hace el versionado más limpio y la edición de codigo menos sensible a errores.
— Trailing commas at MDN
La igualdad de Array es un montruo en JS, como se puede ver a continuación:
[] == '' // -> 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
Es importante mirar los ejemplos anteriores muy detalladamente! El comportamiento es explicado en la sección 7.2.13 Abstract Equality Comparison de la especificación.
Si no se pasan argumentos al constructor de Number
, se obtiene un 0
. El valor undefined
es asignado como argumento por defecto cuando no hay argumentos, por lo tanto se podría esperar que Number
sin argumento, use undefined
como valor de su parametro. Sin embargo, si se pasa undefined
, lo que se obtiene es NaN
.
Number(); // -> 0
Number(undefined); // -> NaN
Según la especificación:
- Si no hay argumentos pasados en la invocación de la función,
n
será+0
. - Si no,
n
será ?ToNumber(value)
. - En el caso de
undefined
,ToNumber(undefined)
devolveráNaN
.
Aquí está la sección correspondiente:
parseInt
es famoso por sus peculiaridades:
parseInt("f*ck"); // -> NaN
parseInt("f*ck", 16); // -> 15
Esto sucede porque parseInt
continua convirtiendo carácter a carácter hasta que encuentra uno que no conoce. La f
en 'f*ck'
es el dígito hexadecimal 15
.
Convirtiendo Infinito
a integer…
//
parseInt("Infinito", 10); // -> NaN
// ...
parseInt("Infinito", 18); // -> NaN...
parseInt("Infinito", 19); // -> 18
// ...
parseInt("Infinito", 23); // -> 18...
parseInt("Infinito", 24); // -> 151176378
// ...
parseInt("Infinito", 29); // -> 385849803
parseInt("Infinito", 30); // -> 13693557269
// ...
parseInt("Infinito", 34); // -> 28872273981
parseInt("Infinito", 35); // -> 1201203301724
parseInt("Infinito", 36); // -> 1461559270678...
parseInt("Infinito", 37); // -> NaN
Cuidado al convertir null
:
parseInt(null, 24); // -> 23
Se está convirtiendo
null
al string"null"
y provando de convertirlo. Para las bases de 0 a 23, no hay numericos que pueda convertir, por lo que retorna NaN. En el 24,"n"
, la 14.ª letra, es añadida al sistema numerico. En el 31,"u"
, la 21.ª letra, es añadida y el string entero puede ser decodificado. En la 37 ya no hay ningún numerico válido que pueda ser generado y se devuelveNaN
.— “parseInt(null, 24) === 23… espera, que?” at StackOverflow
No hay que olvidarnos de los octales:
parseInt("06"); // 6
parseInt("08"); // 8 si soporta ECMAScript 5
parseInt("08"); // 0 si no soporta ECMAScript 5
💡 Explicación: Si el string de entrada empieza por "0", la base es ocho (octal) o 10 (decimal). La base que se selecciona se decide dependiendo de la implementación. ECMAScript 5 especifica que se usa la 10 (decimal), pero no todos los navegadores los soportan aún. Por esta razón siempre hay que especificar una base cuando se usa parseInt
.
parseInt
siempre convierte el valor entrado a string:
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2
Number({ toString: () => 2, valueOf: () => 1 }); // -> 1
Cuidado cuando se convierten valores de punto flotante
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
💡 Explicación: ParseInt
coge un argumento de tipo string argument y retorna un integer con la base especificada. ParseInt
también elimina cualquier cosa a partir del primer dígito no numerico del parámetro tipo string. 0.000001
es convertido a string "0.000001"
y parseInt
retorna 0
. Cuando 0.0000001
se convierte a string es tratado como "1e-7"
y por lo tanto parseInt
retorna 1
. 1/1999999
es interpretado como 5.00000250000125e-7
y parseInt
retorna 5
.
Vamos a hacer un poco de matemáticas:
true +
true(
// -> 2
true + true
) *
(true + true) -
true; // -> 3
Hmmm… 🤔
Se pueden coercer valores a numeros con el constructor de Number
. Es bastante obvio que true
será coercido a 1
:
Number(true); // -> 1
El operador unario de suma intenta converitr su valor en number. Puede convertir las representaciones en string de integers y floats, también las no numéricas como true
, false
, y null
. Si no puede con un valor en concret, lo evalua como NaN
. Esto significa que se coerce true
a 1
facilmente:
+true; // -> 1
Cuando se está ejecutando una acción de suma o multiplicación, el método ToNumber
es invocado. Según la especificación, este método retorna:
Si el
argument
es true, retorna 1. Si elargument
es false, retorna +0.
Por eso es que podemos sumar valores boolean como números y obtener resultados correctos.
Corresponding sections:
Aunque sea sorprendente, <!--
(comentarios de HTML) es un comentario válido también en Javascript.
// valid comment
<!-- valid comment too
¿Impresionado? Los comentarios tipo HTML se usaban para los navegadores que no entendían el tag <script>
. Estos navegadores, como Netscape 1.x ya no son muy populares. Por lo tanto, ya no hay razón para poner comentarios de tipo HTML en JavaScript.
Desde que Node.js está basado en el motor V8, los comentarios tipo HTML HTML-like siguen soportados en tiempo de ejecución. Además, son una parte de la especificación:
Type of NaN
es un 'number'
:
typeof NaN; // -> 'number'
La explicación de como typeof
y instanceof
funcionan:
typeof []; // -> 'object'
typeof null; // -> 'object'
// sin embargo
null instanceof Object; // false
El comportamiento del operador typeof
esta definido en su sección de la especificación:
Según la especificación, el operador typeof
retorna un string según la tabla Table 35: typeof
Operator Results. Para null
, objetos corrientes, exóticos estandar y exóticos no estandar, que no implementen [[Call]]
, retorna el string "object"
.
Sin embargo, se puede comprobar el tipo de un objeto usando el 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
Esto es causado por el estandar IEEE 754-2008 para la aritmetica de binarios de puntos flotantes (Binary Floating-Point Arithmetic). En esta escala, se redondea al número par más cercano. Leer mas:
- 6.1.6 The Number Type
- IEEE 754 on Wikipedia
A well-known joke. An addition of 0.1
and 0.2
is deadly precise:
0.1 +
0.2(
// -> 0.30000000000000004
0.1 + 0.2
) ===
0.3; // -> false
La respuesta para la pregunta en StackOverflow: ¿Están las matemáticas de punto flotante rotas? ”Is floating point math broken?”:
Las constantes
0.2
y0.3
en sus programas serán aproximaciones a sus valores verdaderos. Pasa que eldouble
mas cercano a0.2
es más grande que el número racional0.2
pero eldouble
más cercano a0.3
es más pequeño que el numero racional0.3
. La suma de0.1
y0.2
termina siendo más grande que el número racional0.3
y por lo tanto, distinta a la constante del código.
El problema es tan sabido que incluso hay una web llamada 0.30000000000000004.com. Esto sucede en todos los lenguajes que usen matemáticas de punto flotantes, no solo en JavaScript.
Se pueden añadir métodos propios en el contendor de objetos tipo 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
Obviamente, se puede extender el objeto Number
como cualquier otro objeto en JavaScript. Sin embargo, no se recomienda si el comportamiento del metodo definido no es parte de la especificación. Aqí hay una lista de las propiedades de Number
:
1 < 2 < 3; // -> true
3 > 2 > 1; // -> false
¿Porque esto no funciona así? Bueno,el problema está en la primera parte de la expresión. Aquí está su funcionamiento:
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
Esto se puede solucionar con el operador Greater than or equal (>=
):
3 > 2 >= 1; // true
Leer mas sobre los operadores relacionales en la especificación:
Los operadores aritméticos en Javascript suelen parecer que funcionan de forma inesperada. Algunos ejemplos:
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
¿Que está pasando en los primeros cuatro ejemplos? A continuación una pequeña tabla para entender la suma en JavaScript:
Number + Number -> suma
Boolean + Number -> suma
Boolean + Boolean -> suma
Number + String -> concatenación
String + Boolean -> concatenación
String + String -> concatenación
¿Que pasa con los otros ejemplos? Los métodos ToPrimitive
y ToString
son llamados implicitamente para []
y {}
antes de la suma. Mas información sobre el proceso de evaluación en la especificación:
- 12.8.3 The Addition Operator (
+
) - 7.1.1 ToPrimitive(
input
[,PreferredType
]) - 7.1.12 ToString(
argument
)
¿Sabías que se puede sumar números así?
// Sobreescribiendo un método toString
RegExp.prototype.toString =
function() {
return this.source;
} /
7 /
-/5/; // -> 2
"str"; // -> 'str'
typeof "str"; // -> 'string'
"str" instanceof String; // -> false
El constructor de String
retorna un string:
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> true
Probemos con un new
:
new String("str") == "str"; // -> true
typeof new String("str"); // -> 'object'
¿Objeto? ¿Que es esto?
new String("str"); // -> [String: 'str']
Mas información sobre el constructor de String en la especificación:
Vamos a declarar una función que imprime por consola todos los parametros recibidos:
function f(...args) {
return args;
}
No hay ninguna duda que sabemos llamar la función de esta forma:
f(1, 2, 3); // -> [ 1, 2, 3 ]
Pero sabemos que podemos llamar la función con comillas de ejecución?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`;
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]
Bueno, esto no es mágico del todo si estamos familiarizados con Tagged template literals. En el ejemplo, la función f
es una etiqueta para la plantilla literal. Las etiquetas antes del literal de la plantilla permiten analizar los literales de la plantilla con una función. El primer argumento de una función de etiqueta contiene una matriz de valores string. Los argumentos restantes están relacionados con las expresiones. Ejemplo:
function template(strings, ...keys) {
// hacer algo con los string y las keys
}
Esta es la magia detrás de la famosa librería llamada 💅 styled-components, que es muy popular en la comunidad React.
Link a la especificación:
Encontrado por @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
Cuidado, vas a flipar con esto! Prueba de reproducir este código en tu cabeza: se esta ejecutando el metodo call
usando el metodo apply
. Mas información:
- 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?
Este ejemplo paso a paso:
// Se declara una nueva constante que es un string 'constructor'
const c = "constructor";
// c es un string
c; // -> 'constructor'
// Obteniendo un constructor de string
c[c]; // -> [Function: String]
// Obteniendo un constructor de constructor
c[c][c]; // -> [Function: Function]
// Llama la función constructora y passa
// el cuerpo de la nueva función como argumento
c[c][c]('console.log("WTF?")'); // -> [Function: anonymous]
// Y luego llama la función anónima
// El resultado escribe en la consola un string 'WTF?'
c[c][c]('console.log("WTF?")')(); // > WTF?
Un Object.prototype.constructor
devuelve una referencia a la función constructora del Object
que crea la instancia del objeto. En el caso de los strings es String
, en caso de ser un numerico es Number
y así.
{ [{}]: {} } // -> { '[object Object]': {} }
¿Por qué funciona esto? Se está usando un Nombre de propiedad computada. Cuando se pasa un objeto entre corchetes, se coerce a string, por lo que la key queda '[object Object]'
y el valor {}
.
Se pueden hacer "caos de corchetes" como este:
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}
// estructura:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }
Más información sobre objetos literales aquí:
Como se save, lo tipos primitivos no tienen prototipo. Sin embargo, si se trata de obtener el valor de __proto__
para primitivos, se obtendra lo siguiente:
(1).__proto__.__proto__.__proto__; // -> null
Esto sucede porque cuando algo que no tiene prototipo se envuelve en un objeto contenedor usando el método ToObject
. Paso a paso:
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__; // -> null
Más información sobre __proto__
:
¿Cual és el resultado de la expresión anterior?
`${{ Object }}`;
La respuesta es:
// -> '[object Object]'
Se define un objeto con una propiedad Object
usando la Notació de propiedad abreviada (Shorthand property notation):
{
Object: Object;
}
Luego hemos pasado el objetoa la plantilla literal, por lo que el método toString
es llamado para el objeto. Es por eso que se obtiene el string '[object Object]'
.
Por ejemplo:
let x,
{ x: y = 1 } = { x };
y;
El ejemplo anterior es una buena tarea para una entrevista. ¿Cual es el valor de y
? La respuesta es:
// -> 1
let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
Con el ejemplo anterior:
- Se declara
x
sin valor, por lo que esundefined
. - A continuación se empaqueta el valor de
x
en la propiedadex
del objeto. - A continuación se extrae el valor de
x
usando deconsctrucción y se quiere asignar ay
. Si el valor no está definido, se va a usar1
como valor por defecto. - Se retorna el valor de
y
.
- Object initializer at MDN
Se pueden generar ejemplos interesantes con la propagación de arrays. Como el siguiente:
[...[..."..."]].length; // -> 3
¿Por qué 3
? Cuando se usa el operador de propagación (spread operator), el método @@iterator
es llamado, y el iterador devuleto es usado para obtener los valores donde se va a iterar. El iterador por defecto para los string propaga un string en carácteres. Después de la propagación, se empaquetan los carácteres en un array. Seguidamente se propaga otra vez el array y se empaqueta otra vez en un array.
Un string '...'
está formado por tres .
carácteres, por lo tanto, la longitud del resultado del array es 3
.
Aro, paso a paso:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3
Por supuesto, se puede propagar y empaquetar los elementos de un array las veces que sea necesario:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// etcétera …
No muchos programadores conocen los labels en JavaScript. Son algo interesantes:
foo: {
console.log("first");
break foo;
console.log("second");
}
// > first
// -> undefined
Las declaraciones etiquetadas son unsadas conjuntamente con break
o continue
. Se puede usar una etiqueta para identificar un bucle, y después usar el break
o el continue
para indicar al programa cuendo parar o continuar la ejecución.
En el ejemplo anterior, se identifica una etiqueta foo
. A continuación el console.log('first');
se ejecuta y después se interrumpe la ejecución.
Mas información de las etiquetas en JavaScript:
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
Similar al ejemplo anterior, más información:
¿Que retorna esta expresión? ¿2
o 3
?
(() => {
try {
return 2;
} finally {
return 3;
}
})();
La respuesta es 3
. ¿Sorprendido?
Veamos el siguiente ejemplo:
new class F extends (String, Array) {}(); // -> F []
¿Esto es herencia múltiple? No.
La parte interesante es el valor del extends
: ((String, Array)
). El operador de grup siempre devuelve el último argumento, por lo tanto (String, Array)
es realmente solo Array
. Esto significa que se ha creado una clase que extiende de Array
.
Veamos este ejemplo de un generador que se produce a si mismo:
(function* f() {
yield f;
})().next();
// -> { value: [GeneratorFunction: f], done: false }
Como se puede ver, el valor devuelto es un objeto con su value
igual a f
. En este caso, se puede hacer algo como lo siguiente:
(function* f() {
yield f;
})()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// y otra vez
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// y otra vez
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()
.value()
.next();
// -> { value: [GeneratorFunction: f], done: false }
// etcétera
// …
Para entender como funciona esto, leer las siguientes secciones de la especificación:
Veamos este juego de sintaxis ofuscada:
typeof new class {
class() {}
}(); // -> 'object'
Parece que se está declarando una clase dentro de una clase. Debería ser un error, no obstante, se obtiene el string 'object'
.
Desde la época de ECMAScript 5 , keywords son permitidas como nombres de propiedad. Veamos el siguiente ejemplo de objeto simple:
const foo = {
class: function() {}
};
Y ES6 estandarizó las definiciones de métodos abreviadas. También, las clases pueden ser anónimas. Por lo que si se saca la parte de : function
, se obtiene solamente:
class {
class() {}
}
El resultado de una clase, por defecto, es siempre un objeto simple. Y typeof deve retornar 'object'
.
Mas información:
Con los conocidos symbols, hay una manera de deshacerse de la coerción. Veamos:
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;
}
Ahora se puede usar así:
// objects
const foo = nonCoercible({ foo: "foo" });
foo * 10; // -> TypeError: Probando de coercer un objeto no coercible
foo + "evil"; // -> TypeError: Probando de coercer un objeto no coercible
// strings
const bar = nonCoercible("bar");
bar + "1"; // -> TypeError: Probando de coercer un objeto no coercible
bar.toString() + 1; // -> bar1
bar === "bar"; // -> false
bar.toString() === "bar"; // -> true
bar == "bar"; // -> TypeError: Probando de coercer un objeto no coercible
// numbers
const baz = nonCoercible(1);
baz == 1; // -> TypeError: Probando de coercer un objeto no coercible
baz === 1; // -> false
baz.valueOf() === 1; // -> true
Veamos el siguiente ejemplo:
let f = () => 10;
f(); // -> 10
Okay, vale, pero que hay de esto:
let f = () => {};
f(); // -> undefined
Se podría esperar {}
en canvio se obtiene undefined
. Esto es devido a que las llaves son parte de la sintaxis de las funciones de flecha, por eso f
devuelve undefined. Se puede retornar un objeto {}
directamente en una función de flecha, se deve encerrar las llaves entre paréntesis.
let f = () => ({});
f(); // -> {}
Veamos el siguiente ejemplo:
let f = function() {
this.a = 1;
};
new f(); // -> { 'a': 1 }
Ahora, lo mismo pero con funcion de flecha:
let f = () => {
this.a = 1;
};
new f(); // -> TypeError: f is not a constructor
Las funciones de flecha no pueden ser usadas como constructores y lanzaran un error si se usan con new
. Al tner un this
léxico, y no tener una propiedad prototype
, hacerlo no tendría mucho sentido.
Veamos el siguiente ejemplo:
let f = function() {
return arguments;
};
f("a"); // -> { '0': 'a' }
Ahora lo mismo con una función flecha:
let f = () => arguments;
f("a"); // -> Uncaught ReferenceError: arguments is not defined
Las funciones flecha son las versiones ligeras de las funciones normales y están centradas en ser cortas y en el this
léxico. Al mismo tiempo, las funciones flecha no proveen binding para el objeto arguments
. Como alternativa se puede usar el resto de parametros
para conseguir el mismo resultado:
let f = (...args) => args;
f("a");
- Arrow functions at MDN.
La declaración return
también es compleja. Veamos lo siguiente:
(function() {
return
{
b: 10;
}
})(); // -> undefined
return
y la expresión de retorno deven estar en la misma línea:
(function() {
return {
b: 10
};
})(); // -> { b: 10 }
Esto es devido al concepto llamado Inserción automática de punto y coma, que inserta automaticamente punto y coma al final de cada nueva línea. En el primer ejemplo, va a haver un punto y coma insertado entre el return
y el objeto, por lo que la función devolverá undefined
y el objeto nunca será evaluado.
var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1
¿Que pasa con los arrays pseudo-multidimensionales?
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
El operador []
convierte la expresión dada usando toString
. Convertir un array con un elemento a string es similar a convertir el elemento contenido a string:
["property"].toString(); // -> 'property'
null > 0; // false
null == 0; // false
null >= 0; // true
Si null
es menor que 0
es igual a false
, null >= 0
es igual a true
. Leer la explicación en profundidad para esto aquí.
Number.toFixed()
puede comportarse un poco distindo en diferentes navegadores. Veamos el siguiente ejemplo:
(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
Aunque a primera impresión puede parecer que IE11 hace lo correcto y Firefox/Chrome están equivocados, la realidad es que Firefox/Chrome están cumpliendo directamente los estandares (IEEE-754 Floating Point) para los número, mientras que IE11 is desobedeciéndolos (aunque probablemente séa para conseguir unos resultados más claros).
Veamos como ocurre con algun test rápido:
// Confirmar el resultado impar de redondear un 5 hacia abajo.
(0.7875).toFixed(3); // -> 0.787
// Parece que solo es un 5 cuando se expande a los límites
// de precisión de los float de 64-bit (double-precision) float accuracy
(0.7875).toFixed(14); // -> 0.78750000000000
// ¿Pero y si se va mas allá de los límites?
(0.7875).toFixed(20); // -> 0.78749999999999997780
Los números de punto flotante no se almacenan como una lista de dígitos decimales internamente, sino a través de una metodología más complicada que produce pequeñas imprecisiones que generalmente se redondean con toString y llamadas similares, pero en realidad están presentes internamente.
En este caso, ese "5" al final fue en realidad una fracción extremadamente pequeña por debajo de un verdadero 5. Redondearlo a una longitud razonable lo convertirá en un 5 ... pero en realidad no es un 5 internamente.
IE11, sin embargo, informará el valor ingresado con solo ceros agregados al final incluso en el caso toFixed (20), ya que parece estar redondeando el valor a la fuerza para reducir los problemas de los límites de hardware.
Ver como referéncia NOTE 2
en la definición de ECMA-262 para toFixed
.
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
- Why is Math.max() less than Math.min()? by Charlie Harvey
Las siguientes expresiones parecen caer en contradicciones:
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true
¿Como puede null
no ser ni igual ni mayor que 0
, si null >= 0
es realmente true
? (Esto también sucede de la misma forma con menor o igual.)
La manera con que estas tres expresiones son evaluadas es diferente y es la responsable de producir este comportamiento inesperado.
Primero, la comparación de igualdad abstracta null == 0
. Normalmente, si este operador no puede comparar correctamente los valores en cualquiera de los lados, convierte ambos en números y los compara. Entonces, se podría esperar el siguiente comportamiento:
// Esto no es lo que sucede
(null == 0 + null) == +0;
0 == 0;
true;
Sin embargo, según la especificación, la conversión de números en realidad no ocurre en un lado que es nulo
o no definido
. Por lo tanto, si tiene null
en un lado del signo igual, el otro lado debe ser null
o undefined
para que la expresión devuelva true
. Como este no es el caso, se devuelve false
.
Ahora la comparación relacional null > 0
. El algoritmo aquí, a diferencia del operador de igualdad abstracta, se convertirá null
a un número. Por lo tanto, tenemos este comportamiento:
null > 0
+null = +0
0 > 0
false
Finalmente, la comparación relacional null >= 0
. Se podría argumentar que esta expresión debería ser el resultado de null > 0 || null == 0
; si este fuera el caso, entonces los resultados anteriores significarían que esto también sería false
. Sin embargo, el operador >=
de hecho funciona de una manera muy diferente, que es básicamente tomar el opuesto al operador <
. Debido a que nuestro ejemplo con el operador mayor que el anterior también es válido para el operador menor que, eso significa que esta expresión en realidad se evalúa así:
null >= 0;
!(null < 0);
!(+null < +0);
!(0 < 0);
!false;
true;
JS permite redeclarar variables:
a;
a;
// This is also valid
a, a;
También funciona en el modo estricto:
var a, a, a;
var a;
var a;
Todas las definiciones estan mezcladas en una.
- wtfjs.com — un listado de irregularidades muy especiales, inconsitencias y momentos dolorosamente poco intuitivos en programación web.
- Wat — Una buena charla de Gary Bernhardt en CodeMash 2012
- What the... JavaScript? — Kyle Simpsons habla para Forward 2 sobre intentos de Javascript de "sacar la locura". El quiere ayudar a producir código mas limpio, elegante, legible... También quiere inspirar a la gente a contribuir a la comunidad open source.
Traducción: Carles Hervera