Un tweet reciente contenía este fragmento de JavaScript.
¿Alguien puede explicar lo que está sucediendo paso a paso?
> function dis() { return this }
undefined
> five = dis.call(5)
Number {[[PrimitiveValue]]: 5}
> five.wtf = 'potato'
"potato"
> five.wtf
"potato"
> five * 5
25
> five.wtf
"potato"
> five++
5
> five.wtf
undefined
> five.wtf = 'potato?'
"potato?"
> five.wtf
undefined
> five
6
En particular, no me queda claro:
- por qué el resultado de
dis.call(5)es unNumbercon algún tipo de[[PrimitiveValue]]propiedad, pero los resultados defive++yfive * 5parecen ser solo los números simples5y25(noNumbers) - por qué la
five.wtfpropiedad desaparece después delfive++incremento - por qué la
five.wtfpropiedad ya no es configurable después delfive++incremento, a pesar de que lafive.wtf = 'potato?'asignación aparentemente establece el valor.
javascript
Nathan Long
fuente
fuente

++parece afectar el tipo subyacentePrevsPostincremento? Después de la multiplicación, nuevamente es unNumberobjeto que no tienewtfpropiedad ... Pero aún así es un objetoobjectque puede tenerproperties...discondis.call(5), esto envuelve el número primitivo5en un objeto de tipoNumberque contiene el5, para que este objeto pueda ser devuelto comothisobjeto. La++convierte de nuevo a un número primitivo que no puede contener propiedades, por lo quewtfcomienza a ser indefinido.Respuestas:
OP aquí. Es divertido ver esto en Stack Overflow :)
Antes de pasar por el comportamiento, es importante aclarar algunas cosas:
El valor numérico y el objeto numérico (
a = 3vsa = new Number(3)) son muy diferentes. Uno es primitivo, el otro es un objeto. No puede asignar atributos a primitivas, pero puede asignar a objetos.La coerción entre los dos es implícita.
Por ejemplo:
Cada expresión tiene un valor de retorno. Cuando REPL lee y ejecuta una expresión, esto es lo que muestra. Los valores de retorno a menudo no significan lo que piensas e implican cosas que simplemente no son ciertas.
Está bien, aquí vamos.
La prenda.
Definir una función
disy llamarla con5. Esto ejecutará la función con5el contexto (this). Aquí se coacciona de un valor de Número a un objeto de Número. Es muy importante tener en cuenta que si estuviéramos en modo estricto, esto no habría sucedido .Ahora establecemos el atributo
five.wtfen'potato', y con cinco como objeto, efectivamente acepta la Asignación simple .Con
fivecomo objeto, me aseguro de que aún pueda realizar operaciones aritméticas simples. Puede. ¿Sus atributos aún se mantienen? Si.El turno.
Ahora lo comprobamos
five++. El truco con el incremento de postfix es que toda la expresión se evaluará contra el valor original y luego incrementará el valor. Parece quefivetodavía son cinco, pero realmente la expresión se evaluó en cinco, luego se estableciófiveen6.No solo
fivese configuró en6, sino que se volvió a convertir en un valor de Número y todos los atributos se pierden. Como las primitivas no pueden contener atributos,five.wtfno está definido.De nuevo intento reasignar un atributo
wtfafive. El valor de retorno implica que se mantiene, pero de hecho no lo hace porquefivees un valor de Número, no un objeto de Número. La expresión se evalúa como'potato?', pero cuando verificamos vemos que no se asignó.El prestigio.
Desde el incremento de postfix,
fiveha sido6.fuente
intyInteger, por ejemplo. Supongo que esto es para que pueda crear una funcióndoSomething(Object)y aún así poder darle primitivas. Las primitivas se convertirán a sus clases correspondientes en este caso. Pero a JS realmente no le importan los tipos, por lo que la razón es probablemente otra cosa++hacer es aplicar ToNumber al valor . Compare un caso similar con cadenas: si tienex="5",x++devuelve el número5.dis.call(5), la coerción para objetar, nunca hubiera esperado eso.Hay dos formas diferentes de representar un número:
El primero es un primitivo, el segundo un objeto. Para todos los efectos, ambos se comportan igual, excepto que se ven diferentes cuando se imprimen en la consola. Una diferencia importante es que, como objeto,
new Number(5)acepta nuevas propiedades como cualquier plano{}, mientras que la primitiva5no:En cuanto a la
dis.call(5)parte inicial , consulte ¿Cómo funciona la palabra clave "this"? . Digamos que el primer argumento decallse usa como el valor dethis, y que esta operación fuerza el número a laNumberforma de objeto más compleja . * Más tarde lo++fuerza a volver a la forma primitiva, porque la operación de suma+resulta en una nueva primitiva.Un
Numberobjeto acepta nuevas propiedades.++da como resultado un nuevo6valor primitivo ...... que no tiene y no acepta atributos personalizados.
* Tenga en cuenta que en modo estricto el
thisargumento se trataría de manera diferente y no se convertiría a aNumber. Ver http://es5.github.io/#x10.4.3 para los detalles de implementación.fuente
#define true false. O en Java, donde puedes redefinir qué significan los números. Estos son buenos lenguajes sólidos. En verdad, cada idioma tiene "trucos" similares en los que obtienes un resultado que funciona según lo previsto pero que puede parecer extraño.Hay coerción en el mundo de JavaScript: una historia de detectives
Nathan, no tienes idea de lo que has descubierto.
Llevo semanas investigando esto. Todo comenzó en una noche de tormenta en octubre pasado. Accidentalmente me topé con la
Numberclase. Quiero decir, ¿por qué demonios JavaScript tenía unaNumberclase?No estaba preparado para lo que iba a descubrir a continuación.
Resulta que JavaScript, sin decirte, ha estado cambiando tus números a objetos y tus objetos a números justo debajo de tu nariz.
JavaScript esperaba que nadie se diera cuenta, pero la gente ha estado informando un comportamiento extraño e inesperado, y ahora gracias a ti y a tu pregunta, tengo la evidencia que necesito para revelar esto.
Esto es lo que hemos descubierto hasta ahora. No sé si debería decirte esto, es posible que quieras desactivar tu JavaScript.
Cuando creó esa función, probablemente no tenía idea de lo que sucedería después. Todo se veía bien, y todo estaba bien, por ahora.
No hay mensajes de error, solo la palabra "indefinido" en la salida de la consola, exactamente lo que cabría esperar. Después de todo, esta fue una declaración de función: no se supone que devuelva nada.
Pero esto fue solo el comienzo. Lo que sucedió después, nadie podría haberlo predicho.
Sí, lo sé, esperabas un
5, pero eso no es lo que obtuviste, ¿fue algo diferente?Lo mismo me pasó a mí.
No sabía qué hacer con eso. Me volvió loco. No podía dormir, no podía comer, traté de beberlo, pero ninguna cantidad de Mountain Dew me haría olvidar. ¡Simplemente no tenía ningún sentido!
Fue entonces cuando descubrí lo que realmente estaba sucediendo: fue una coerción, y estaba sucediendo justo en frente de mis ojos, pero estaba demasiado ciego para verlo.
Mozilla intentó enterrarlo poniéndolo donde sabían que nadie miraría: su documentación .
Después de horas de lectura recursiva y relectura y relectura, encontré esto:
Estaba justo ahí, como se puede explicar en la fuente Open Sans. Era la
call()función, ¿cómo podría ser tan estúpido?Mi número ya no era un número en absoluto. En el momento en que lo pasé
call(), se convirtió en otra cosa. Se convirtió en ... un objeto.No podía creerlo al principio. ¿Cómo puede esto ser verdad? Pero no podía ignorar la evidencia que se estaba acumulando a mi alrededor. Está ahí si solo miras:
wtfera correcto. Los números no pueden tener propiedades personalizadas, ¡todos lo sabemos! Es lo primero que te enseñan en la academia.Deberíamos haber sabido el momento en que vimos la salida de la consola; este no era el número que pensábamos que era. Este era un impostor, un objeto que se hacía pasar por nuestro dulce número inocente.
Esto fue ...
new Number(5).¡Por supuesto! Tenía perfecto sentido.
call()tenía un trabajo que hacer, tenía que invocar una función, y para eso necesitaba poblarthis, sabía que no podía hacerlo con un número: necesitaba un objeto y estaba dispuesto a hacer cualquier cosa para obtenerlo, incluso si eso significaba coaccionar nuestro número. Cuandocall()vio el número5, vio una oportunidad.Era el plan perfecto: esperar hasta que nadie estuviera mirando e intercambiar nuestro número por un objeto que se parezca a él. Obtenemos un número, se invoca la función y nadie sería más sabio.
Realmente era el plan perfecto, pero como todos los planes, incluso los perfectos, tenía un agujero y estábamos a punto de caer en él.
Mira, lo
call()que no entendió fue que él no era el único en la ciudad que podía obligar a los números. Esto era JavaScript después de todo, la coerción estaba en todas partes.call()tomé mi número y no iba a parar hasta que le quitara la máscara a su pequeño impostor y lo exponga a toda la comunidad de Stack Overflow.¿Pero cómo? Necesitaba un plan Claro que parece un número, pero sé que no lo es, debe haber una forma de demostrarlo. ¡Eso es! Se ve como un número, pero puede actuar como tal?
Le dije
fiveque necesitaba que fuera 5 veces más grande; no preguntó por qué y no le expliqué. Luego hice lo que cualquier buen programador haría: multipliqué. Seguramente no había forma de que pudiera fingir salir de esto.¡Maldición! No solo se
fivemultiplicó bienwtf, todavía estaba allí. Maldito sea este tipo y su papa.¿Qué demonios está pasando? ¿Estaba equivocado sobre todo esto? ¿Es
fiverealmente un número? No, debo estar perdiendo algo, lo sé, hay algo que debo olvidar, algo tan simple y básico que lo estoy pasando por alto por completo.Esto no se veía bien, había estado escribiendo esta respuesta durante horas y todavía no estaba más cerca de hacer mi punto. No podía seguir así, eventualmente la gente dejaba de leer, tenía que pensar en algo y tenía que pensarlo rápido.
Espera eso es todo!
fiveno fue 25, 25 fue el resultado, 25 fue un número completamente diferente. Por supuesto, ¿cómo podría olvidarlo? Los números son inmutables. Cuando multiplica5 * 5nada, nada se asigna a nada, simplemente crea un nuevo número25.Eso debe ser lo que está sucediendo aquí. De alguna manera, cuando multiplico
five * 5,fivedebe ser forzado a un número y ese número debe ser el que se usa para la multiplicación. Son los resultados de esa multiplicación los que se imprimen en la consola, no el valor defivesí mismo.fivenunca se le asigna nada, por lo que, por supuesto, no cambia.Entonces, ¿cómo puedo
fiveasignarme el resultado de una operación? Lo tengo. Antesfiveincluso de tener la oportunidad de pensar, grité "++".¡Ajá! Lo tuve! Todo el mundo sabe
5 + 1es6, esta era la evidencia que necesitaba para exponer quefiveno era un número! Fue un impostor! Un mal impostor que no sabía contar. Y podría probarlo. Así es como actúa un número real:¿Espere? ¿Qué estaba pasando aquí? suspiro Me quedé tan atrapado en el revés
fiveque olvidé cómo funcionan los operadores de correos. Cuando uso el++al final defiveestoy diciendo que devuelva el valor actual, luego incrementefive. Es el valor antes de que ocurra la operación lo que se imprime en la consola.numfue de hecho6y pude demostrarlo:Hora de ver qué
fiveera realmente:... era exactamente lo que debería ser.
fiveestaba bien, pero estaba mejor. Sifivetodavía fuera un objeto que significaría que todavía tendría la propiedadwtfy estaba dispuesto a apostar todo lo que no.¡Ajá! Yo tenía razón. Lo tuve!
fiveahora era un número, ya no era un objeto. Sabía que el truco de la multiplicación no lo salvaría esta vez. Verfive++es realmentefive = five + 1. A diferencia de la multiplicación, el++operador asigna un valor afive. Más específicamente, le asigna los resultados de losfive + 1cuales, como en el caso de la multiplicación, devuelve un nuevo número inmutable .Sabía que lo tenía, y solo para asegurarme de que no pudiera retorcerse. Tenía una prueba más bajo la manga. Si
fivetuviera razón, y realmente fuera un número ahora, entonces esto no funcionaría:No me iba a engañar esta vez. Sabía que
potato?iba a imprimirse en la consola porque ese es el resultado de la tarea. La verdadera pregunta es, ¿wtfseguirá allí?Tal como sospechaba, nada, porque a los números no se les pueden asignar propiedades. Aprendimos que el primer año en la academia;)
Gracias nathan Gracias a su coraje al hacer esta pregunta, finalmente puedo dejar todo esto atrás y pasar a un nuevo caso.
Como este sobre la función
toValue(). Oh Dios mio. Nooo!fuente
01declara una funcióndisque devuelve el objeto de contexto. Lo quethisrepresenta cambios dependiendo de si está utilizando el modo estricto o no. Todo el ejemplo tiene resultados diferentes si la función se declara como:Esto se detalla en la sección 10.4.3 de la especificación ES5
02es el valor de retorno de la declaración de función.undefineddebería explicarse por sí mismo aquí.03la variablefivese inicializa con el valor de retorno dediscuando se llama en el contexto del valor primitivo5. Comodisno está en modo estricto, esta línea es idéntica a la llamadafive = Object(5).04ElNumber {[[PrimitiveValue]]: 5}valor de retorno impar es la representación del objeto que envuelve el valor primitivo.505A la propiedadfivedel objetowtfse le asigna un valor de cadena de'potato'06es el valor de retorno de la tarea y debe explicarse por sí mismo.07lafivedel objetowtfpropiedad está siendo examinado08comofive.wtfse estableció anteriormente'potato', vuelve'potato'aquí09Elfiveobjeto se multiplica por el valor primitivo5. Esto no es diferente de cualquier otro objeto que se esté multiplicando y se explica en la sección 11.5 de la especificación ES5 . De particular interés es cómo los objetos se convierten en valores numéricos, que se cubre en algunas secciones.9.3 ToNumber :
9.1 ToPrimitive :
8.12.8 [[Valor predeterminado]] :
Esta es una forma indirecta de decir que
valueOfse llama a la función del objeto y que el valor de retorno de esa función se usa en la ecuación. Si tuviera que cambiar lavalueOffunción, podría cambiar los resultados de la operación:10comofivelavalueOffunción s no se modificó, devuelve el valor primitivo envuelto5para quefive * 5evalúe a5 * 5qué resultados2511la propiedadfivedel objetowtfse evalúa nuevamente a pesar de no haberse modificado desde el momento en que se asignó05.12'potato'13el Incremento del operador Postfix se llama elfiveque obtiene el valor numérico (5, cubrimos la forma anteriormente), almacena el valor para que pueda ser devuelto, se suma1al valor (6), asigna el valor afive, y devuelve el valor almacenado (5)14como antes, el valor devuelto es el valor antes de que se incrementara15Se accede a lawtfpropiedad del valor primitivo (6) almacenado en la variablefive. La sección 15.7.5 de la especificación ES5 define este comportamiento. Los números obtienen las propiedades deNumber.prototype.16Number.prototypeno tiene unawtfpropiedad, por lo queundefinedse devuelve17five.wtfse le asigna un valor de'potato?'. La asignación se define en 11.13.1 de la especificación ES5 . Básicamente, el valor asignado se devuelve pero no se almacena.18'potato?'fue devuelto por el operador de asignación19nuevamentefive, que tiene un valor de6se accede, y nuevamenteNumber.prototypeno tiene unawtfpropiedad20undefinedcomo se explicó anteriormente21fivese accede226se devuelve como se explica en13fuente
Es muy simple
Esto devuelve el
thiscontexto. Entonces, si lo hacescall(5), estás pasando el número como un objeto.La
callfunción no proporciona argumentos, el primer argumento que da es el contexto dethis. Por lo general, si lo desea en su contexto, se lo da,{}lodis.call({})que significa quethisen la función está vacíothis. Sin embargo, si pasa5, parece que se convertirá en un objeto. Ver .callEntonces el regreso es
objectCuando lo hace
five * 5, JavaScript ve el objetofivecomo el tipo primitivo, por lo que es equivalente a5 * 5. Curiosamente, hazlo'5' * 5, todavía es igual25, por lo que JavaScript está claramente bajo el capó. Nofivese realizan cambios en el tipo subyacente en esta líneaPero cuando lo haga
++, convertirá el objeto alnumbertipo primitivo , eliminando así la.wtfpropiedad. Porque estás afectando el tipo subyacentefuente
++convierte y lo asigna de nuevo.++es igual avariable = variable + 1. Así que pierdeswtf*vs++no me confunde en absoluto. Tener una función que no espera argumentos y retornathis, tener esa función acepta un argumento de todos modos y devuelve algo que es, pero no exactamente, el argumento, eso no tiene sentido para mí.dis.call()hace.thises simplemente un puntero a un objeto, por lo que los tipos primitivos se convierten implícitamente. ¿Por qué una función sin argumentos no podría tener al menos un contexto? El primer argumento decallobindse utiliza para establecer el contexto. Además, las funciones son cierres, lo que significa que tienen acceso a más que soloargumentsLos valores primitivos no pueden tener propiedad. Pero cuando intentas acceder a una propiedad con valor primitivo, se transcribe de forma transparente a un objeto numérico temporal.
Entonces:
fuente
Declarar función
dis. La función devuelve su contextoLlamar
discon contexto5. Los valores primitivos se encuadran cuando se pasan como contexto en modo estricto ( MDN ). Entoncesfiveahora es objeto (número en caja).Declarar
wtfpropiedad enfivevariableValor de
five.wtffiveestá encuadrado5, por lo que es número y objeto al mismo tiempo (5 * 5 = 25). No cambiafive.Valor de
five.wtfUnboxing
fiveaquí.fiveahora es simplemente primitivonumber. Se imprime5y luego se agrega1afive.fivees un número primitivo6ahora, no tiene propiedades.las primitivas no pueden tener propiedades, no puede establecer esto
no puedes leer esto, porque no estaba configurado
fivese6debe a que la publicación se incrementó arribafuente
En primer lugar, parece que esto se está ejecutando a través de la consola de nodejs.
1)
crea la función dis (), pero debido a que no se configuró como
varno había ningún valor que devolver, también loundefinedfue la salida, aunquedis()se definió. En una nota al margen,thisno se devolvió porque la función no se ejecutó.2)
Esto devuelve Javascript de
Numberobjeto porque se acaba de establecer la función dedis()'sthisvalor a la primitiva cinco.3)
El primero vuelve
"potato"porque acabas de establecer la propiedadwtfdefiveto'potato'. Javascript devuelve el valor de una variable se establece, por lo que es fácil de múltiples variables de la cadena y los puso en el mismo valor como esto:a = b = c = 2.4)
Esto vuelve
25porque acabas de multiplicar el número primitivo5porfive. El valor defivefue determinado por el valor delNumberobjeto.5)
Me salteé esta línea antes porque la repetiría aquí. Simplemente devuelve el valor de la propiedad
wtfque configuró anteriormente.6)
Como dijo @Callum,
++convertirá el tipo alnumbermismo valor del objetoNumber {[[PrimitiveValue]]: 5}}.Ahora, como
fivees unnumber, ya no puede establecer propiedades hasta que haga algo como esto:o
También tenga en cuenta que la segunda forma tendrá un comportamiento diferente al uso del primer método porque está definiendo un objeto genérico en lugar del
Numberobjeto que se creó anteriormente.Espero que esto ayude, a JavaScript le gusta asumir cosas, por lo que puede ser confuso cuando se cambia del
Numberobjeto a primitivonumber. Puede comprobar qué tipo es algo mediante el uso de latypeofpalabra, la escritura typeof cinco después de inicializar vuelve'object', y después de hacerlofive++se vuelve'number'.@deceze describe la diferencia entre el objeto Number y el número primitivo extremadamente bien.
fuente
Los ámbitos de JavaScript están hechos de contextos de ejecución. Cada contexto de ejecución tiene un entorno léxico (valores con ámbito externo / global), un entorno variable (valores con ámbito local) y este enlace .
La presente unión es una parte muy importante del contexto de ejecución. Usar
calles una forma de alterar este enlace , y al hacerlo creará automáticamente un objeto para llenar el enlace.Function.prototype.call () (de MDN)
Una vez que es evidente que 5 se está convirtiendo
new Number(5), el resto debería ser bastante obvio. Tenga en cuenta que otros ejemplos también funcionarán siempre que sean valores primitivos.fuente
Un par de conceptos explican lo que sucede.
5es un número, un valor primitivoNumber {[[PrimitiveValue]]: 5}es una instancia de Number (llamémosla envoltura de objeto)Siempre que acceda a una propiedad / método en un valor primitivo, el motor JS creará un contenedor de objetos del tipo apropiado (
Numberpara5,Stringpara'str'yBooleanparatrue) y resolverá la llamada de acceso a propiedades / método en ese contenedor de objetos. Esto es lo que sucede cuando lo haces,true.toString()por ejemplo.Al realizar operaciones en objetos, se convierten en valores primitivos (usando
toStringovalueOf) para resolver esas operaciones, por ejemplo, al hacerstringmantendrá la concatenación de cadenas demystryobj.toString()ynumbermantendrá la adición de3yobj.valueOf().Ahora para ponerlo todo junto
dis.call(5)se comporta como(5).dis()si5realmente tuviera el métododis. Para resolver la llamada al método, se crea el contenedor de objetos y la llamada al método se resuelve en él. En este punto, cinco puntos a un contenedor de objetos alrededor del valor primitivo 5.Establecer una propiedad en un objeto, nada lujoso aquí.
En realidad, esto es
five.valueOf() * 5obtener el valor primitivo del contenedor de objetos.fiveTodavía apunta al objeto inicial.Esto es en realidad
five = five.valueOf() + 1. Antes de que esta líneafivecontenga el objeto envoltorio alrededor del valor 5, mientras que después de este punto sefivemantiene el valor primitivo6.fiveya no es un objeto. Cada una de esas líneas crea una nueva instancia de Número para resolver el.wtfacceso a la propiedad. Las instancias son independientes, por lo que establecer una propiedad en una no será visible en otra. El código es completamente equivalente a este:fuente