toString: Ótimo e terrível

imagem


A função toString em JavaScript é provavelmente a mais "implícita" discutida entre os próprios desenvolvedores js e entre observadores externos. Ela é a causa de inúmeras piadas e memes sobre muitas operações aritméticas suspeitas, transformações que entram em um estupor [objeto Objeto] . É concedido, talvez, apenas para surpreender ao trabalhar com float64.


Casos interessantes que eu tive que observar, usar ou superar, me motivaram a escrever um relatório real. Galoparemos sobre a especificação da linguagem e usaremos os exemplos para analisar os recursos não óbvios do toString .


Se você espera orientações úteis e suficientes, então este , este e esse material são mais adequados para você. Se a sua curiosidade ainda prevalece sobre o pragmatismo, então por favor, sob gato.


Tudo o que você precisa saber


A função toString é uma propriedade do objeto de protótipo Object, em palavras simples, seu método. É usado para a conversão de string de um objeto e deve retornar um valor primitivo em um bom caminho. Os objetos de protótipo também têm suas implementações: Função, Matriz, String, Booleano, Número, Símbolo, Data, RegExp, Erro . Se você implementar seu objeto de protótipo (classe), o toString será uma boa forma para ele.


JavaScript é uma linguagem com um sistema de tipos fraco: o que significa que nos permite misturar tipos diferentes, executa muitas operações implicitamente. Nas conversões, toString é emparelhado com valueOf para reduzir o objeto ao primitivo necessário para a operação. Por exemplo, o operador de adição se transforma em concatenação se houver pelo menos uma linha entre os operadores. Algumas funções padrão da linguagem antes do trabalho levam a um argumento para a sequência: parseInt, decodeURI, JSON.parse, btoa e assim por diante.


Muito já foi dito e ridicularizado sobre elenco implícito. Consideraremos implementações de toString de objetos de protótipo de linguagem-chave.


Object.prototype.toString


Se voltarmos para a seção correspondente da especificação, descobrimos que a principal tarefa do toString padrão é fazer com que a chamada tag concatene a string resultante:


"[object " + tag + "]" 

Para fazer isso:


  1. Uma chamada para o símbolo interno toStringTag (ou a pseudo-propriedade [[Class]] na edição antiga) ocorre: ele possui muitos objetos protótipos internos ( Mapa, Matemática, JSON e outros).
  2. Se estiver faltando ou não uma sequência, ela enumera várias outras pseudo-propriedades e métodos internos que sinalizam o tipo do objeto: [[Call]] para Function , [[DateValue]] para Date e assim por diante.
  3. Bem, se nada, então a tag é "Objeto" .

Aqueles que são afetados pela reflexão notarão imediatamente a possibilidade de obter o tipo de um objeto com uma operação simples (não recomendada pela especificação, mas possível):


 const getObjT = obj => Object.prototype.toString.call(obj).match(/\[object\s(\w+)]/)[1]; 

A peculiaridade do toString padrão é que ele funciona com qualquer valor desse tipo . Se for um primitivo, será convertido para o objeto ( nulo e indefinido são verificados separadamente). Nenhum TypeError :


 [Infinity, null, x => 1, new Date, function*(){}].map(getObjT); > ["Number", "Null", "Function", "Date", "GeneratorFunction"] 

Como isso pode ser útil? Por exemplo, ao desenvolver ferramentas para análise dinâmica de código. Tendo um conjunto improvisado de variáveis ​​usado durante o trabalho do aplicativo, você pode coletar estatísticas homogêneas úteis em tempo de execução.


Essa abordagem tem uma grande desvantagem: tipos de usuários. Não é difícil adivinhar que para as instâncias deles apenas obtemos "Objeto" .


Symbol.toStringTag personalizado e Function.name


OOP no JavaScript é baseado em protótipos, e não em classes (como em Java), e não temos um método getClass () pronto. Uma definição explícita do caractere toStringTag para um tipo de usuário ajudará a resolver o problema:


 class Cat { get [Symbol.toStringTag]() { return 'Cat'; } } 

ou no estilo de protótipo:


 function Dog(){} Dog.prototype[Symbol.toStringTag] = 'Dog'; 

