HolyJS 2019: Nachbesprechung von SEMrush (Teil 1)



Auf der regulären Konferenz für HolyJS- JavaScript-Entwickler , die vom 24. bis 25. Mai in St. Petersburg stattfand, bot unser Firmenstand allen neue Aufgaben. Diesmal waren es 3! Die Aufgaben wurden der Reihe nach gegeben, und für die Lösung jedes nachfolgenden wurden die Insignien (JS Brave> JS Adept> JS Master) herangezogen, was als gute Motivation diente, nicht aufzuhören. Insgesamt haben wir rund 900 Antworten gesammelt und haben es eilig, eine Analyse der beliebtesten und einzigartigsten Lösungen zu teilen.

Die vorgeschlagenen Tests setzen voraus, dass die grundlegenden "Merkmale" der Sprache mutig und verständlich sind und dass die neuen Merkmale von ECMAScript 2019 bekannt sind (letzteres ist in der Tat nicht erforderlich). Es ist wichtig, dass diese Aufgaben nicht für Interviews, nicht praktisch und nur zum Zweck des Spaßes gedacht sind.

Aufgabe 1 ~ Countdown-Ausdruck


Was wird den Ausdruck zurückgeben? Ordnen Sie ein einzelnes Zeichen neu an

  1. Ausdruck zurückgegeben 2
  2. Ausdruck zurückgegeben 1

+(_ => [,,~1])().length 

Zusätzlich : Ist es möglich, durch Permutationen 0 zu erhalten?

Was wird den Ausdruck zurückgeben?


Es dauert nicht lange, hier nachzudenken: Der Ausdruck wird 3 zurückgeben . Wir haben eine anonyme Funktion, die einfach ein Array von drei Elementen zurückgibt, von denen die ersten beiden leer sind. Der Funktionsaufruf gibt uns dieses Array, wir nehmen die Länge daraus und der unäre Operator plus löst nichts.

Der Ausdruck gibt 2 zurück


Eine schnelle Lösung scheint die Anzahl der Elemente im zurückgegebenen Array zu verringern. Um dies zu tun, werfen Sie einfach ein Komma weg:

 [,~1].length // 2 

Es ist unmöglich, ein Symbol einfach so wegzuwerfen: Es muss an einer anderen Stelle im ursprünglichen Ausdruck neu angeordnet werden. Wir wissen jedoch, dass die Längeneigenschaft des Arrays die leeren Elemente berücksichtigt, die im Array-Literal aufgeführt sind, mit Ausnahme eines Falls :

Wenn ein Element am Ende eines Arrays entfernt wird, trägt dieses Element nicht zur Länge des Arrays bei.

Wenn sich also ein leeres Element am Ende des Arrays befindet, wird es ignoriert:

 [,10,] // [empty, 10] 

Der korrigierte Ausdruck sieht also folgendermaßen aus:

 +(_ => [,~1,])().length // 2 

Gibt es eine andere Möglichkeit, dieses Komma zu entfernen? Oder fuhr weiter.

Der Ausdruck gibt 1 zurück


Es ist nicht mehr möglich, eine zu erhalten, indem die Größe des Arrays verringert wird, da Sie mindestens zwei Permutationen durchführen müssen. Müssen nach einer anderen Option suchen.

Die Funktion selbst weist auf die Entscheidung hin. Denken Sie daran, dass eine Funktion in JavaScript ein Objekt ist, das über eigene Eigenschaften und Methoden verfügt. Eine der Eigenschaften ist auch die Länge , die die Anzahl der von der Funktion erwarteten Argumente bestimmt. In unserem Fall hat die Funktion ein einziges Argument ( Unterstrich ) - was Sie brauchen!

Damit die Länge einer Funktion und nicht einem Array entnommen werden kann, müssen Sie den Aufruf in Klammern beenden. Es wird offensichtlich, dass eine dieser Klammern ein Anwärter auf eine Permutation ist. Und ein paar Optionen kommen in den Sinn:

 +((_ => [,,~1])).length // 1 +(_ => ([,,~1])).length // 1 

Vielleicht gibt es noch etwas? Oder das nächste Level.

Der Ausdruck gibt 0 zurück


In der zusätzlichen Aufgabe ist die Anzahl der Permutationen nicht begrenzt. Aber wir können versuchen, es zu erfüllen, indem wir minimale Gesten machen.

Wenn Sie frühere Erfahrungen mit der Länge eines Funktionsobjekts entwickeln, können Sie schnell zu einer Lösung kommen:

 +(() => [,_,~1]).length // 0 

