Lösen eines lustigen Puzzles in JavaScript

Unsere Geschichte beginnt mit einem Tweet von Tomas Lakoma, in dem er Sie einlädt, sich vorzustellen, dass Ihnen eine solche Frage in einem Interview begegnet ist.







Es scheint mir, dass die Reaktion auf eine solche Frage in einem Interview davon abhängt, was genau es ist. Wenn die Frage wirklich ist, was der Wert ist tree



, kann der Code einfach in die Konsole eingefügt werden und das Ergebnis erhalten.



Wenn sich jedoch die Frage stellt, wie Sie dieses Problem lösen würden , wird alles ziemlich merkwürdig und führt zu einem Test des Wissens über die Feinheiten von JavaScript und dem Compiler. In diesem Artikel werde ich versuchen, all diese Verwirrung auszuräumen und interessante Schlussfolgerungen zu ziehen.



Ich habe den Prozess zur Lösung dieses Problems auf Twitch gestreamt . Die Sendung ist lang, aber Sie können sich den schrittweisen Prozess zur Lösung solcher Probleme noch einmal genauer ansehen.



Allgemeine Argumentation



Konvertieren wir zunächst den Code in ein kopierbares Format:



let b = 3, d = b, u = b;
const tree = ++d * d*b * b++ +
 + --d+ + +b-- +
 + +d*b+ +
 u
      
      





Ich bemerkte sofort einige Besonderheiten und entschied, dass hier einige Compiler-Tricks verwendet werden könnten. Sie sehen, JavaScript fügt normalerweise Semikolons am Ende jeder Zeile hinzu, es sei denn , es gibt einen Ausdruck, der nicht unterbrochen werden kann . In diesem Fall +



teilt es dem Compiler am Ende jeder Zeile mit, dass diese Konstruktion nicht unterbrochen werden muss.



In der ersten Zeile werden einfach drei Variablen erstellt und ihnen ein Wert zugewiesen 3



. 3



Ist ein primitiver Wert, wird jedes Mal, wenn eine Kopie erstellt wird, diese nach Wert erstellt , sodass alle neuen Variablen mit einem Wert erstellt werden 3



... Wenn JavaScript diesen Variablen Werte als Referenz zuweisen würde , würde jede neue Variable auf die zuvor verwendete Variable verweisen , jedoch keinen Wert für sich selbst erstellen.



Weitere Informationen



Vorrang und Assoziativität des Operators



Dies sind die Schlüsselkonzepte zur Lösung dieser entmutigenden Aufgabe. Kurz gesagt, sie definieren die Reihenfolge, in der eine Kombination von JavaScript-Ausdrücken ausgewertet wird.



Bedienerpriorität



F: Was ist der Unterschied zwischen diesen beiden Ausdrücken?



3 + 5 * 5
      
      





5 * 5 + 3
      
      





Aus Sicht des Ergebnisses gibt es keinen Unterschied. Jeder, der sich an den Mathematikunterricht in der Schule erinnert, weiß, dass die Multiplikation vor der Addition erfolgt. Auf Englisch erinnern wir uns an die Reihenfolge als BODMAS (Klammern aus Teilen, Multiplizieren, Addieren, Subtrahieren - Klammern, Grad, Division, Multiplikation, Addition, Subtraktion). JavaScript hat ein ähnliches Konzept namens Operator Precedence: Es bedeutet die Reihenfolge, in der wir Ausdrücke auswerten. Wenn wir zuerst die Berechnung erzwingen wollten 3 + 5



, würden wir Folgendes tun:



(3+5) * 5
      
      





Die Klammern erzwingen, dass dieser Teil des Ausdrucks zuerst ausgewertet wird, da der Operator eine ()



höhere Priorität als der Operator hat *



.



Jeder JavaScript-Operator hat Vorrang. Bei so vielen Operatoren tree



müssen wir also herausfinden, in welcher Reihenfolge sie ausgewertet werden. Es ist besonders wichtig, was --



die Werte ändert, b



und d



daher müssen wir wissen, wann diese Ausdrücke im Verhältnis zum Rest ausgewertet werden tree



.