Existe uma solução alternativa através da propriedade somente leitura Function.name , que ainda não faz parte da especificação, mas é suportada pela maioria dos navegadores. Cada instância do objeto / classe do protótipo possui um link para a função do construtor com a qual foi criado. Para que possamos descobrir o nome do tipo:


 class Cat {} (new Cat).constructor.name < 'Cat' 

ou no estilo de protótipo:


 function Dog() {} (new Dog).constructor.name < 'Dog' 

Obviamente, esta solução não funciona para objetos criados usando uma função anônima ( "anônimo" ) ou Object.create (null) , bem como para primitivas sem um objeto de wrapper ( nulo, indefinido ).


Assim, para manipulação confiável de tipos de variáveis, vale a pena combinar técnicas conhecidas, baseadas principalmente na tarefa em questão. Na grande maioria dos casos, typeof e instanceof são suficientes.


Function.prototype.toString


Ficamos um pouco distraídos, mas, como resultado, chegamos a funções que têm seu próprio toString interessante. Primeiro, dê uma olhada no seguinte código:


 (function() { console.log('(' + arguments.callee.toString() + ')()'); })() 

Muitos provavelmente adivinharam que este é um exemplo de Quine . Se você carregar um script com esse conteúdo no corpo da página, uma cópia exata do código-fonte será exibida no console. Isso ocorre devido à chamada toString da função argumentos.callee .


A implementação usada do objeto de protótipo toString do Function retorna uma representação em string do código fonte da função, preservando a sintaxe usada em sua definição: FunctionDeclaration, FunctionExpression, ClassDeclaration, ArrowFunction , etc.


Por exemplo, temos uma função de seta:


 const bind = (f, ctx) => function() { return f.apply(ctx, arguments); } 

Chamar bind.toString () nos retornará uma representação de string de ArrowFunction :


 "(f, ctx) => function() { return f.apply(ctx, arguments); }" 

E chamar toString de uma função agrupada já é uma representação de string de FunctionExpression :


 "function() { return f.apply(ctx, arguments); }" 

Esse exemplo de ligação não é acidental, pois temos uma solução pronta com a ligação de contexto Function.prototype.bind e, em relação às funções nativas , existe um recurso de Function.prototype.toString trabalhando com elas. Dependendo da implementação, é possível obter uma representação da própria função agrupada e da função de destino . V8 e SpiderMonkey versões mais recentes do chrome e ff:


 function getx() { return this.x; } getx.bind({ x: 1 }).toString() < "function () { [native code] }" 

Portanto, deve-se ter cuidado com os recursos decorados de forma nativa.


Pratique usando f.toString


Há muitas opções para usar o toString em questão, mas é urgente apenas como uma ferramenta de metaprogramação ou depuração. Ter uma aplicação típica semelhante na lógica de negócios, mais cedo ou mais tarde, levará a uma calha quebrada não suportada.


A coisa mais simples que vem à mente é determinar o tamanho da função :


 f.toString().replace(/\s+/g, ' ').length 

A localização e o número de caracteres de espaço em branco do resultado toString são fornecidos pela especificação para a compra de uma implementação específica; portanto, para limpeza, primeiro removemos o excesso, levando a uma visão geral. A propósito, nas versões mais antigas do mecanismo Gecko, a função tinha um parâmetro de indentação especial que ajuda na formatação de indentações.


A definição de nomes de parâmetros de função vem imediatamente à mente, o que pode ser útil para reflexão:


 f.toString().match(/^function(?:\s+\w+)?\s*\(([^\)]+)/m)[1].split(/\s*,\s*/) 

Essa solução de joelho é adequada para as sintaxes FunctionDeclaration e FunctionExpression . Se você precisar de um mais detalhado e preciso, recomendo que você procure exemplos do código-fonte da sua estrutura favorita, que provavelmente possui algum tipo de injeção de dependência oculta, com base nos nomes dos parâmetros declarados.


Uma opção perigosa e interessante para substituir uma função através de eval :


 const sum = (a, b) => a + b; const prod = eval(sum.toString().replace(/\+(?=\s*(?:a|b))/gm, '*')); sum(5, 10) < 15 prod(5, 10) < 50 

