
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:
- 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).
- 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.
- 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() < ",,,...,,,"
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:
- Se o valor for NaN, 0 ou Infinito , retorne a sequência correspondente.
- 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:

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.