Wichtig: Bedienerprioritätstabelle und zusätzliche Informationen



Assoziativität



Die Assoziativität wird verwendet, um zu bestimmen, in welcher Reihenfolge Ausdrücke in Operatoren mit gleicher Priorität ausgewertet werden. Beispielsweise:



a + b + c
      
      





In diesem Ausdruck gibt es keine Operator-Priorität, da es nur einen Operator gibt. Wie ist es zu berechnen - wie (a + b) + c



oder wie a + (b + c)



?



Ich weiß, dass das Ergebnis dasselbe sein wird, aber der Compiler muss dies wissen, damit er zuerst eine Operation auswählen und dann die Berechnung fortsetzen kann. In diesem Fall ist die richtige Antwort, (a + b) + c



weil der Operator +



assoziativ bleibt, dh zuerst den Ausdruck links auswertet.



"Warum nicht einfach alle Operatoren assoziativ machen?", Könnten Sie fragen.



Nehmen wir ein Beispiel wie dieses:



a = b + c
      
      





Wenn wir die linke Assoziativitätsformel verwenden, erhalten wir



(a = b) + c
      
      





Aber warte, das sieht komisch aus und das habe ich nicht gemeint. Wenn wir wollten, dass dieser Ausdruck nur mit linker Assoziativität funktioniert, müssten wir so etwas tun:



a + b = c
      
      





Dies wird umgewandelt (a + b) = c



, das heißt, zuerst a + b



, und dann wird der Wert dieses Ergebnisses wird der Variablen zugewiesen c



.



Wenn wir so denken müssten, wäre JavaScript viel verwirrender, weshalb wir unterschiedliche Assoziativitäten für unterschiedliche Operatoren verwenden - dies macht den Code lesbarer. Wenn wir lesen a = b + c



, erscheint uns die Reihenfolge der Berechnung natürlich, obwohl alles im Inneren geschickter angeordnet ist und rechts- und linksassoziative Operanden verwendet.



Sie haben wahrscheinlich das Assoziativitätsproblem in bemerkt a = b + c



... Wenn beide Operatoren unterschiedliche Assoziativitäten haben, woher wissen Sie, welcher Ausdruck zuerst ausgewertet werden soll? Antwort: die mit der höheren Operator-Priorität , wie im vorherigen Abschnitt! In diesem Fall hat es +



eine höhere Priorität, daher wird es zuerst berechnet.



Ich habe am Ende des Artikels eine ausführlichere Erklärung hinzugefügt, oder Sie können weitere Informationen lesen .



Verstehen, wie unser Baumausdruck bewertet wird



Nachdem wir diese Prinzipien verstanden haben, können wir beginnen, unser Problem zu analysieren. Es werden viele Operatoren verwendet, und das Fehlen von Klammern erschwert das Verständnis. Fügen wir also einfach Klammern hinzu und listen alle verwendeten Operatoren zusammen mit ihrer Priorität und Assoziativität auf.



(Operator mit Variable x): eine Priorität Assoziativität
x ++: 18 Nein
x--: 18 Nein
++ x: 17 Recht
--x: 17 Recht
+ x: 17 Recht
*: fünfzehn links
x + y: 14 links
=: 3 Recht


Klammern



Erwähnenswert ist hier, dass das korrekte Hinzufügen von Klammern eine schwierige Aufgabe ist. Ich habe überprüft, ob die Antwort in jeder Phase korrekt berechnet wurde, dies garantiert jedoch nicht, dass meine Klammern immer richtig platziert sind! Wenn Sie ein Tool für die automatische Platzierung von Zahnspangen kennen, senden Sie mir bitte eine E-Mail.



Lassen Sie uns die Reihenfolge herausfinden, in der die Ausdrücke ausgewertet werden, und Klammern hinzufügen, um sie anzuzeigen. Ich werde Ihnen Schritt für Schritt zeigen, wie ich zum Endergebnis gekommen bin, indem ich mich von den Operatoren mit der höchsten Priorität nach unten bewegt habe.



Postfix ++ und Postfix -



const tree = ++d * d*b * (b++) +
 + --d+ + +(b--) +
 + +d*b+ +
 u
      
      





