Parece-me que a reação a tal pergunta em uma entrevista depende do que exatamente é. Se a questão realmente for qual é o valor
tree
, o código pode simplesmente ser inserido no console e obter o resultado.
No entanto, se a questão é como você resolveria esse problema, então tudo se torna bastante curioso e leva a um teste de conhecimento dos meandros do JavaScript e do compilador. Neste artigo, tentarei resolver toda essa confusão e obter conclusões interessantes.
Eu estava transmitindo o processo para resolver esse problema no Twitch . A transmissão é longa, mas permite que você dê uma outra olhada no processo passo a passo de solução desses problemas.
Raciocínio geral
Primeiro, vamos converter o código para o formato copiável:
let b = 3, d = b, u = b;
const tree = ++d * d*b * b++ +
+ --d+ + +b-- +
+ +d*b+ +
u
Eu imediatamente percebi algumas peculiaridades e decidi que alguns truques do compilador poderiam ser usados aqui. Veja, JavaScript geralmente adiciona ponto-e-vírgula no final de cada linha, a menos que haja uma expressão que não possa ser interrompida . Neste caso,
+
ao final de cada linha ele informa ao compilador que não há necessidade de interromper a construção.
A primeira linha simplesmente cria três variáveis e atribui um valor a elas
3
.
3
É um valor primitivo, então toda vez que uma cópia é criada, ela é criada por valor , então todas as novas variáveis são criadas com um valor
3
... Se o JavaScript atribuísse valores a essas variáveis por referência , cada nova variável apontaria para a variável usada anteriormente, mas não criaria um valor para si mesma.
informação adicional
Precedência e associatividade do operador
Esses são os conceitos-chave para resolver essa tarefa assustadora. Resumindo, eles definem a ordem em que uma combinação de expressões JavaScript é avaliada.
Prioridade do operador
P: qual é a diferença entre essas duas expressões?
3 + 5 * 5
5 * 5 + 3
Do ponto de vista do resultado, não há diferença. Qualquer pessoa que se lembra das aulas de matemática da escola sabe que a multiplicação é feita antes da adição. Em inglês, lembramos a ordem como BODMAS (colchetes Off Divide Multiply Add Subtract - colchetes, grau, divisão, multiplicação, adição, subtração). JavaScript tem um conceito semelhante chamado precedência de operador: significa a ordem em que avaliamos as expressões. Se quiséssemos primeiro forçar a computação
3 + 5
, faríamos o seguinte:
(3+5) * 5
Os parênteses forçam esta parte da expressão a ser avaliada primeiro, porque o operador tem uma precedência
()
mais alta do que o operador
*
.
Cada operador JavaScript tem precedência, portanto, com tantos operadores
tree
nele, precisamos descobrir em que ordem eles serão avaliados. É especialmente importante o
--
que mudará os valores
b
e
d
, portanto, precisamos saber quando essas expressões são avaliadas em relação ao resto
tree
.
Importante: Tabela de prioridades do operador e informações adicionais
Associatividade
A associatividade é usada para determinar em que ordem as expressões são avaliadas em operadores com igual precedência. Por exemplo:
a + b + c
Não há precedência de operador nesta expressão porque há apenas um operador. Então, como deve ser calculado - como
(a + b) + c
ou como
a + (b + c)
?
Eu sei que o resultado será o mesmo, mas o compilador precisa saber disso para que possa selecionar uma operação primeiro e depois continuar o cálculo. Nesse caso, a resposta correta é
(a + b) + c
porque o operador é
+
associativo à esquerda, ou seja, avalia primeiro a expressão da esquerda.
“Por que não tornar todos os operadores associativos à esquerda?” Você pode perguntar.
Bem, vamos dar um exemplo como este:
a = b + c
Se usarmos a fórmula de associatividade à esquerda, obtemos
(a = b) + c
Mas espere, isso parece estranho, e não foi isso que eu quis dizer. Se quisermos que essa expressão funcione usando apenas associatividade à esquerda, teríamos que fazer algo assim:
a + b = c
Isso é convertido em
(a + b) = c
, ou seja, primeiro
a + b
e, em seguida, o valor desse resultado é atribuído à variável
c
.
Se tivéssemos que pensar dessa maneira, JavaScript seria muito mais confuso, é por isso que usamos diferentes associatividades para diferentes operadores - isso torna o código mais legível. Quando lemos
a = b + c
, a ordem de cálculo parece natural para nós, apesar do fato de que tudo é mais habilmente organizado internamente e usa operandos associativos à direita e à esquerda.
Você provavelmente notou o problema de associatividade em
a = b + c
... Se os dois operadores têm associatividade diferente, como você sabe qual expressão avaliar primeiro? Resposta: aquele com a precedência de operador mais alta , como na seção anterior! Nesse caso, ele
+
tem uma prioridade mais alta, portanto, é calculado primeiro.
Eu adicionei uma explicação mais detalhada no final do artigo, ou você pode ler mais informações .
Compreender como a nossa expressão de árvore é avaliada
Tendo compreendido esses princípios, podemos começar a analisar nosso problema. Ele usa muitos operadores e a ausência de parênteses torna-o difícil de entender. Portanto, vamos apenas adicionar parênteses, listando todos os operadores usados junto com sua precedência e associatividade.
| (operador com variável x): | uma prioridade | associatividade |
| x ++: | 18 | não |
| x--: | 18 | não |
| ++ x: | 17 | certo |
| --x: | 17 | certo |
| + x: | 17 | certo |
| *: | quinze | esquerda |
| x + y: | 14 | esquerda |
| =: | 3 | certo |
Parênteses
Vale a pena mencionar aqui que adicionar parênteses corretamente é uma tarefa complicada. Verifiquei se a resposta é calculada corretamente em cada etapa, mas isso não garante que meus parênteses estejam sempre colocados corretamente! Se você conhece uma ferramenta para colocação automática de cinta, envie-me um e-mail.
Vamos descobrir a ordem em que as expressões são avaliadas e adicionar parênteses para mostrá-lo. Vou mostrar passo a passo como cheguei ao resultado final, apenas passando dos operadores de maior prioridade para baixo.
Postfix ++ e postfix -
const tree = ++d * d*b * (b++) +
+ --d+ + +(b--) +
+ +d*b+ +
u
Unário +, prefixo ++ e prefixo -
Temos um pequeno problema aqui, mas começarei avaliando o operador unário
+
e, em seguida, chegaremos ao ponto do problema.
const tree = ++d * d*b * (b++) +
+ --d+ (+(+(b--))) +
(+(+(d*b+ (+
u))))
E é aqui que surgem as dificuldades.
+ --d+
--
e
+()
têm a mesma prioridade. Como sabemos em que ordem devemos calculá-los? Vamos formular o problema de uma maneira mais simples:
let d = 10
const answer = + --d
Lembre-se de que
+
isso não é adição, mas adição unária ou positividade. Você pode percebê-lo como
-1
, apenas aqui está
+1
.
A solução é que avaliamos da direita para a esquerda, porque os operadores dessa precedência são associativos à direita .
Portanto, nossa expressão é convertida em
+ (--d)
.
Para entender isso, tente imaginar que todos os operadores são iguais. Nesse caso,
+ +1
será equivalente de
(+ (+1))
acordo com a lógica, que
1 — 1 — 1
equivale a
((1 — 1) — 1)
... Observe que o resultado dos operadores associativos à direita na notação com parênteses é o oposto do caso com os operadores à esquerda?
Se aplicarmos a mesma lógica ao ponto do problema, obteremos o seguinte:
const tree = ++d * d*b * (b++) +
(+ (--d)) + (+(+(b--))) +
(+(+(d*b+ (+
u))))
E, finalmente, inserindo os parênteses para o último
++
, obtemos:
const tree = (++d) * d*b * (b++) +
(+ (--d)) + (+(+(b--))) +
(+(+(d*b+ (+
u))))
Multiplicação (*)
Novamente, temos que lidar com a associatividade, mas desta vez com o mesmo operador, que é associativo à esquerda. Em comparação com a etapa anterior, isso deve ser fácil!
const tree = ((((++d) * d) * b) * (b++)) +
(+ (--d)) + (+(+(b--))) +
(+(+((d*b) + (+u))))
Chegamos a um estágio em que já é possível iniciar os cálculos. Seria possível adicionar parênteses para o operador de atribuição, mas acho que será mais confuso do que fácil de ler, portanto, não faremos isso. Observe que a expressão acima é um pouco mais complicada
x = a + b + c
.
Podemos encurtar alguns dos operadores unários, mas irei salvá-los caso sejam importantes.
Ao dividir a expressão em várias partes, podemos compreender os estágios individuais dos cálculos e desenvolvê-los.
let b = 3, d = b, u = b;
const treeA = ((((++d) * d) * b) * (b++))
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
Feito isso, podemos começar a explorar o cálculo de diferentes valores. Vamos começar com treeA.
TreeA
let b = 3, d = b, u = b;
const treeA = (((++d) * d) * b) * (b++)
A primeira coisa que será avaliada aqui é uma expressão
++d
que retornará
4
e incrementará
d
.
// b = 3
// d = 4
((4 * d) * b) * (b++)
Então é executado
4*d
: sabemos que neste estágio d é 4, portanto
4*4
é 16.
// b = 3
// d = 4
(16 * b) * (b++)
O interessante sobre essa etapa é que vamos multiplicar por b antes de incrementar b, então o cálculo é feito da esquerda para a direita.
16 * 3 = 48
...
// b = 3
// d = 4
48 * (b++)
Acima, falamos sobre o que
++
tem uma prioridade maior do que
*
, então isso pode ser escrito como
48 * b++
, mas há outros truques aqui - o valor de retorno
b++
é o valor antes do incremento, não depois. Portanto, mesmo que b eventualmente se torne 4, o valor multiplicado será 3.
// b = 3
// d = 4
48 * 3
// b = 4
// d = 4
48 * 3
é igual
144
, então após calcular a primeira parte b e d são iguais a 4, e o resultado da expressão é
144
let b = 4, d = 4, u = 3;
const treeA = 144
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
TreeB
const treeB = (+ (--d)) + (+(+(b--)))
Nesse ponto, podemos ver que os operadores unários não fazem nada. Se os encurtarmos, simplificaremos muito a expressão.
// b = 4
// d = 4
const treeB = (--d) + (b--)
Já vimos esse truque acima.
--d
retorna
3
, mas
b--
retorna
4
, mas no momento em que a expressão é avaliada, ambos receberão o valor 3.
const treeB = 3 + 4
// b = 3
// d = 3
Portanto, agora nossa tarefa se parece com isto:
let b = 3, d= 3, u = 3;
const treeA = 144
const treeB = 7
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
TreeC
E estamos quase terminando!
// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + (+u))))
Vamos nos livrar desses irritantes operadores unários primeiro.
// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + u)))
Nós nos livramos dele, mas aqui você precisa ter cuidado com os colchetes, etc.
// b = 3
// d = 3
// u = 3
const treeC = (d*b) + u
É muito simples agora.
3 * 3
igual
9
,
9 + 3
igual
12
e, finalmente, temos ...
Responda!
let b = 3, d= 3, u = 3;
const treeA = 144
const treeB = 7
const treeC = 12
const tree = treeA + treeB + treeC
144 + 7 + 12
igual
163
. A resposta para o problema:
163
.
Conclusão
JavaScript pode confundir você de várias maneiras estranhas e deliciosas. Mas, ao compreender como a linguagem funciona, você pode chegar à razão mais fundamental para isso.
De modo geral, o caminho para uma solução pode ser mais informativo do que a resposta, e as mini-soluções encontradas ao longo do caminho podem nos ensinar algo por si mesmas.
Vale a pena dizer que verifiquei meu trabalho usando o console do navegador e foi mais interessante para mim fazer a engenharia reversa da solução do que resolver o problema com base nos princípios básicos.
Mesmo que você saiba como resolver um problema, existem muitas ambigüidades sintáticas que precisam ser tratadas ao longo do caminho. E tenho certeza que muitos de vocês notaram quando olharam para a expressão de nossa árvore. Listei alguns deles abaixo, mas cada um vale um artigo separado!
Também gostaria de agradecer https://twitter.com/AnthonyPAlicea, sem o curso do qual eu nunca teria sido capaz de descobrir tudo, e https://twitter.com/tlakomy por esta pergunta.
Notas e esquisitices
Eu destaquei os mini-enigmas que encontrei ao longo do caminho em uma seção separada para que o processo de encontrar uma solução permaneça transparente.
Como mudar a ordem das variáveis afeta
Veja este video
let x = 10
console.log(x++ + x)
Várias perguntas podem ser feitas aqui. O que será impresso no console e qual é o valor
x
na segunda linha?
Se você acha que esse é o mesmo número, desculpe-me, eu enganei você. O truque é o que é
x++ + x
calculado como
(x++) + x
, e quando o mecanismo JavaScript calcula o lado esquerdo
(x++)
, ele faz o incremento
x
, portanto, quando se trata de
+ x
, o valor de x é igual
11
, não
10
.
Outra pergunta complicada - que valor
x++
ele retorna ?
Eu dei uma pista bastante óbvia sobre qual é a resposta realmente
10
.
Esta é a diferença entre
x++
e
++x
. Se olharmos para as funções subjacentes dos operadores, eles se parecem com isto:
function ++x(x) {
const oldValue = x;
x = x + 1;
return oldValue;
}
function x++(x) {
x = x + 1;
return x
}
Olhando para eles desta forma, podemos entender que
let x = 10
console.log(x++ + x)
significará o que
x++
retorna
10
e, no momento da avaliação,
+ x
seu valor é
11
. Portanto, ele será impresso no console
21
e o valor x será igual a
11
.
Essa tarefa relativamente simples aponta para um antipadrão comum usado em todo o código - expressões confusas e efeitos colaterais . Mais detalhes.
Poderia haver dois operadores com a mesma precedência, mas associatividades diferentes?
Vamos mover em ordem e esquecer a palavra "associatividade" por enquanto.
Vamos pegar os operadores
+
e
=
, e resumir a situação.
Foi mostrado acima o que é
a + b + c
calculado como
(a + b) + c
, porque é
+
associativo à esquerda.
a = b = c
calculado como
a = (b = c)
porque é
=
associativo correto. Observe que ele
=
retorna o valor atribuído à variável, portanto,
a
será igual ao que é
b
após avaliar a expressão.
Vamos substituir os operandos por suas prioridades:
a left b left c = (a left b) left c a right b right c = a right (b right c) a left b right c = ? a right b left c = ?
Vê que os segundos exemplos são logicamente impossíveis?
a + b = c
só é possível porque
+
tem precedência sobre
=
, portanto, o analisador sabe o que fazer. Se dois operadores têm a mesma precedência, mas associatividade diferente, o analisador de sintaxe não será capaz de determinar em que ordem realizar as ações!
Então, para resumir: não, operadores com a mesma precedência não podem ter associatividade diferente!
É curioso que no F # você possa alterar a associatividade das funções na hora, por isso consegui falar sobre associatividade sem enlouquecer! Mais detalhes.
Operadores unários
Um ponto interessante descoberto ao analisar a ordem de cálculo
+n
e
++n
.
Não pode ser executado
-- -i
porque
-
retorna um número e os números não podem ser aumentados ou diminuídos e não pode ser executado
---i
porque o significado é
---
ambíguo (isto
-- -
ou
- --
? Veja os comentários abaixo.), Mas você pode fazer isto:
let i = 10
console.log(-+-+-+-+-+--i)
Positividade confusa
Um dos problemas mais problemáticos era a ambigüidade
+
em JavaScript. O mesmo símbolo, conforme visto abaixo, é usado em quatro funções diferentes:
let i = 10
console.log(i++ + + ++i)
Cada operando tem seu próprio significado, prioridade e associatividade. Isso me lembra o famoso quebra-cabeça de palavras:
Buffalo buffalo Buffalo buffalo buffalo Buffalo buffalo .
Operadores unários ou atribuição?
+
pode significar um operador unário ou uma atribuição. O que é no caso do
u
problema desde o início do artigo?
... + u
Em última análise, a resposta depende de ... o que é. Se escrevêssemos tudo em uma linha
... + u
então a resposta seria diferente para
x + u
e
x - + u
. No primeiro caso, o símbolo significa adição, e no segundo - unário
+
. A única maneira de descobrir o que isso significa é analisar o resto da expressão até que haja apenas um operador para representar!
Propaganda
VDS para programadores com o hardware mais recente, proteção contra ataques e uma grande seleção de sistemas operacionais. A configuração máxima é de 128 núcleos de CPU, 512 GB de RAM, 4000 GB NVMe.