Um tweet recente continha esse trecho de JavaScript.
Alguém pode explicar o que está acontecendo nele passo a passo?
> 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
Em particular, não está claro para mim:
- por que o resultado de
dis.call(5)
é umNumber
com algum tipo de[[PrimitiveValue]]
propriedade, mas os resultados defive++
efive * 5
parecem ser apenas os números simples5
e25
(nãoNumber
s) - por que a
five.wtf
propriedade desaparece após ofive++
incremento - por que a
five.wtf
propriedade não é mais configurável após ofive++
incremento, apesar dafive.wtf = 'potato?'
atribuição aparentemente definir o valor.
javascript
Nathan Long
fonte
fonte
++
parece afetar o tipo subjacentePre
vsPost
incremento? Depois de multiplicação, É novamente umNumber
objeto que não temwtf
propriedade ... Mas ainda é umobject
, portanto, pode terproperties
..dis
comdis.call(5)
, isso envolve o número primitivo5
em um objeto do tipoNumber
que contém o5
, de modo que este objeto pode ser retornado comothis
objeto. O++
converte novamente em um número primitivo que não pode conter propriedades e, portanto,wtf
começa a ser indefinido.Respostas:
OP aqui. Engraçado ver isso no Stack Overflow :)
Antes de avançar no comportamento, é importante esclarecer algumas coisas:
O valor numérico e o objeto numérico (
a = 3
vsa = new Number(3)
) são muito diferentes. Um é um primitivo, o outro é um objeto. Você não pode atribuir atributos a primitivos, mas a objetos.A coerção entre os dois está implícita.
Por exemplo:
Toda expressão tem um valor de retorno. Quando o REPL lê e executa uma expressão, é isso que ela exibe. Os valores de retorno geralmente não significam o que você pensa e implicam coisas que simplesmente não são verdadeiras.
OK, vamos lá.
O penhor.
Defina uma função
dis
e chame -a com5
. Isso executará a função com5
o contexto (this
). Aqui, ele é coagido de um valor Number a um objeto Number. É muito importante observar que, se estivéssemos no modo estrito, isso não teria acontecido .Agora, definimos o atributo
five.wtf
como'potato'
, e com cinco como objeto, com certeza ele aceita a atribuição simples .Com
five
como objeto, garanto que ele ainda pode executar operações aritméticas simples. Pode. Seus atributos ainda permanecem? Sim.A vez.
Agora vamos checar
five++
. O truque com o incremento do postfix é que a expressão inteira será avaliada em relação ao valor original e, em seguida, aumentará o valor. Parece quefive
ainda tem cinco anos, mas na verdade a expressão foi avaliada em cinco e depois definidafive
como6
.Não apenas foi
five
definido6
como foi coagido novamente a um valor Number e todos os atributos foram perdidos. Como os primitivos não podem conter atributos,five.wtf
é indefinido.Tento novamente atribuir um atributo
wtf
afive
. O valor de retorno implica que ele adere, mas na verdade não é porquefive
é um valor Number, não um objeto Number. A expressão é avaliada como'potato?'
, mas quando verificamos, vemos que ela não foi atribuída.O prestígio.
Desde o incremento do postfix,
five
tem sido6
.fonte
int
eInteger
, por exemplo. Suponho que seja para que você possa criar uma funçãodoSomething(Object)
e ainda poder dar a ela primitivas. As primitivas serão convertidas em suas classes correspondentes neste caso. Mas JS realmente não se preocupam com os tipos, então a razão é provavelmente algo mais++
fazer é aplicar ToNumber ao valor . Compare um caso semelhante com seqüências de caracteres: se você tiverx="5"
, entãox++
retorna o número5
.dis.call(5)
, a coerção para objetar, eu nunca esperaria isso.Existem duas maneiras diferentes de representar um número:
O primeiro é um primitivo, o segundo é um objeto. Para todos os efeitos, ambos se comportam da mesma forma, exceto que parecem diferentes quando impressos no console. Uma diferença importante é que, como objeto,
new Number(5)
aceita novas propriedades como qualquer planície{}
, enquanto a primitiva5
não:Quanto à
dis.call(5)
parte inicial , consulte Como funciona a palavra-chave "this"? . Digamos apenas que o primeiro argumento paracall
é usado como o valor dethis
, e que esta operação força o número naNumber
forma de objeto mais complexa . * Posteriormente, o++
força de volta à forma primitiva, porque a operação de adição+
resulta em uma nova primitiva.Um
Number
objeto aceita novas propriedades.++
resulta em um novo6
valor primitivo ...... que não possui e não aceita atributos customizados.
* Observe que, no modo estrito, o
this
argumento seria tratado de maneira diferente e não seria convertido em aNumber
. Consulte http://es5.github.io/#x10.4.3 para obter detalhes da implementação.fonte
#define true false
. Ou em Java, onde você pode redefinir o significado dos números. Essas são boas linguagens sólidas. Na verdade, todos os idiomas têm "truques" semelhantes, nos quais você obtém um resultado que funciona como pretendido, mas pode parecer estranho.Há coerção no mundo do JavaScript - uma história de detetive
Nathan, você não tem ideia do que descobriu.
Estou investigando isso há semanas. Tudo começou em uma noite de tempestade em outubro passado. Eu acidentalmente tropecei na
Number
aula - quero dizer, por que diabos o JavaScript tinha umaNumber
aula?Eu não estava preparado para o que iria descobrir a seguir.
Acontece que o JavaScript, sem dizer a você, alterou seus números para objetos e seus objetos para números logo abaixo do seu nariz.
O JavaScript esperava que ninguém entendesse, mas as pessoas têm relatado um comportamento estranho e inesperado, e agora, graças a você e à sua pergunta, tenho a evidência de que preciso para estourar tudo isso.
Foi isso que descobrimos até agora. Eu não sei se eu deveria estar dizendo isso a você. Você pode desativar o JavaScript.
Quando você criou essa função, provavelmente não tinha ideia do que aconteceria a seguir. Tudo parecia bem, e tudo estava bem - por enquanto.
Nenhuma mensagem de erro, apenas a palavra "indefinido" na saída do console, exatamente o que você esperaria. Afinal, essa era uma declaração de função - não deveria retornar nada.
Mas isso foi apenas o começo. O que aconteceu depois, ninguém poderia ter previsto.
Sim, eu sei, você esperava um
5
, mas não foi isso que você conseguiu, foi - você conseguiu outra coisa - algo diferente.A mesma coisa aconteceu comigo.
Eu não sabia o que fazer disso. Isso me deixou louco. Eu não conseguia dormir, não conseguia comer, tentei beber, mas nenhuma quantidade de Mountain Dew me faria esquecer. Simplesmente não fazia sentido!
Foi quando eu descobri o que realmente estava acontecendo - era coerção, e estava acontecendo bem diante dos meus olhos, mas eu estava cego demais para vê-lo.
A Mozilla tentou enterrá-lo, colocando-o onde eles sabiam que ninguém iria olhar - sua documentação .
Após horas lendo e relendo e relendo recursivamente, encontrei o seguinte:
Estava bem claro, como pode ser escrito na fonte Open Sans. Era a
call()
função - como eu poderia ser tão estúpido ?!Meu número não era mais um número. No momento em que passei
call()
, tornou-se outra coisa. Tornou-se ... um objeto.Eu não pude acreditar no começo. Como isso pode ser verdade? Mas eu não podia ignorar as evidências que estavam se acumulando ao meu redor. Está aí se você apenas olhar:
wtf
estava certo. Os números não podem ter propriedades personalizadas - todos sabemos isso! É a primeira coisa que eles ensinam na academia.Deveríamos saber o momento em que vimos a saída do console - este não era o número que pensávamos. Era um impostor - um objeto que se passava como nosso doce número inocente.
Este foi ...
new Number(5)
.Claro! Fazia todo o sentido.
call()
tinha um trabalho a fazer, tinha que invocar uma função e, para fazer isso, precisava preencherthis
, sabia que não podia fazer isso com um número - precisava de um objeto e estava disposto a fazer qualquer coisa para obtê-lo, até se isso significava coagir nosso número. Quandocall()
viu o número5
, viu uma oportunidade.Era o plano perfeito: espere até que ninguém olhe e troque nosso número por um objeto parecido com ele. Nós obtemos um número, a função é invocada e ninguém seria o mais sábio.
Realmente era o plano perfeito, mas como todos os planos, mesmo os perfeitos, havia um buraco nele, e estávamos prestes a cair nele.
Veja, o
call()
que não entendia era que ele não era o único na cidade que conseguia coagir números. Afinal, isso era JavaScript - coerção estava em toda parte.call()
peguei meu número e não pararia até tirar a máscara de seu pequeno impostor e expô-lo a toda a comunidade Stack Overflow.Mas como? Eu precisava de um plano. Claro que parece um número, mas sei que não, tem que haver uma maneira de provar isso. É isso aí! Ele parece como um número, mas pode agir como um?
Eu disse
five
que precisava que ele ficasse 5 vezes maior - ele não perguntou o porquê e eu não expliquei. Fiz então o que qualquer bom programador faria: eu me multipliquei. Certamente não havia como ele conseguir escapar disso.Droga! Não só
five
multiplicar muito bemwtf
ainda estava lá. Maldito esse cara e sua batata.Que diabos estava acontecendo? Eu estava errado sobre essa coisa toda? É
five
realmente um número? Não, devo estar perdendo alguma coisa, eu sei, há algo que devo estar esquecendo, algo tão simples e básico que estou completamente ignorando.Isso não parecia bom, eu estava escrevendo essa resposta por horas e ainda não estava mais perto de fazer o meu argumento. Eu não conseguia continuar, eventualmente as pessoas paravam de ler, eu tinha que pensar em alguma coisa e tinha que pensar rápido.
Espere, é isso!
five
não foi 25, 25 foi o resultado, 25 foi um número completamente diferente. Claro, como eu poderia esquecer? Os números são imutáveis. Quando você multiplica,5 * 5
nada é atribuído a nada, basta criar um novo número25
.Deve ser o que está acontecendo aqui. De alguma forma, quando eu multiplico
five * 5
,five
deve estar sendo coagido a um número e esse número deve ser o usado para a multiplicação. São os resultados dessa multiplicação impressa no console, não o valor emfive
si.five
nunca recebe nada - então é claro que isso não muda.Então, como faço para me
five
atribuir o resultado de uma operação. Deixa comigo. Antesfive
mesmo de ter a chance de pensar, eu gritei "++".Aha! Eu o peguei! Todo mundo sabe que
5 + 1
é6
, essa era a evidência que eu precisava para expor quefive
não era um número! Foi um impostor! Um impostor ruim que não sabia contar. E eu pude provar isso. Veja como um número real age:Esperar? O que estava acontecendo aqui? suspiro , fiquei tão preso em flagras
five
que esqueço como os operadores de correios funcionam. Quando eu uso o++
no final dofive
que estou dizendo, retorne o valor atual e depois aumentefive
. É o valor antes da operação que é impresso no console.num
foi de fato6
e eu pude provar:Hora de ver o que
five
realmente era:... era exatamente o que deveria ser.
five
foi bom - mas eu estava melhor. Sefive
ainda fosse um objeto, isso significaria que ainda teria a propriedadewtf
e eu estava disposto a apostar tudo que não tinha.Aha! Eu tinha razão. Eu o peguei!
five
era um número agora - não era mais um objeto. Eu sabia que o truque da multiplicação não salvaria desta vez. Verfive++
é realmentefive = five + 1
. Diferentemente da multiplicação, o++
operador atribui um valor afive
. Mais especificamente, atribui a ele os resultados dosfive + 1
quais, como no caso da multiplicação, retorna um novo número imutável .Eu sabia que o tinha, e apenas para ter certeza de que ele não conseguiria se esquivar disso. Eu tinha mais um teste na manga. Se eu estivesse certo e
five
realmente fosse um número agora, isso não funcionaria:Ele não ia me enganar dessa vez. Eu sabia
potato?
que seria impresso no console porque isso é resultado da tarefa. A verdadeira questão é:wtf
ainda estará lá?Assim como eu suspeitava - nada - porque números não podem ser atribuídos a propriedades. Aprendemos que no primeiro ano na academia;)
Obrigado Nathan. Graças à sua coragem em fazer esta pergunta, finalmente posso deixar tudo isso para trás e seguir para um novo caso.
Como este sobre a função
toValue()
. Oh meu Deus. Nããão!fonte
01
declara uma funçãodis
que retorna o objeto de contexto. O quethis
representa as mudanças dependendo de você estar usando o modo estrito ou não. O exemplo inteiro tem resultados diferentes se a função foi declarada como:Isso está detalhado na seção 10.4.3 na especificação ES5
02
é o valor de retorno da declaração da função.undefined
deve ser auto-explicativo aqui.03
a variávelfive
é inicializada com o valor de retorno dedis
quando chamado no contexto do valor primitivo5
. Comodis
não está no modo estrito, essa linha é idêntica à chamadafive = Object(5)
.04
ONumber {[[PrimitiveValue]]: 5}
valor de retorno ímpar é a representação do objeto que envolve o valor primitivo5
05
ofive
objetowtf
de propriedade é atribuído um valor de seqüência de'potato'
06
é o valor de retorno da atribuição e deve ser auto-explicativo.07
a propriedadefive
do objetowtf
está sendo examinada08
comofive.wtf
foi definido anteriormente'potato'
, retorna'potato'
aqui09
ofive
objeto está sendo multiplicado pelo valor primitivo5
. Isso não é diferente de qualquer outro objeto que está sendo multiplicado e é explicado na seção 11.5 da especificação ES5 . É importante notar como os objetos são convertidos em valores numéricos, abordados em algumas seções.9.3 ToNumber :
9.1 ToPrimitive :
8.12.8 [[DefaultValue]] :
Essa é uma maneira indireta de dizer que a
valueOf
função do objeto é chamada e o valor de retorno dessa função é usado na equação. Se você alterar avalueOf
função, poderá alterar os resultados da operação:10
Comofive
avalueOf
função s não foi alterada, ele retorna o valor primitivo encapsulado, de5
modo quefive * 5
avalie a5 * 5
qual resultado25
11
a propriedadefive
do objetowtf
é avaliada novamente, apesar de não ter sido alterada desde quando foi atribuída05
.12
'potato'
13
é chamado o Operador de incremento do Postfixfive
, que obtém o valor numérico (5
abordamos anteriormente), armazena o valor para que ele possa ser retornado, adiciona1
ao valor (6
), atribui o valorfive
e retorna o valor armazenado (5
)14
como antes, o valor retornado é o valor anterior ao incremento15
awtf
propriedade do valor primitivo (6
) armazenado na variávelfive
é acessada. A seção 15.7.5 da especificação ES5 define esse comportamento. Os números obtêm as propriedadesNumber.prototype
.16
Number.prototype
não tem umawtf
propriedade, entãoundefined
é retornado17
five.wtf
é atribuído um valor de'potato?'
. A atribuição é definida em 11.13.1 da especificação ES5 . Basicamente, o valor atribuído é retornado, mas não armazenado.18
'potato?'
foi devolvido pelo operador de atribuição19
novamentefive
, cujo valor6
é acessado, e novamenteNumber.prototype
não possui umawtf
propriedade20
undefined
como explicado acima21
five
é acessado22
6
é retornado como explicado em13
fonte
É bem simples.
Isso retorna o
this
contexto. Então, se vocêcall(5)
está passando o número como um objeto.A
call
função não fornece argumentos, o primeiro argumento que você fornece é o contexto dethis
. Normalmente, se você quer que nele está no contexto, você dá-lo{}
de mododis.call({})
que os meiosthis
na função é um vaziothis
. No entanto, se você passar5
, parece que será convertido em um objeto. Consulte .callEntão o retorno é
object
Quando você faz isso
five * 5
, o JavaScript vê o objetofive
como o tipo primitivo, portanto é equivalente a5 * 5
. Curiosamente,'5' * 5
ainda é igual25
, então o JavaScript está claramente sendo transmitido. Nenhuma alteração nofive
tipo subjacente é feita nesta linhaMas quando você faz
++
isso, ele converte o objeto nonumber
tipo primitivo , removendo a.wtf
propriedade. Porque você está afetando o tipo subjacentefonte
++
converte e atribui de volta.++
é igual avariable = variable + 1
. Então você perdewtf
*
vs++
não me confunde. Ter uma função que não espera argumentos e retornosthis
; ter essa função aceita um argumento de qualquer maneira e retorna algo que é como, mas não é exatamente, o argumento - que não faz sentido para mim.dis.call()
faz.this
é simplesmente um ponteiro para um objeto, para que tipos primitivos sejam convertidos implicitamente. Por que uma função sem argumentos não pode ter pelo menos um contexto? O primeiro argumento decall
oubind
é usado para definir o contexto. Além disso, as funções são fechamentos que significa que têm acesso a mais do que apenasarguments
Os valores primitivos não podem ter propriedade. Mas quando você tenta acessar uma propriedade com valor primitivo, ela é alterada de forma transparente para um objeto Number temporário.
Assim:
fonte
Declarar função
dis
. Função retorna seu contextoLigue
dis
com contexto5
. Os valores primitivos são colocados na caixa quando passados como contexto no modo estrito ( MDN ). Entãofive
agora é objeto (número da caixa).Declarar
wtf
propriedade nafive
variávelValor de
five.wtf
five
está na caixa5
, portanto, é número e objeto ao mesmo tempo (5 * 5 = 25). Isso não mudafive
.Valor de
five.wtf
Desmarque
five
aqui.five
agora é apenas primitivonumber
. Imprime5
e depois adiciona1
afive
.five
é um número primitivo6
agora, não há propriedades nele.primitivas não podem ter propriedades, você não pode definir isso
você não pode ler isso, porque não foi definido
five
é6
por causa do incremento pós acimafonte
Primeiro, parece que isso está sendo executado no console do nodejs.
1
cria a função dis (), mas como ela não foi definida como
var
não havia valor a ser retornado,undefined
o resultado foi a saída, mesmo que tenhadis()
sido definida. Em uma nota lateral,this
não foi retornada porque a função não foi executada.2)
Isso retorna de javascript
Number
objeto porque você apenas definir a funçãodis()
dethis
valor para os cinco primitivo.3)
O primeiro retorna
"potato"
porque você acabou de definir a propriedadewtf
defive
como'potato'
. Javascript retorna o valor de uma variável que você definir, tornando mais fácil para variáveis múltiplas cadeias e configurá-los para o mesmo valor como este:a = b = c = 2
.4)
Isso retorna
25
porque você acabou de multiplicar o número primitivo5
parafive
. O valor defive
foi determinado pelo valor doNumber
objeto.5)
Eu pulei essa linha antes porque repetiria aqui. Apenas retorna o valor da propriedade
wtf
que você definiu acima.6
Como o @Callum disse,
++
converterá o tipo paranumber
o mesmo valor do objetoNumber {[[PrimitiveValue]]: 5}}
.Agora, como
five
é umnumber
, você não pode mais definir propriedades para ele até fazer algo assim:ou
Observe também que a segunda maneira terá um comportamento diferente do uso do primeiro método, pois está definindo um objeto genérico em vez do
Number
objeto criado anteriormente.Espero que isso ajude, o javascript gosta de assumir as coisas, para que fique confuso ao mudar do
Number
objeto para um primitivonumber
. Você pode verificar qual é o tipo de algo usando atypeof
palavra - chave, escrevendo um tipo de cinco depois de inicializá-lo'object'
e, em seguidafive++
, retornar'number'
.@deceze descreve muito bem a diferença entre o objeto Number e o número primitivo.
fonte
Os escopos JavaScript são feitos de contextos de execução. Cada Contexto de Execução possui um Ambiente Lexical (valores com escopo externo / globalmente), um Ambiente Variável (valores com escopo local) e essa ligação .
A ligação this é uma parte muito importante do contexto de execução. Usar
call
é uma maneira de alterar essa ligação , e isso criará automaticamente um objeto para preencher a ligação.Function.prototype.call () (do MDN)
Uma vez que é evidente que 5 está sendo convertido em
new Number(5)
, o resto deve ser bastante óbvio. Observe que outros exemplos também funcionarão desde que sejam valores primitivos.fonte
Alguns conceitos explicam o que acontece
5
é um número, um valor primitivoNumber {[[PrimitiveValue]]: 5}
é uma instância de Number (vamos chamá-lo de invólucro de objeto)Sempre que você acessar uma propriedade / método em um valor primitivo, o mecanismo JS criará um wrapper de objeto do tipo apropriado (
Number
para5
,String
para'str'
eBoolean
paratrue
) e resolverá a chamada de acesso / método de propriedade nesse wrapper de objeto. É o que acontece quando você faz,true.toString()
por exemplo.Ao executar operações em objetos, elas são convertidas em valores primitivos (usando
toString
ouvalueOf
) para resolver essas operações - por exemplo, ao executarstring
vai realizar a concatenação demystr
eobj.toString()
enumber
vai realizar a adição de3
eobj.valueOf()
.Agora, para juntar tudo
dis.call(5)
se comporta exatamente como(5).dis()
se5
realmente tivesse o métododis
. Para resolver a chamada do método, o wrapper de objeto é criado e a chamada do método é resolvida nele. Nesse ponto, cinco pontos para um invólucro de objeto em torno do valor primitivo 5.Definir uma propriedade em um objeto, nada sofisticado aqui.
Na verdade, está
five.valueOf() * 5
obtendo o valor primitivo do wrapper de objeto.five
ainda aponta para o objeto inicial.Isto é realmente
five = five.valueOf() + 1
. Antes dessa linha,five
o wrapper do objeto fica em torno do valor 5, enquanto após esse ponto,five
o valor primitivo é mantido6
.five
não é mais um objeto. Cada uma dessas linhas cria uma nova instância de Number para resolver o.wtf
acesso à propriedade. As instâncias são independentes, portanto, definir uma propriedade em uma não será visível em outra. O código é completamente equivalente a este:fonte