Unary +, Präfix ++ und Präfix -



Wir haben hier ein kleines Problem, aber ich beginne mit der Bewertung des unären Operators +



und komme dann zum Problempunkt.



const tree = ++d * d*b * (b++) +
 + --d+ (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Und hier entstehen Schwierigkeiten.



+ --d+
      
      





--



und +()



haben die gleiche Priorität. Woher wissen wir, in welcher Reihenfolge wir sie berechnen? Lassen Sie uns das Problem einfacher formulieren:



let d = 10
const answer = + --d
      
      





Denken Sie daran, +



dies ist keine Addition, sondern ein unäres Plus oder eine positive Einstellung. Man kann es so wahrnehmen -1



, nur hier ist es +1



.



Die Lösung besteht darin , dass wir von rechts nach links zu bewerten, weil die Betreiber dieses Vorrangs Recht sind assoziativ .



Also wird unser Ausdruck in umgewandelt + (--d)



.



Um dies zu verstehen, stellen Sie sich vor, dass alle Operatoren gleich sind. In diesem Fall ist es + +1



wird äquivalent sein (+ (+1))



nach der Logik, das ist 1 — 1 — 1



äquivalent ((1 — 1) — 1)



... Beachten Sie, dass das Ergebnis von rechtsassoziativen Operatoren in der Notation mit Klammern das Gegenteil des Falls bei linken Operatoren ist.



Wenn wir dieselbe Logik auf den Problempunkt anwenden, erhalten wir Folgendes:



const tree = ++d * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Und schließlich erhalten ++



wir durch Einfügen der Klammern für Letzteres :



const tree = (++d) * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Multiplikation (*)



Wieder müssen wir uns mit Assoziativität befassen, diesmal jedoch mit demselben Operator, der assoziativ bleibt. Im Vergleich zum vorherigen Schritt sollte dies einfach sein!



const tree = ((((++d) * d) * b) * (b++)) +
 (+ (--d)) + (+(+(b--))) +
 (+(+((d*b) + (+u))))
      
      





Wir haben das Stadium erreicht, in dem bereits mit Berechnungen begonnen werden kann. Es wäre möglich, Klammern für den Zuweisungsoperator hinzuzufügen, aber ich denke, es wird eher verwirrend als leichter zu lesen sein, also werden wir das nicht tun. Beachten Sie, dass der obige Ausdruck nur etwas komplizierter ist x = a + b + c



.



Wir können einige der unären Operatoren kürzen, aber ich werde sie speichern, falls sie wichtig sind.



Indem wir den Ausdruck in mehrere Teile aufteilen, können wir die einzelnen Berechnungsstufen verstehen und darauf aufbauen.



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
      
      





Nachdem dies erledigt ist, können wir beginnen, die Berechnung verschiedener Werte zu untersuchen. Beginnen wir mit treeA.



TreeA



let b = 3, d = b, u = b;
const treeA = (((++d) * d) * b) * (b++)
      
      





Das erste, was hier ausgewertet wird, ist ein Ausdruck ++d



, der zurückgibt 4



und inkrementiert d



.



// b = 3
// d = 4
((4 * d) * b) * (b++)
      
      





Dann wird es ausgeführt 4*d



: Wir wissen, dass in diesem Stadium d 4 ist, also 4*4



16.



// b = 3
// d = 4
(16 * b) * (b++)
      
      





Das Interessante an diesem Schritt ist, dass wir vor dem Inkrementieren von b mit b multiplizieren , sodass die Berechnung von links nach rechts erfolgt. 16 * 3 = 48



...



// b = 3
// d = 4
48 * (b++)
      
      





Oben haben wir darüber gesprochen, was ++



eine höhere Priorität als hat *



, daher kann dies so geschrieben werden 48 * b++



, aber es gibt hier andere Tricks - der Rückgabewert b++



ist der Wert vor dem Inkrement, nicht danach. Obwohl b schließlich 4 wird, ist der multiplizierte Wert 3.



// b = 3
// d = 4
48 * 3
// b = 4
// d = 4
      
      





48 * 3



ist gleich 144



, also sind nach der Berechnung der erste Teil b und d gleich 4, und das Ergebnis des Ausdrucks ist 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--)))
      
      