Hier gibt es mehrere Ableitungen, aber der springende Punkt ist, dass wir die Anzahl der Funktionsargumente auf Null reduziert haben. Dazu benötigten wir bis zu drei Permutationen : zwei Klammern und den Unterstrich , der zu einem Element des Arrays wurde.

Ok, aber vielleicht ist es an der Zeit, die Additionsoperatoren (+) und bitweise NICHT (~) in unseren Überlegungen nicht mehr zu ignorieren? Es scheint, dass die Arithmetik in diesem Problem in unsere Hände spielen kann. Um nicht zum Anfang zu springen, hier der ursprüngliche Ausdruck:

 +(_ => [,,~1])().length 

Zunächst berechnen wir ~ 1 . Das bitweise NICHT von x gibt - (x + 1) zurück . Das heißt, ~ 1 = -2 . Und wir haben auch einen Additionsoperator im Ausdruck, der sozusagen andeutet, dass Sie irgendwo anders 2 weitere finden müssen und alles klappen wird.

Zuletzt haben wir uns an die „Nichtwirkung“ des letzten leeren Elements in einem Array-Literal in Bezug auf seine Größe erinnert, was bedeutet, dass unsere Zwei irgendwo hier sind:

 [,,].length // 2 

Und alles summiert sich sehr erfolgreich: Wir holen das Element ~ 1 aus dem Array, reduzieren seine Länge auf 2 und fügen es als ersten Operanden für unsere Hinzufügung zum Anfang des Ausdrucks hinzu:

 ~1+(_ => [,,])().length // 0 

Damit haben wir das Ziel bereits in zwei Permutationen erreicht !

Aber was ist, wenn dies nicht die einzige Option ist? Ein kleines Trommelwirbel ...

 +(_ => [,,~1.])(),length // 0 

Es sind auch zwei Permutationen erforderlich: der Punkt nach der Einheit (dies ist möglich, da der numerische Typ nur eine Zahl ist ) und das Komma vor der Länge . Es scheint Unsinn, aber "manchmal" funktioniert. Warum manchmal?

In diesem Fall wird der Ausdruck durch den Kommaoperator in zwei Ausdrücke unterteilt und das Ergebnis ist der berechnete Wert des zweiten. Aber der zweite Ausdruck ist nur Länge ! Tatsache ist, dass wir hier in einem globalen Kontext auf den Wert einer Variablen zugreifen. Wenn die Laufzeit ein Browser ist, dann window.length . Das Fensterobjekt verfügt über eine Längeneigenschaft , die die Anzahl der Frames auf der Seite zurückgibt. Wenn unser Dokument leer ist, gibt die Länge 0 zurück. Ja, eine Option mit einer Annahme ... lassen Sie uns daher auf die vorherige eingehen.

Und hier sind einige weitere interessante Optionen entdeckt (bereits für eine andere Anzahl von Permutationen):

 (_ => [,,].length+~1)() // 0 +(~([,,].len_gth) >= 1) // 0 ~(_ => 1)()+[,,].length // 0 ~(_ => 1)().length,+[,] // 0 ~[,,]+(_ => 1()).length // 0 

Es gibt keine Kommentare. Findet jemand etwas noch lustiger?

Motivation


Gute altmodische Aufgabe, „ein oder zwei Übereinstimmungen neu anzuordnen, um ein Quadrat zu erstellen“. Das bekannte Rätsel zur Entwicklung von Logik und kreativem Denken verwandelt sich in solchen JavaScript-Variationen in Obskurantismus. Ist das nützlich? Wahrscheinlicher nein als ja. Wir haben viele Merkmale der Sprache angesprochen, einige sogar konzeptionell, um den Lösungen auf den Grund zu gehen. Bei echten Projekten geht es jedoch nicht um die Länge von Funktion und Ausdruck bis hin zu Steroiden. Diese Aufgabe wurde auf der Konferenz als eine Art süchtig machendes Training vorgeschlagen, um sich daran zu erinnern, wie JavaScript ist.

Eval Kombinatorik


Es wurden nicht alle möglichen Antworten berücksichtigt, aber um überhaupt nichts zu verpassen, wenden wir uns echtem JavaScript zu ! Wenn es interessant ist, sie selbst zu finden, gab es oben genügend Tipps, und dann ist es besser, nicht weiter zu lesen.



