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 unNumber
con algún tipo de[[PrimitiveValue]]
propiedad, pero los resultados defive++
yfive * 5
parecen ser solo los números simples5
y25
(noNumber
s) - por qué la
five.wtf
propiedad desaparece después delfive++
incremento - por qué la
five.wtf
propiedad 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 subyacentePre
vsPost
incremento? Después de la multiplicación, nuevamente es unNumber
objeto que no tienewtf
propiedad ... Pero aún así es un objetoobject
que puede tenerproperties
...dis
condis.call(5)
, esto envuelve el número primitivo5
en un objeto de tipoNumber
que contiene el5
, para que este objeto pueda ser devuelto comothis
objeto. La++
convierte de nuevo a un número primitivo que no puede contener propiedades, por lo quewtf
comienza 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 = 3
vsa = 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
dis
y llamarla con5
. Esto ejecutará la función con5
el 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.wtf
en'potato'
, y con cinco como objeto, efectivamente acepta la Asignación simple .Con
five
como 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 quefive
todavía son cinco, pero realmente la expresión se evaluó en cinco, luego se estableciófive
en6
.No solo
five
se 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.wtf
no está definido.De nuevo intento reasignar un atributo
wtf
afive
. El valor de retorno implica que se mantiene, pero de hecho no lo hace porquefive
es 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,
five
ha sido6
.fuente
int
yInteger
, 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 primitiva5
no:En cuanto a la
dis.call(5)
parte inicial , consulte ¿Cómo funciona la palabra clave "this"? . Digamos que el primer argumento decall
se usa como el valor dethis
, y que esta operación fuerza el número a laNumber
forma 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
Number
objeto acepta nuevas propiedades.++
da como resultado un nuevo6
valor primitivo ...... que no tiene y no acepta atributos personalizados.
* Tenga en cuenta que en modo estricto el
this
argumento 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
Number
clase. Quiero decir, ¿por qué demonios JavaScript tenía unaNumber
clase?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:
wtf
era 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
five
que 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
five
multiplicó bienwtf
, todavía estaba allí. Maldito sea este tipo y su papa.¿Qué demonios está pasando? ¿Estaba equivocado sobre todo esto? ¿Es
five
realmente 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!
five
no 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 * 5
nada, 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
,five
debe 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 defive
sí mismo.five
nunca se le asigna nada, por lo que, por supuesto, no cambia.Entonces, ¿cómo puedo
five
asignarme el resultado de una operación? Lo tengo. Antesfive
incluso de tener la oportunidad de pensar, grité "++".¡Ajá! Lo tuve! Todo el mundo sabe
5 + 1
es6
, esta era la evidencia que necesitaba para exponer quefive
no 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
five
que olvidé cómo funcionan los operadores de correos. Cuando uso el++
al final defive
estoy 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.num
fue de hecho6
y pude demostrarlo:Hora de ver qué
five
era realmente:... era exactamente lo que debería ser.
five
estaba bien, pero estaba mejor. Sifive
todavía fuera un objeto que significaría que todavía tendría la propiedadwtf
y estaba dispuesto a apostar todo lo que no.¡Ajá! Yo tenía razón. Lo tuve!
five
ahora 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 + 1
cuales, 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
five
tuviera 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, ¿wtf
seguirá 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
01
declara una funcióndis
que devuelve el objeto de contexto. Lo quethis
representa 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
02
es el valor de retorno de la declaración de función.undefined
debería explicarse por sí mismo aquí.03
la variablefive
se inicializa con el valor de retorno dedis
cuando se llama en el contexto del valor primitivo5
. Comodis
no está en modo estricto, esta línea es idéntica a la llamadafive = Object(5)
.04
ElNumber {[[PrimitiveValue]]: 5}
valor de retorno impar es la representación del objeto que envuelve el valor primitivo.5
05
A la propiedadfive
del objetowtf
se le asigna un valor de cadena de'potato'
06
es el valor de retorno de la tarea y debe explicarse por sí mismo.07
lafive
del objetowtf
propiedad está siendo examinado08
comofive.wtf
se estableció anteriormente'potato'
, vuelve'potato'
aquí09
Elfive
objeto 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
valueOf
se 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 lavalueOf
función, podría cambiar los resultados de la operación:10
comofive
lavalueOf
función s no se modificó, devuelve el valor primitivo envuelto5
para quefive * 5
evalúe a5 * 5
qué resultados25
11
la propiedadfive
del objetowtf
se evalúa nuevamente a pesar de no haberse modificado desde el momento en que se asignó05
.12
'potato'
13
el Incremento del operador Postfix se llama elfive
que obtiene el valor numérico (5
, cubrimos la forma anteriormente), almacena el valor para que pueda ser devuelto, se suma1
al valor (6
), asigna el valor afive
, y devuelve el valor almacenado (5
)14
como antes, el valor devuelto es el valor antes de que se incrementara15
Se accede a lawtf
propiedad 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
.16
Number.prototype
no tiene unawtf
propiedad, por lo queundefined
se devuelve17
five.wtf
se 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ón19
nuevamentefive
, que tiene un valor de6
se accede, y nuevamenteNumber.prototype
no tiene unawtf
propiedad20
undefined
como se explicó anteriormente21
five
se accede22
6
se devuelve como se explica en13
fuente
Es muy simple
Esto devuelve el
this
contexto. Entonces, si lo hacescall(5)
, estás pasando el número como un objeto.La
call
funció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 quethis
en la función está vacíothis
. Sin embargo, si pasa5
, parece que se convertirá en un objeto. Ver .callEntonces el regreso es
object
Cuando lo hace
five * 5
, JavaScript ve el objetofive
como 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ó. Nofive
se realizan cambios en el tipo subyacente en esta líneaPero cuando lo haga
++
, convertirá el objeto alnumber
tipo primitivo , eliminando así la.wtf
propiedad. 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.this
es 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 decall
obind
se utiliza para establecer el contexto. Además, las funciones son cierres, lo que significa que tienen acceso a más que soloarguments
Los 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
dis
con contexto5
. Los valores primitivos se encuadran cuando se pasan como contexto en modo estricto ( MDN ). Entoncesfive
ahora es objeto (número en caja).Declarar
wtf
propiedad enfive
variableValor de
five.wtf
five
está encuadrado5
, por lo que es número y objeto al mismo tiempo (5 * 5 = 25). No cambiafive
.Valor de
five.wtf
Unboxing
five
aquí.five
ahora es simplemente primitivonumber
. Se imprime5
y luego se agrega1
afive
.five
es un número primitivo6
ahora, no tiene propiedades.las primitivas no pueden tener propiedades, no puede establecer esto
no puedes leer esto, porque no estaba configurado
five
se6
debe 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
var
no había ningún valor que devolver, también loundefined
fue la salida, aunquedis()
se definió. En una nota al margen,this
no se devolvió porque la función no se ejecutó.2)
Esto devuelve Javascript de
Number
objeto porque se acaba de establecer la función dedis()
'sthis
valor a la primitiva cinco.3)
El primero vuelve
"potato"
porque acabas de establecer la propiedadwtf
defive
to'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
25
porque acabas de multiplicar el número primitivo5
porfive
. El valor defive
fue determinado por el valor delNumber
objeto.5)
Me salteé esta línea antes porque la repetiría aquí. Simplemente devuelve el valor de la propiedad
wtf
que configuró anteriormente.6)
Como dijo @Callum,
++
convertirá el tipo alnumber
mismo valor del objetoNumber {[[PrimitiveValue]]: 5}}
.Ahora, como
five
es 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
Number
objeto que se creó anteriormente.Espero que esto ayude, a JavaScript le gusta asumir cosas, por lo que puede ser confuso cuando se cambia del
Number
objeto a primitivonumber
. Puede comprobar qué tipo es algo mediante el uso de latypeof
palabra, 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
call
es 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.
5
es 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 (
Number
para5
,String
para'str'
yBoolean
paratrue
) 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
toString
ovalueOf
) para resolver esas operaciones, por ejemplo, al hacerstring
mantendrá la concatenación de cadenas demystr
yobj.toString()
ynumber
mantendrá la adición de3
yobj.valueOf()
.Ahora para ponerlo todo junto
dis.call(5)
se comporta como(5).dis()
si5
realmente 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() * 5
obtener el valor primitivo del contenedor de objetos.five
Todavía apunta al objeto inicial.Esto es en realidad
five = five.valueOf() + 1
. Antes de que esta líneafive
contenga el objeto envoltorio alrededor del valor 5, mientras que después de este punto sefive
mantiene el valor primitivo6
.five
ya no es un objeto. Cada una de esas líneas crea una nueva instancia de Número para resolver el.wtf
acceso 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