An diesem Punkt können wir sehen, dass unäre Operatoren eigentlich nichts tun. Wenn wir sie kürzen, werden wir den Ausdruck stark vereinfachen.



// b = 4
// d = 4
const treeB = (--d) + (b--)
      
      





Diesen Trick haben wir oben bereits gesehen. --d



gibt zurück 3



, gibt aber b--



zurück 4



, aber bis der Ausdruck ausgewertet wird, wird beiden der Wert 3 zugewiesen.



const treeB = 3 + 4
// b = 3
// d = 3
      
      





Jetzt sieht unsere Aufgabe ungefähr so ​​aus:



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





TreeC



Und wir sind fast fertig!



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + (+u))))
      
      





Lassen Sie uns zuerst diese nervigen unären Operatoren loswerden.



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + u)))
      
      





Wir haben es losgeworden, aber hier müssen Sie vorsichtig mit Klammern usw. sein.



// b = 3
// d = 3
// u = 3
const treeC = (d*b) + u
      
      





Es ist jetzt ziemlich einfach. 3 * 3



gleich 9



, 9 + 3



gleich 12



und schließlich haben wir ...



Antworten!



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = 12
const tree = treeA + treeB + treeC
      
      





144 + 7 + 12



gleich 163



. Die Antwort auf das Problem : 163



.



Fazit



JavaScript kann Sie auf eine Reihe von seltsamen und entzückenden Arten rätseln. Wenn Sie jedoch verstehen, wie Sprache funktioniert, können Sie den grundlegendsten Grund dafür finden.



Im Allgemeinen kann der Weg zu einer Lösung informativer sein als die Antwort, und die auf dem Weg gefundenen Minilösungen können uns etwas von selbst beibringen.



Es ist erwähnenswert, dass ich meine Arbeit über die Browserkonsole überprüft habe und es für mich interessanter war, die Lösung zurückzuentwickeln, als das Problem anhand der Grundprinzipien zu lösen.



Selbst wenn Sie wissen, wie man ein Problem löst, gibt es viele syntaktische Unklarheiten, die auf dem Weg behandelt werden müssen. Und ich bin sicher, dass viele von Ihnen es bemerkt haben, als Sie unseren Baumausdruck betrachteten. Ich habe einige davon unten aufgelistet, aber jeder ist einen eigenen Artikel wert!



Ich möchte mich auch bei https://twitter.com/AnthonyPAlicea, ohne deren Verlauf ich nie alles hätte herausfinden können, und bei https://twitter.com/tlakomy für diese Frage bedanken .



Notizen und Kuriositäten



Ich habe die Mini-Rätsel, denen ich unterwegs begegnet bin, in einem separaten Abschnitt hervorgehoben, damit der Prozess der Lösungsfindung transparent bleibt.



Wie sich das Ändern der Reihenfolge von Variablen auswirkt



Sehen Sie sich dieses Video an



let x = 10
console.log(x++ + x)
      
      





Hier können mehrere Fragen gestellt werden. Was wird auf der Konsole gedruckt und wie hoch ist der Wert x



in der zweiten Zeile?



Wenn Sie denken, dass dies die gleiche Zahl ist, dann entschuldigen Sie, ich habe Sie überlistet. Der Trick ist, wie x++ + x



berechnet wird (x++) + x



, und wenn die JavaScript-Engine die linke Seite berechnet (x++)



, führt sie das Inkrement durch x



. Wenn es darum geht, + x



ist der Wert von x gleich 11



und nicht gleich 10



.



Eine weitere knifflige Frage: Welchen Wert x++



gibt es zurück ?



Ich habe einen ziemlich offensichtlichen Hinweis darauf gegeben, wie die Antwort tatsächlich lautet 10



.



Dies ist der Unterschied zwischen x++



und ++x



. Wenn wir uns die zugrunde liegenden Funktionen der Operatoren ansehen, sehen sie ungefähr so ​​aus:



function ++x(x) {
  const oldValue = x;
  x = x + 1;
  return oldValue;
}
function x++(x) {
  x = x + 1;
  return x
}
      
      