Conhecendo a estrutura da função original, criamos uma nova substituindo o operador de adição usado em seu corpo por argumentos com multiplicação. No caso de código gerado por software ou a falta de uma interface de extensão de função, isso pode ser magicamente útil. Por exemplo, se você estiver pesquisando um modelo matemático, selecionando uma função adequada, jogando com operadores e coeficientes.


Um uso mais prático é a compilação e distribuição de modelos . Muitas implementações de mecanismo de modelo compilam o código-fonte de um modelo e fornecem uma função de dados que já forma o HTML final (ou outro). A seguir, é apresentado um exemplo da função _.template :


 const helloJst = "Hello, <%= user %>" _.template(helloJst)({ user: 'admin' }) < "Hello, admin" 

Mas e se a compilação do modelo exigir recursos de hardware ou o cliente for muito fraco? Nesse caso, podemos compilar o modelo no lado do servidor e fornecer aos clientes não o texto do modelo, mas uma representação em string da função finalizada. Além disso, você não precisa carregar a biblioteca de modelos no cliente.


 const helloStr = _.template(helloJst).toString() helloStr < "function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { __p += 'Hello, ' + ((__t = ( user )) == null ? '' : __t); } return __p }" 

Agora precisamos executar esse código no cliente antes do uso. Que na compilação não havia SyntaxError devido à sintaxe de FunctionExpression :


 const helloFn = eval(helloStr.replace(/^function\(obj\)/, 'obj=>')); 

ou mais:


 const helloFn = eval(`const f = ${helloStr};f`); 

Ou como você gosta mais. De qualquer forma:


 helloFn({ user: 'admin' }) < "Hello, admin" 

Essa pode não ser a melhor prática para compilar modelos no lado do servidor e distribuí-los aos clientes. Apenas um exemplo usando um monte de Function.prototype.toString e eval .


Finalmente, a tarefa antiga de definir um nome de função (antes que a propriedade Function.name apareça) via toString :


 f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1] 

Obviamente, isso funciona bem com a sintaxe FunctionDeclaration . Uma solução mais inteligente exigirá expressão regular astuta ou correspondência de padrões.


A Internet está cheia de soluções interessantes baseadas em Function.prototype.toString , basta perguntar. Compartilhe sua experiência nos comentários: muito interessante.


Array.prototype.toString


A implementação do toString de um objeto de protótipo de matriz é genérica e pode ser chamada para qualquer objeto. Se o objeto tiver um método de junção , o resultado de toString será sua chamada, caso contrário, Object.prototype.toString .


Matriz , logicamente, possui um método de junção que concatena a representação de string de todos os seus elementos através do separador passado como parâmetro (o padrão é uma vírgula).


Suponha que precisamos escrever uma função que serialize uma lista de seus argumentos. Se todos os parâmetros são primitivos, em muitos casos, podemos ficar sem o JSON.stringify :


 function seria() { return Array.from(arguments).toString(); } 

ou mais:


 const seria = (...a) => a.toString(); 

Lembre-se de que a sequência '10' e o número 10 serão serializados da mesma forma. No problema do memoizer mais curto em um estágio, essa solução foi usada.


A junção nativa dos elementos da matriz funciona por um ciclo aritmético de 0 a comprimento e não filtra os elementos ausentes ( nulo e indefinido ). Em vez disso, a concatenação ocorre com o separador . Isso leva ao seguinte:


 const ar = new Array(1000); ar.toString() < ",,,...,,," // 1000 times 

Portanto, se por um motivo ou outro você adicionar um elemento com um índice grande à matriz (por exemplo, esse é um ID natural gerado), em nenhum caso, junte-se e, portanto, não leve a uma sequência sem preparação preliminar. Caso contrário, pode haver consequências: Comprimento de string inválido, falta de memória ou apenas um script pendente. Use as funções dos valores e chaves do objeto Object para iterar apenas sobre suas próprias propriedades enumeradas do objeto:


 const k = []; k[2**10] = 1; k[2**20] = 2; k[2**30] = 3; Object.values(k).toString() < "1,2,3" Object.keys(k).toString() < "1024,1048576,1073741824" 

