
Il me semble que la réaction à une telle question dans une interview dépend de ce que c'est exactement. Si la question est vraiment de savoir quelle est la valeur
tree
, alors le code peut simplement être inséré dans la console et obtenir le résultat.
Cependant, si la question est de savoir comment résoudre ce problème, alors tout devient assez curieux et conduit à un test de connaissance des subtilités de JavaScript et du compilateur. Dans cet article, je vais essayer de dissiper toute cette confusion et d'obtenir des conclusions intéressantes.
Je diffusais le processus de résolution de ce problème sur Twitch . L'émission est longue, mais elle vous permet de jeter un autre regard sur le processus étape par étape de résolution de tels problèmes.
Raisonnement général
Tout d'abord, convertissons le code au format copiable:
let b = 3, d = b, u = b;
const tree = ++d * d*b * b++ +
+ --d+ + +b-- +
+ +d*b+ +
u
J'ai immédiatement remarqué quelques particularités et j'ai décidé que quelques astuces de compilateur pourraient être utilisées ici. Vous voyez, JavaScript ajoute généralement des points-virgules à la fin de chaque ligne, sauf s'il y a une expression qui ne peut pas être interrompue . Dans ce cas,
+
à la fin de chaque ligne, il indique au compilateur qu'il n'est pas nécessaire d'interrompre cette construction.
La première ligne crée simplement trois variables et leur attribue une valeur
3
.
3
Est une valeur primitive, donc chaque fois qu'une copie est créée, elle est créée par valeur , donc toutes les nouvelles variables sont créées avec une valeur
3
... Si JavaScript devait attribuer des valeurs à ces variables par référence , chaque nouvelle variable pointerait vers la variable précédemment utilisée, mais ne créerait pas de valeur pour elle-même.
Information additionnelle
Priorité et associativité des opérateurs
Ce sont les concepts clés pour résoudre cette tâche ardue. En bref, ils définissent l'ordre dans lequel une combinaison d'expressions JavaScript est évaluée.
Priorité de l'opérateur
Q: quelle est la différence entre ces deux expressions?
3 + 5 * 5
5 * 5 + 3
Du point de vue du résultat, il n'y a pas de différence. Quiconque se souvient des cours de mathématiques à l'école sait que la multiplication se fait avant l'addition. En anglais, nous nous souvenons de l'ordre comme BODMAS (Brackets Off Divide Multiply Add Subtract - parenthèses, degré, division, multiplication, addition, soustraction). JavaScript a un concept similaire appelé Operator Precedence: cela signifie l'ordre dans lequel nous évaluons les expressions. Si nous voulions d'abord forcer le calcul
3 + 5
, nous ferions ce qui suit:
(3+5) * 5
Les parenthèses forcent cette partie de l'expression à être évaluée en premier, car l'opérateur a une priorité
()
plus élevée que l'opérateur
*
.
Chaque opérateur JavaScript est prioritaire, donc avec autant d'opérateurs
tree
qu'il contient, nous devons déterminer dans quel ordre ils seront évalués. Il est particulièrement important de savoir ce
--
qui changera les valeurs
b
et
d
, par conséquent, nous devons savoir quand ces expressions sont évaluées par rapport aux autres
tree
.
Important: Tableau des priorités de l'opérateur et informations supplémentaires
Associativité
L'associativité est utilisée pour déterminer dans quel ordre les expressions sont évaluées dans des opérateurs de priorité égale. Par exemple:
a + b + c
Il n'y a pas de priorité d'opérateur dans cette expression car il n'y a qu'un seul opérateur. Alors, comment le calculer - comment
(a + b) + c
ou comment
a + (b + c)
?
Je sais que le résultat sera le même, mais le compilateur a besoin de le savoir pour pouvoir sélectionner d'abord une opération, puis continuer le calcul. Dans ce cas, la réponse correcte est
(a + b) + c
que l'opérateur est
+
laissé associatif, c'est-à-dire qu'il évalue d'abord l'expression de gauche.
«Pourquoi ne pas simplement rendre tous les opérateurs associatifs?» Vous pourriez demander.
Eh bien, prenons un exemple comme celui-ci:
a = b + c
Si nous utilisons la formule d'associativité de gauche, nous obtenons
(a = b) + c
Mais attendez, cela a l'air bizarre, et ce n'est pas ce que je voulais dire. Si nous voulions que cette expression fonctionne en utilisant uniquement l'associativité de gauche, nous devrons alors faire quelque chose comme ceci:
a + b = c
Ceci est converti en
(a + b) = c
, c'est-à-dire d'abord
a + b
, puis la valeur de ce résultat est affectée à la variable
c
.
Si nous devions penser de cette façon, JavaScript serait beaucoup plus déroutant, c'est pourquoi nous utilisons différentes associativités pour différents opérateurs - cela rend le code plus lisible. Quand nous lisons
a = b + c
, l'ordre de calcul nous semble naturel, malgré le fait que tout est plus habilement arrangé à l'intérieur et utilise des opérandes associatifs à droite et à gauche.
Vous avez probablement remarqué le problème d'associativité dans
a = b + c
... Si les deux opérateurs ont une associativité différente, comment savoir quelle expression évaluer en premier? Réponse: celui avec la priorité d'opérateur la plus élevée , comme dans la section précédente! Dans ce cas, il
+
a une priorité plus élevée, il est donc calculé en premier.
J'ai ajouté une explication plus détaillée à la fin de l'article, ou vous pouvez lire plus d'informations .
Comprendre comment notre expression d'arbre est évaluée
Ayant compris ces principes, nous pouvons commencer à analyser notre problème. Il utilise de nombreux opérateurs et l'absence de parenthèses le rend difficile à comprendre. Ajoutons donc simplement des parenthèses, listant tous les opérateurs utilisés avec leur priorité et leur associativité.
(opérateur avec variable x): | une priorité | associativité |
x ++: | 18 | ne pas |
X--: | 18 | ne pas |
++ x: | 17 | droite |
--X: | 17 | droite |
+ x: | 17 | droite |
*: | quinze | la gauche |
x + y: | 14 | la gauche |
=: | 3 | droite |
Parenthèses
Il convient de mentionner ici que l'ajout correct de parenthèses est une tâche délicate. J'ai vérifié que la réponse est correctement calculée à chaque étape, mais cela ne garantit pas que mes parenthèses sont toujours placées correctement! Si vous connaissez un outil pour le placement automatique des accolades, veuillez m'envoyer un e-mail.
Déterminons l'ordre dans lequel les expressions sont évaluées et ajoutons des parenthèses pour l'afficher. Je vais vous montrer étape par étape comment je suis arrivé au résultat final, en passant simplement des opérateurs les plus prioritaires vers le bas.
Postfix ++ et postfix -
const tree = ++d * d*b * (b++) +
+ --d+ + +(b--) +
+ +d*b+ +
u
Unaire +, préfixe ++ et préfixe -
Nous avons un petit problème ici, mais je commencerai par évaluer l'opérateur unaire
+
, puis nous aborderons le problème.
const tree = ++d * d*b * (b++) +
+ --d+ (+(+(b--))) +
(+(+(d*b+ (+
u))))
Et c'est là que surgissent les difficultés.
+ --d+
--
et
+()
ont la même priorité. Comment savons-nous dans quel ordre les calculer? Formulons le problème de manière plus simple:
let d = 10
const answer = + --d
Rappelez-vous,
+
ce n'est pas une addition, mais un plus unaire ou une positivité. Vous pouvez le percevoir comme
-1
, seulement ici
+1
.
La solution est que nous évaluons de droite à gauche, car les opérateurs de cette priorité sont associatifs de droite .
Donc, notre expression est convertie en
+ (--d)
.
Pour comprendre cela, essayez d'imaginer que tous les opérateurs sont identiques. Dans ce cas, il
+ +1
sera équivalent
(+ (+1))
selon la logique, ce qui
1 — 1 — 1
équivaut à
((1 — 1) — 1)
... Remarquez que le résultat des opérateurs associatifs de droite dans la notation entre parenthèses est le contraire du cas des opérateurs de gauche?
Si nous appliquons la même logique au point problématique, nous obtenons ce qui suit:
const tree = ++d * d*b * (b++) +
(+ (--d)) + (+(+(b--))) +
(+(+(d*b+ (+
u))))
Et enfin, en insérant les parenthèses pour ce dernier
++
, on obtient:
const tree = (++d) * d*b * (b++) +
(+ (--d)) + (+(+(b--))) +
(+(+(d*b+ (+
u))))
Multiplication (*)
Encore une fois, nous devons faire face à l'associativité, mais cette fois avec le même opérateur, qui reste associatif. Par rapport à l'étape précédente, cela devrait être facile!
const tree = ((((++d) * d) * b) * (b++)) +
(+ (--d)) + (+(+(b--))) +
(+(+((d*b) + (+u))))
Nous avons atteint le stade auquel il est déjà possible de commencer les calculs. Il serait possible d'ajouter des parenthèses pour l'opérateur d'affectation, mais je pense que ce sera plus déroutant que plus facile à lire, donc nous ne le ferons pas. Notez que l'expression ci-dessus est juste un peu plus compliquée
x = a + b + c
.
Nous pouvons raccourcir certains des opérateurs unaires, mais je les garde au cas où ils seraient importants.
En divisant l'expression en plusieurs parties, nous pouvons comprendre les étapes individuelles des calculs et nous en inspirer.
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
Cela fait, nous pouvons commencer à explorer le calcul de différentes valeurs. Commençons par treeA.
ArbreA
let b = 3, d = b, u = b;
const treeA = (((++d) * d) * b) * (b++)
La première chose qui sera évaluée ici est une expression
++d
qui retournera
4
et s'incrémentera
d
.
// b = 3
// d = 4
((4 * d) * b) * (b++)
Puis il est exécuté
4*d
: on sait qu'à ce stade d est 4, donc il
4*4
vaut 16.
// b = 3
// d = 4
(16 * b) * (b++)
La chose intéressante à propos de cette étape est que nous allons multiplier par b avant d' incrémenter b, donc le calcul se fait de gauche à droite.
16 * 3 = 48
...
// b = 3
// d = 4
48 * (b++)
Ci-dessus, nous avons parlé de ce qui
++
a une priorité plus élevée que
*
, donc cela peut être écrit comme
48 * b++
, mais il y a d'autres astuces ici - la valeur de retour
b++
est la valeur avant l' incrément, pas après. Ainsi, même si b devient finalement 4, la valeur multipliée sera 3.
// b = 3
// d = 4
48 * 3
// b = 4
// d = 4
48 * 3
est égal
144
, donc après avoir calculé la première partie b et d sont égaux à 4, et le résultat de l'expression est
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--)))
À ce stade, nous pouvons voir que les opérateurs unaires ne font rien. Si nous les raccourcissons, nous simplifierons grandement l'expression.
// b = 4
// d = 4
const treeB = (--d) + (b--)
Nous avons déjà vu cette astuce ci-dessus.
--d
renvoie
3
, mais
b--
retourne
4
, mais au moment où l'expression est évaluée, les deux se verront attribuer la valeur 3.
const treeB = 3 + 4
// b = 3
// d = 3
Alors maintenant, notre tâche ressemble à ceci:
let b = 3, d= 3, u = 3;
const treeA = 144
const treeB = 7
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
TreeC
Et nous avons presque terminé!
// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + (+u))))
Débarrassons-nous d'abord de ces opérateurs unaires ennuyeux.
// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + u)))
Nous nous en sommes débarrassés, mais ici, vous devez faire attention aux crochets, etc.
// b = 3
// d = 3
// u = 3
const treeC = (d*b) + u
C'est assez simple maintenant.
3 * 3
égal
9
,
9 + 3
égal
12
, et enfin, nous avons ...
Réponse!
let b = 3, d= 3, u = 3;
const treeA = 144
const treeB = 7
const treeC = 12
const tree = treeA + treeB + treeC
144 + 7 + 12
égal
163
. La réponse au problème:
163
.
Conclusion
JavaScript peut vous déconcerter de nombreuses façons étranges et délicieuses. Mais en comprenant comment fonctionne la langue, vous pouvez en trouver la raison la plus fondamentale.
De manière générale, le chemin vers une solution peut être plus instructif que la réponse, et les mini-solutions trouvées en cours de route peuvent nous apprendre quelque chose par elles-mêmes.
Cela vaut la peine de dire que j'ai vérifié mon travail à l'aide de la console du navigateur et qu'il était plus intéressant pour moi de faire de l'ingénierie inverse de la solution que de résoudre le problème sur la base des principes de base.
Même si vous savez résoudre un problème, de nombreuses ambiguïtés syntaxiques doivent être traitées en cours de route. Et je suis sûr que beaucoup d'entre vous l'ont remarqué en regardant l'expression de notre arbre. J'en ai énuméré quelques-uns ci-dessous, mais chacun vaut un article séparé!
Je voudrais également remercier https://twitter.com/AnthonyPAlicea, sans le cours duquel je n'aurais jamais pu tout comprendre, et https://twitter.com/tlakomy pour cette question.
Notes et bizarreries
J'ai mis en évidence les mini-énigmes que j'ai rencontrées en cours de route dans une section distincte afin que le processus de recherche d'une solution reste transparent.
Comment la modification de l'ordre des variables affecte
Regardez cette vidéo
let x = 10
console.log(x++ + x)
Plusieurs questions peuvent être posées ici. Qu'est-ce qui sera imprimé sur la console et quelle est la valeur
x
sur la deuxième ligne?
Si vous pensez que c'est le même nombre, alors excusez-moi, je vous ai déjoué. L'astuce est ce qui est
x++ + x
calculé comme
(x++) + x
, et lorsque le moteur JavaScript calcule le côté gauche
(x++)
, il fait l'incrémentation
x
, donc quand il s'agit de
+ x
, la valeur de x est égale
11
, non
10
.
Une autre question délicate - quelle valeur renvoie-
x++
t-il?
J'ai donné un indice assez évident quant à la réponse
10
.
C'est la différence entre
x++
et
++x
. Si nous regardons les fonctions sous-jacentes des opérateurs, elles ressemblent à ceci:
function ++x(x) {
const oldValue = x;
x = x + 1;
return oldValue;
}
function x++(x) {
x = x + 1;
return x
}
En les regardant de cette manière, nous pouvons comprendre que
let x = 10
console.log(x++ + x)
signifiera ce qu'il
x++
renvoie
10
, et au moment de l'évaluation,
+ x
sa valeur est
11
. Par conséquent, il sera imprimé sur la console
21
et la valeur x sera égale à
11
.
Cette tâche relativement simple pointe vers un anti-pattern commun utilisé dans tout le code - expressions confuses et effets secondaires . Plus de détails.
Pourrait-il y avoir deux opérateurs avec la même priorité mais des associativités différentes?
Avançons dans l'ordre et oublions pour l'instant le mot «associativité».
Prenons les opérateurs
+
et
=
, et résumons la situation.
Il a été montré au-dessus de ce qui est
a + b + c
calculé
(a + b) + c
, car il est
+
laissé associatif.
a = b = c
calculé comme
a = (b = c)
parce qu'il est
=
juste associatif. Notez qu'il
=
renvoie la valeur affectée à la variable, donc elle
a
sera égale à ce qu'elle est
b
après avoir évalué l'expression.
Remplaçons les opérandes par leur priorité:
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 = ?
Voyez-vous que les deuxièmes exemples sont logiquement impossibles?
a + b = c
n'est possible que parce qu'il
+
a la priorité sur
=
, donc l'analyseur sait quoi faire. Si deux opérateurs ont la même priorité, mais une associativité différente, alors l'analyseur syntaxique ne pourra pas déterminer dans quel ordre effectuer les actions!
Donc, pour résumer: non, les opérateurs avec la même priorité ne peuvent pas avoir d'associativité différente!
C'est curieux qu'en F # on puisse changer l'associativité des fonctions à la volée, c'est pourquoi j'ai pu parler d'associativité sans devenir fou! Plus de détails.
Opérateurs unaires
Un point intéressant découvert lors de l'analyse de l'ordre de calcul
+n
et
++n
.
Ne peut pas être exécuté
-- -i
car il
-
renvoie un nombre, et les nombres ne peuvent pas être incrémentés ou décrémentés, et ne peuvent pas être effectués
---i
car la signification est
---
ambiguë (ceci
-- -
ou
- --
? Voir les commentaires ci-dessous.), Mais vous pouvez le faire:
let i = 10
console.log(-+-+-+-+-+--i)
Positivité confuse
L'un des problèmes les plus problématiques était l'ambiguïté
+
de JavaScript. Le même symbole, comme indiqué ci-dessous, est utilisé dans quatre fonctions différentes:
let i = 10
console.log(i++ + + ++i)
Chaque opérande a sa propre signification, priorité et associativité. Cela me rappelle le fameux puzzle de mots:
Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo .
Opérateurs unaires ou affectation?
+
peut signifier soit un opérateur unaire, soit une affectation. Qu'est-ce que c'est dans le cas du
u
problème depuis le début de l'article?
... + u
En fin de compte, la réponse dépend de ... ce que c'est. Si nous écrivions tout sur une seule ligne
... + u
alors la réponse serait différente pour
x + u
et
x - + u
. Dans le premier cas, le symbole signifie addition, et dans le second - unaire
+
. La seule façon de comprendre ce que cela signifie est d'analyser le reste de l'expression jusqu'à ce qu'il ne reste plus qu'un opérateur à représenter!
La publicité
VDS pour les programmeurs avec le dernier matériel, une protection contre les attaques et une vaste sélection de systèmes d'exploitation. La configuration maximale est de 128 cœurs de processeur, 512 Go de RAM, 4000 Go de NVMe.