Wenn wir sie so betrachten, können wir das verstehen



let x = 10
console.log(x++ + x)
      
      





bedeutet, was es x++



zurückgibt 10



, und zum Zeitpunkt der Bewertung ist + x



sein Wert 11



. Daher wird es auf der Konsole gedruckt 21



und der Wert x ist gleich 11



.



Diese relativ einfache Aufgabe weist auf ein allgemeines Anti-Muster hin, das im gesamten Code verwendet wird - durcheinandergebrachte Ausdrücke und Nebenwirkungen . Mehr Details.



Könnte es zwei Operatoren mit derselben Priorität, aber unterschiedlichen Assoziativitäten geben?



Bewegen wir uns der Reihe nach und vergessen wir vorerst das Wort "Assoziativität".



Nehmen wir die Operatoren +



und =



und fassen die Situation zusammen.



Es wurde oben gezeigt, was a + b + c



berechnet wird (a + b) + c



, weil es +



assoziativ bleibt.



a = b = c



berechnet als a = (b = c)



weil es =



richtig assoziativ ist. Beachten Sie, dass es =



gibt den Wert der Variablen zugewiesen, so dass es a



gleich sein wird, was es ist , b



nach Auswertung des Ausdrucks.



Ersetzen wir die Operanden durch ihre Priorität:



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 = ?
      
      





Sehen Sie, dass die zweiten Beispiele logisch unmöglich sind? a + b = c



ist nur möglich, weil es +



Vorrang hat =



, sodass der Parser weiß, was zu tun ist. Wenn zwei Operatoren dieselbe Priorität, aber unterschiedliche Assoziativität haben, kann der Syntaxparser nicht bestimmen, in welcher Reihenfolge Aktionen ausgeführt werden sollen!



Zusammenfassend: Nein, Operatoren mit derselben Priorität können keine unterschiedliche Assoziativität haben!



Es ist merkwürdig, dass Sie in F # die Assoziativität von Funktionen im laufenden Betrieb ändern können, weshalb ich über Assoziativität sprechen konnte, ohne verrückt zu werden! Mehr Details.



Unäre Operatoren



Ein interessanter Punkt, der beim Parsen der Berechnungsreihenfolge +n



und entdeckt wurde ++n



.



Kann nicht ausgeführt werden, -- -i



da -



eine Zahl zurückgegeben wird und Zahlen nicht inkrementiert oder dekrementiert werden können und nicht ausgeführt werden können, ---i



weil die Bedeutung nicht ---



eindeutig ist (dies -- -



oder - --



? Siehe Kommentare unten). Sie können dies jedoch tun:



let i = 10
console.log(-+-+-+-+-+--i)
      
      





Verwirrte Positivität



Eines der problematischsten Probleme war die Mehrdeutigkeit +



in JavaScript. Das gleiche Symbol, wie unten dargestellt, wird in vier verschiedenen Funktionen verwendet:



let i = 10
console.log(i++ + + ++i)
      
      





Jeder Operand hat seine eigene Bedeutung, Priorität und Assoziativität. Es erinnert mich an das berühmte Worträtsel:



Büffelbüffel Büffelbüffel Büffel Büffel Büffel .



Unäre Operatoren oder Zuordnung?



+



kann entweder einen unären Operator oder eine Zuordnung bedeuten. Was ist bei dem u



Problem vom Anfang des Artikels an?



... +
u
      
      





Letztendlich hängt die Antwort davon ab ... was ist. Wenn wir alles in einer Zeile schreiben würden



... + u
      
      





dann wäre die Antwort für x + u



und anders x - + u



. Im ersten Fall bedeutet das Symbol Addition und im zweiten Fall - unär +



. Die einzige Möglichkeit, herauszufinden, was dies bedeutet, besteht darin, den Rest des Ausdrucks zu analysieren, bis nur noch ein Operator zur Darstellung übrig ist!






Werbung



VDS für Programmierer mit der neuesten Hardware, Angriffsschutz und einer großen Auswahl an Betriebssystemen. Die maximale Konfiguration beträgt 128 CPU-Kerne, 512 GB RAM, 4000 GB NVMe.






All Articles