HolyJS 2019: Analisando a partir do SEMrush (Parte 1)



Na conferência regular para desenvolvedores de JavaScript do HolyJS, realizada de 24 a 25 de maio em São Petersburgo, o estande da nossa empresa ofereceu a todos novas tarefas. Desta vez, havia três deles! As tarefas foram dadas por sua vez, e para a solução de cada uma subseqüente foi confiada a insígnia (JS Brave> JS Adept> JS Master), o que serviu como uma boa motivação para não parar. No total, coletamos cerca de 900 respostas e temos pressa em compartilhar uma análise das soluções mais populares e exclusivas.

Os testes propostos esperam do corajoso e do entendimento dos "recursos" básicos da linguagem e do conhecimento dos novos recursos do ECMAScript 2019 (na verdade, o último não é necessário). É importante que essas tarefas não sejam para entrevistas, não sejam práticas e sejam pensadas apenas para se divertir.

Tarefa 1 ~ Expressão de contagem regressiva


O que retornará a expressão? Reorganize qualquer caractere único para

  1. expressão retornou 2
  2. expressão retornada 1

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

Além disso : é possível obter 0 por permutações?

O que retornará a expressão?


Não demora muito para pensar aqui: a expressão retornará 3 . Temos uma função anônima que simplesmente retorna uma matriz de três elementos, os dois primeiros vazios. A chamada de função nos fornece essa matriz, tiramos o comprimento dela e o operador unário plus não resolve nada.

A expressão retorna 2


Uma solução rápida parece reduzir o número de elementos na matriz retornada. Para fazer isso, basta jogar uma vírgula:

 [,~1].length // 2 

É impossível jogar fora um símbolo assim: ele precisa ser reorganizado para outro lugar na expressão original. Mas sabemos que a propriedade length da matriz leva em consideração os elementos vazios listados no literal da matriz, exceto em um caso :

Se um elemento é elidido no final de uma matriz, esse elemento não contribui para o comprimento da matriz.

Ou seja, se um elemento vazio estiver no final da matriz, ele será ignorado:

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

Assim, a expressão corrigida fica assim:

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

Existe outra opção para se livrar dessa vírgula? Ou seguiu em frente.

A expressão retorna 1


Não é mais possível obter um reduzindo o tamanho da matriz, pois você precisará fazer pelo menos duas permutações. Tem que procurar outra opção.

A própria função sugere a decisão. Lembre-se de que uma função em JavaScript é um objeto que possui propriedades e métodos próprios. E uma das propriedades também é comprimento , que determina o número de argumentos esperados pela função. No nosso caso, a função possui um único argumento ( sublinhado ) - o que você precisa!

Para que o comprimento seja obtido de uma função, não de uma matriz, é necessário parar de chamá-la entre parênteses. Torna-se óbvio que um desses colchetes é um candidato a uma permutação. E algumas opções vêm à mente:

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

Talvez haja algo mais? Ou o próximo nível.

A expressão retorna 0


Na tarefa adicional, o número de permutações não é limitado. Mas podemos tentar cumpri-lo fazendo gestos mínimos.

Desenvolvendo a experiência anterior com o comprimento de um objeto de função, você pode chegar rapidamente a uma solução:

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

Existem várias derivadas aqui, mas o ponto principal é que reduzimos o número de argumentos da função para zero. Para fazer isso, precisávamos de três permutações : dois colchetes e o caractere sublinhado , que se tornou um elemento da matriz.

Ok, mas talvez seja hora de parar de ignorar os operadores de adição (+) e NOT bit a bit (~) em nosso raciocínio? Parece que a aritmética nesse problema pode se colocar em nossas mãos. Para não voltar ao começo, aqui está a expressão original:

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

Para começar, calculamos ~ 1 . O bit a bit NOT de x retornará - (x + 1) . Ou seja, ~ 1 = -2 . E também temos um operador de adição na expressão, o que, por assim dizer, sugere que em outro lugar você precisa encontrar mais 2 e tudo vai dar certo.

Mais recentemente, lembramos o “não efeito” do último elemento vazio em uma matriz literal em seu tamanho, o que significa que nosso empate está em algum lugar aqui:

 [,,].length // 2 

E tudo é adicionado com muito sucesso: obtemos o elemento ~ 1 da matriz, reduzindo seu comprimento para 2, e o adicionamos como o primeiro operando para nossa adição ao início da expressão:

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