Wir haben also einen Ausdruck von 23 Zeichen, in dessen String-Datensatz wir Permutationen durchführen werden. Insgesamt müssen wir im Originaldatensatz des Ausdrucks n * (n - 1) = 506 Permutationen durchführen, um alle Varianten mit einem permutierten Symbol zu erhalten (wie es die Bedingungen von Problem 1 und 2 erfordern).

Wir definieren eine Kombinationsfunktion , die einen Eingabeausdruck als Zeichenfolge und Prädikat verwendet, um die Eignung des durch Ausführen dieses Ausdrucks erhaltenen Werts zu testen. Die Funktion erzwingt alle möglichen Optionen und wertet den Ausdruck durch Auswertung aus. Das Ergebnis wird in einem Objekt gespeichert : Schlüssel - der empfangene Wert, Wert - eine Liste von Mutationen unseres Ausdrucks für diesen Wert. So etwas ist passiert:

 const combine = (expr, cond) => { let res = {}; let indices = [...Array(expr.length).keys()]; indices.forEach(i => indices.forEach(j => { if (i !== j) { let perm = replace(expr, i, j); try { let val = eval(perm); if (cond(val)) { (res[val] = res[val] || []).push(perm); } } catch (e) { /* do nothing */ } } })); return res; } 

Wobei die Ersetzungsfunktion aus der übergebenen Zeichenfolge des Ausdrucks eine neue Zeichenfolge zurückgibt, deren Zeichen von Position i zu Position j neu angeordnet wurde . Und jetzt werden wir ohne große Angst tun:

 console.dir(combine('+(_ => [,,~1])().length', val => typeof val === 'number' && !isNaN(val))); 

Als Ergebnis haben wir eine Reihe von Lösungen erhalten:

 { "1": [ "+(_ => [,,~1]()).length", "+((_ => [,,~1])).length", "+(_ =>( [,,~1])).length", "+(_ => ([,,~1])).length" ], "2": [ "+(_ => [,~1,])().length" ] "3": [/* ... */] "-4": [/* ... */] } 

Die Lösungen für 3 und -4 interessieren uns nicht, für die beiden haben wir die einzige Lösung gefunden, und für die Einheit gibt es einen interessanten neuen Fall mit [,, ~ 1] () . Warum nicht TypeError: bla-bla ist keine Funktion ? Und alles ist einfach: Dieser Ausdruck hat keinen Fehler für den syntaktischen Parser und wird zur Laufzeit einfach nicht ausgeführt, da die Funktion nicht aufgerufen wird.

Wie Sie sehen können, ist es in einer Permutation unmöglich, das Problem für Null zu lösen. Können wir es in zwei versuchen? Wenn wir das Problem durch eine so erschöpfende Suche lösen, haben wir in diesem Fall die Komplexität O (n ^ 4) der Länge der Zeichenfolge und "bewerten" so oft verfolgt und bestraft, aber es herrscht Neugier. Es ist nicht schwierig, die gegebene Kombinationsfunktion unabhängig zu verfeinern oder eine bessere Aufzählung zu schreiben, die die Merkmale eines bestimmten Ausdrucks berücksichtigt.

 console.dir(combine2('+(_ => [,,~1])().length', val => val === 0)); 

Am Ende wäre die Menge der Lösungen für Null:

 { "0": [ "+(_ => [,~.1])(),length", "+(_ => [,~1.])(),length", "~1+(_ => [,,])().length" ] } 

Es ist merkwürdig, dass wir in der Überlegung den Punkt nach der Einheit neu angeordnet haben, aber vergessen haben, dass der Punkt vor der Einheit ebenfalls möglich ist: Nehmen Sie 0,1 auf, wobei Null weggelassen wird.

Wenn Sie alle möglichen Permutationen mit jeweils zwei Zeichen ausführen, gibt es eine Vielzahl von Antworten für Werte im Bereich von 3 bis -4:

 { "3": 198, "2": 35, "1": 150, "0": 3, "-1": 129, "-2": 118, "-3": 15, "-4": 64 } 

Somit kann der Lösungspfad für den Countdown-Ausdruck bei zwei Permutationen länger sein als der vorgeschlagene Pfad von 3 bis 0 bei einer.

Dies war der erste Teil der Analyse unserer Aufgaben bei HolyJS 2019 und der zweite sollte bald erscheinen, wo wir die Lösungen des zweiten und dritten Tests betrachten werden. Wir bleiben in Kontakt!

Source: https://habr.com/ru/post/de452114/


All Articles