Mas é muito melhor evitar esse manuseio da matriz: provavelmente um objeto de valor-chave simples serviria como armazenamento.


A propósito, o mesmo perigo existe ao serializar através do JSON.stringify . Apenas mais sério, já que os elementos vazios e sem suporte já estão representados como "nulos" :


 const ar = new Array(1000); JSON.stringify(ar); < "[null,null,null,...,null,null,null]" // 1000 times 

Concluindo a seção, gostaria de lembrá-lo de que você pode definir seu método de junção para o tipo de usuário e chamar Array.prototype.toString.call como uma conversão alternativa à string, mas duvido que ele tenha algum uso prático.


Number.prototype.toString e parseInt


Uma das minhas tarefas favoritas para os questionários js é: O que retornará a próxima chamada de análise ?


 parseInt(10**30, 2) 

A primeira coisa que o parseInt faz é converter implicitamente um argumento em uma string chamando a função abstrata ToString , que, dependendo do tipo de argumento, executa o ramo de conversão desejado. Para o número do tipo, é feito o seguinte:


  1. Se o valor for NaN, 0 ou Infinito , retorne a sequência correspondente.
  2. Caso contrário, o algoritmo retornará o registro mais conveniente para o homem do número: na forma decimal ou exponencial.

Não duplicarei o algoritmo para determinar a forma preferida aqui, observarei apenas o seguinte: se o número de dígitos em uma notação decimal exceder 21 , uma forma exponencial será selecionada. E isso significa que, no nosso caso, o parseInt não funciona com "100 ... 000", mas com "1e30". Portanto, a resposta não é de todo esperada 2 ^ 30. Quem sabe a natureza desse número mágico 21 - escreva!


Em seguida, parseInt examina a base do sistema de números de raiz usado (por padrão 10, temos 2) e verifica os caracteres da sequência recebida quanto à compatibilidade com ela. Tendo encontrado 'e', ​​corta toda a cauda, ​​deixando apenas "1". O resultado será um número inteiro obtido pela conversão do sistema com a base do radical em decimal - no nosso caso, é 1.


Procedimento reverso:


 (2**30).toString(2) 

É aqui que a função toString é chamada do objeto de protótipo Number , que usa o mesmo algoritmo para converter o número em uma string. Ele também possui o parâmetro radix opcional. Somente ele lança um RangeError para um valor inválido (deve ser um número inteiro de 2 a 36 inclusive), enquanto parseInt retorna NaN .


Vale lembrar o limite superior do sistema numérico se você planeja implementar uma função de hash exótica: esse toString pode não funcionar para você.


A tarefa de distrair por um momento:


 '3113'.split('').map(parseInt) 

O que retornará e como consertar?


Privado de atenção


Examinamos toString de maneira alguma mesmo todos os objetos protótipos nativos. Em parte porque, pessoalmente, não tive problemas com eles e não há muito interesse neles. Além disso, não tocamos na função toLocaleString , pois seria bom falar sobre isso separadamente. Se eu fiz algo em vão privado de atenção, perdido de vista ou incompreendido - não se esqueça de escrever!


Chamada para inação


Os exemplos que citei não são de modo algum receitas prontas - apenas alimento para reflexão. Além disso, acho inútil e meio estúpido discutir isso em entrevistas técnicas: para isso, existem tópicos eternos sobre fechamento, junção, ciclo de eventos, padrões de módulo / fachada / mediador e perguntas "é claro" sobre [a estrutura usada].


Este artigo acabou se tornando uma miscelânea, e espero que você tenha encontrado algo interessante para si mesmo. PS A linguagem JavaScript - incrível!


Bônus


Ao preparar este material para publicação, usei o Google Translate. E, por acidente, descobri um efeito divertido. Se você selecionar uma tradução do russo para o inglês, digite "toString" e comece a apagá-la usando a tecla Backspace, e observaremos:


bônus


Que ironia! Acho que estou longe do primeiro, mas apenas para o caso de lhes enviar uma captura de tela com um script de reprodução. Parece um auto-XSS inofensivo, é por isso que eu o compartilho.

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


All Articles