Assim, já alcançamos o objetivo em duas permutações !

Mas e se essa não for a única opção? Um pequeno rolo de tambor ...

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

Também requer duas permutações: o ponto após a unidade (isso é possível, porque o tipo numérico é apenas número ) e a vírgula antes do comprimento . Parece bobagem, mas "às vezes" funciona. Por que às vezes?

Nesse caso, a expressão através do operador vírgula é dividida em duas expressões e o resultado será o valor calculado da segunda. Mas a segunda expressão é apenas comprimento ! O fato é que aqui estamos acessando o valor de uma variável em um contexto global. Se o tempo de execução for um navegador, window.length . E o objeto da janela possui uma propriedade length que retorna o número de quadros na página. Se nosso documento estiver vazio, o comprimento retornará 0. Sim, uma opção com uma suposição ... portanto, vamos nos debruçar sobre a anterior.

E aqui estão algumas opções mais interessantes descobertas (já para um número diferente de permutações):

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

Não há comentários. Alguém encontra algo ainda mais divertido?

Motivação


Boa tarefa antiquada sobre "reorganizar uma ou duas partidas para formar um quadrado". O conhecido quebra-cabeça para o desenvolvimento da lógica e do pensamento criativo se transforma em obscurantismo nessas variações do JavaScript. Isso é útil? Provavelmente não, sim. Abordamos muitos recursos da linguagem, alguns até bastante conceituais, para chegar ao fundo das soluções. No entanto, projetos reais não se limitam ao tamanho, da função e da expressão aos esteróides. Essa tarefa foi proposta na conferência como uma espécie de treino viciante para lembrar como é o JavaScript.

Eval combinatorics


Nem todas as respostas possíveis foram consideradas, mas para não perder nada, passamos ao JavaScript real ! Se é interessante tentar encontrá-los você mesmo, havia dicas suficientes acima e é melhor não ler mais.



Portanto, temos uma expressão de 23 caracteres, no registro de string do qual faremos permutações. No total, precisamos executar n * (n - 1) = 506 permutações no registro original da expressão para obter todas as variantes com um símbolo permutado (conforme exigido pelas condições dos problemas 1 e 2).

Definimos uma função de combinação que recebe uma expressão de entrada como uma sequência e um predicado para testar a adequação do valor obtido pela execução dessa expressão. A função aplicará força bruta em todas as opções possíveis e avaliará a expressão por meio de eval , salvando o resultado em um objeto: chave - o valor recebido, valor - uma lista de mutações de nossa expressão para esse valor. Algo assim aconteceu:

 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; } 

Onde a função de substituição da sequência passada da expressão retorna uma nova sequência com o caractere reorganizado da posição i para a posição j . E agora, sem muito medo, faremos:

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

Como resultado, obtivemos conjuntos de soluções:

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

As soluções para 3 e -4 não nos interessam, para as duas que encontramos a única solução e para a unidade há um novo caso interessante com [,, ~ 1] () . Por que não TypeError: bla-bla não é uma função ? E tudo é simples: essa expressão não tem erro para o analisador sintático e, em tempo de execução, simplesmente não é executada, pois a função não é chamada.

Como você pode ver, em uma permutação é impossível resolver o problema para zero. Podemos tentar em dois? Resolvendo o problema com uma pesquisa tão exaustiva, neste caso, teremos a complexidade O (n ^ 4) do comprimento da corda e "avaliaremos" tantas vezes perseguidas e punidas, mas a curiosidade prevalecerá. Não é difícil refinar independentemente a função combinada fornecida ou escrever uma enumeração melhor que leve em consideração os recursos de uma expressão específica.

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

No final, o conjunto de soluções para zero seria:

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

É curioso que, no raciocínio, reorganizamos o ponto após a unidade, mas esquecemos que o ponto na frente da unidade também é possível: registro 0,1 com zero omitido.

Se você executar todas as permutações possíveis de dois caracteres cada, poderá descobrir que há muitas respostas para valores no intervalo de 3 a -4:

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

Assim, o caminho da solução Countdown Expression em duas permutações pode ser maior que o caminho proposto de 3 a 0 em um.

Esta foi a primeira parte da análise de nossas tarefas no HolyJS 2019 e, em breve, a segunda deverá aparecer, onde consideraremos as soluções do segundo e terceiro testes. Entraremos em contato!

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


All Articles