toString: genial y terrible

imagen


La función toString en JavaScript es probablemente la más "implícita" discutida tanto entre los desarrolladores de js como entre los observadores externos. Ella es la causa de numerosos chistes y memes sobre muchas operaciones aritméticas sospechosas, transformaciones que entran en un estupor [objeto de objeto] . Se admite, tal vez, solo para sorprender cuando se trabaja con float64.


Los casos interesantes que tuve que observar, usar o superar, me motivaron a escribir un informe real. Galoparemos sobre la especificación del lenguaje y usaremos los ejemplos para analizar las características no obvias de toString .


Si espera una guía útil y suficiente, entonces este , este y ese material es más adecuado para usted. Si su curiosidad aún prevalece sobre el pragmatismo, entonces, por favor, debajo del gato.


Todo lo que necesitas saber


La función toString es una propiedad del objeto prototipo Object, en palabras simples, su método. Se utiliza para la conversión de cadenas de un objeto y debe devolver un valor primitivo de una buena manera. Los objetos prototipo también tienen sus implementaciones: Función, Matriz, Cadena, Booleano, Número, Símbolo, Fecha, RegExp, Error . Si implementa su prototipo de objeto (clase), toString será una buena forma de hacerlo.


JavaScript es un lenguaje con un sistema de tipo débil: lo que significa que nos permite mezclar diferentes tipos, realiza muchas operaciones implícitamente. En las conversiones, toString se combina con valueOf para reducir el objeto a la primitiva necesaria para la operación. Por ejemplo, el operador de suma se convierte en concatenación si hay al menos una línea entre los operadores. Algunas funciones estándar del lenguaje antes de su trabajo conducen a un argumento a la cadena: parseInt, decodeURI, JSON.parse, btoa, etc.


Se ha dicho y ridiculizado bastante sobre el casting implícito. Consideraremos implementaciones de toString de objetos prototipo de lenguaje clave.


Object.prototype.toString


Si pasamos a la sección correspondiente de la especificación, encontramos que la tarea principal del toString predeterminado es conseguir que la llamada etiqueta se concatene con la cadena resultante:


"[object " + tag + "]" 

Para hacer esto:


  1. Se produce una llamada al símbolo interno toStringTag (o la pseudo-propiedad [[Class]] en la edición anterior): tiene muchos objetos prototipo incorporados ( Map, Math, JSON y otros).
  2. Si falta una cadena o no, entonces se enumeran varias otras pseudo-propiedades internas y métodos que indican el tipo de objeto: [[Call]] para Function , [[DateValue]] para Date, y así sucesivamente.
  3. Bueno, si nada, entonces la etiqueta es "Objeto" .

Aquellos afectados por la reflexión notarán de inmediato la posibilidad de obtener el tipo de un objeto con una operación simple (no recomendado por la especificación, pero posible):


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

La peculiaridad del valor predeterminado de toString es que funciona con cualquier valor de este tipo . Si es un primitivo, se lanzará al objeto ( nulo e indefinido se verifican por separado). No TypeError :


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

¿Cómo puede ser útil? Por ejemplo, al desarrollar herramientas para el análisis dinámico de código. Al tener un conjunto improvisado de variables utilizadas durante el trabajo de la aplicación, puede recopilar estadísticas útiles y homogéneas en tiempo de ejecución.


Este enfoque tiene un inconveniente importante: los tipos de usuarios. No es difícil adivinar que para sus instancias solo obtenemos "Objeto" .


Symbol.toStringTag personalizado y Function.name


OOP en JavaScript se basa en prototipos, y no en clases (como en Java), y no tenemos un método getClass () listo. Una definición explícita del carácter toStringTag para un tipo de usuario ayudará a resolver el problema:


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

o en estilo prototipo:


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

Existe una solución alternativa a través de la propiedad de solo lectura Function.name , que aún no forma parte de la especificación, pero es compatible con la mayoría de los navegadores. Cada instancia del prototipo de objeto / clase tiene un enlace a la función constructora con la que se creó. Entonces podemos encontrar el nombre del tipo:


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

o en estilo prototipo:


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

Por supuesto, esta solución no funciona para objetos creados usando una función anónima ( "anónimo" ) o Object.create (nulo) , o para primitivas sin un objeto contenedor ( nulo, indefinido ).


Por lo tanto, para la manipulación confiable de tipos de variables, vale la pena combinar técnicas bien conocidas, basadas principalmente en la tarea en cuestión. En la gran mayoría de los casos, typeof e instanceof son suficientes.


Function.prototype.toString


Estábamos un poco distraídos, pero como resultado llegamos a funciones que tienen su propio toString interesante. Primero, eche un vistazo al siguiente código:


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

Muchos probablemente adivinaron que este es un ejemplo de Quine . Si carga una secuencia de comandos con dicho contenido en el cuerpo de la página, se mostrará una copia exacta del código fuente en la consola. Esto se debe a la llamada aString desde la función argumentos.callee .


La implementación utilizada del objeto prototipo toString of the Function devuelve una representación de cadena del código fuente de la función, conservando la sintaxis utilizada en su definición: FunctionDeclaration, FunctionExpression, ClassDeclaration, ArrowFunction , etc.


Por ejemplo, tenemos una función de flecha:


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

Llamar a bind.toString () nos devolverá una representación de cadena de ArrowFunction :


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

Y llamar a toString desde una función envuelta ya es una representación de cadena de FunctionExpression :


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

Este ejemplo de enlace no es accidental, ya que tenemos una solución preparada con el enlace de contexto Function.prototype.bind , y con respecto a las funciones enlazadas nativas , hay una característica de Function.prototype.toString que trabaja con ellas. Dependiendo de la implementación, se puede obtener una representación tanto de la función ajustada como de la función objetivo . V8 y SpiderMonkey últimas versiones de Chrome y FF:


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

Por lo tanto, se debe tener precaución con las características decoradas de forma nativa.


Practica usando f.toString


Hay muchas opciones para usar toString en cuestión, pero es urgente solo como una herramienta de metaprogramación o depuración. Tener una aplicación típica similar en lógica de negocios tarde o temprano conducirá a una depresión rota no admitida.


Lo más simple que viene a la mente es determinar la duración de la función :


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

La ubicación y el número de caracteres de espacio en blanco del resultado de toString están dados por la especificación para la compra de una implementación específica, por lo tanto, para la limpieza, primero eliminamos el exceso, lo que lleva a una vista general. Por cierto, en versiones anteriores del motor Gecko, la función tenía un parámetro de sangría especial que ayuda a formatear sangrías.


La definición de los nombres de los parámetros de la función viene inmediatamente a la mente, lo que puede ser útil para la reflexión:


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

Esta solución de rodilla es adecuada para la sintaxis de FunctionDeclaration y FunctionExpression . Si necesita uno más detallado y preciso, le recomiendo que busque ejemplos del código fuente de su marco favorito, que probablemente tenga algún tipo de inyección de dependencia bajo el capó, según los nombres de los parámetros declarados.


Una opción peligrosa e interesante para anular una función a travé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 

Conociendo la estructura de la función original, creamos una nueva reemplazando el operador de suma usado en su cuerpo con argumentos con multiplicación. En el caso de código generado por software o la falta de una interfaz de extensión de función, esto puede ser mágicamente útil. Por ejemplo, si está investigando un modelo matemático, seleccionando una función adecuada, jugando con operadores y coeficientes.


Un uso más práctico es la compilación y distribución de plantillas . Muchas implementaciones de motor de plantillas compilan el código fuente de una plantilla y proporcionan una función de datos que ya forma el HTML final (u otro). El siguiente es un ejemplo de la función _.template :


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

Pero, ¿qué pasa si compilar la plantilla requiere recursos de hardware o si el cliente es muy delgado? En este caso, podemos compilar la plantilla en el lado del servidor y dar a los clientes no el texto de la plantilla, sino una representación en cadena de la función finalizada. Además, no necesita cargar la biblioteca de plantillas en el cliente.


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

Ahora necesitamos ejecutar este código en el cliente antes de usarlo. Que en la compilación no hubo SyntaxError debido a la sintaxis de FunctionExpression :


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

más o menos:


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

O como más te guste. En cualquier caso:


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

Puede que esta no sea la mejor práctica para compilar plantillas en el lado del servidor y distribuirlas a los clientes aún más. Solo un ejemplo usando un montón de Function.prototype.toString y eval .


Finalmente, la antigua tarea de definir el nombre de una función (antes de que aparezca la propiedad Function.name ) a través de toString :


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

Por supuesto, esto funciona bien con la sintaxis de FunctionDeclaration . Una solución más inteligente requerirá una astuta expresión regular o coincidencia de patrones.


Internet está lleno de soluciones interesantes basadas en Function.prototype.toString , solo pregunte. Comparte tu experiencia en los comentarios: muy interesante.


Array.prototype.toString


La implementación de toString de un prototipo de objeto Array es genérica y se puede llamar para cualquier objeto. Si el objeto tiene un método de unión , el resultado de toString será su llamada, de lo contrario, Object.prototype.toString .


La matriz , lógicamente, tiene un método de unión que concatena la representación de cadena de todos sus elementos a través del separador pasado como parámetro (el valor predeterminado es una coma).


Supongamos que necesitamos escribir una función que serialice una lista de sus argumentos. Si todos los parámetros son primitivos, en muchos casos podemos prescindir de JSON.stringify :


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

más o menos:


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

Solo recuerde que la cadena '10' y el número 10 se serializarán de la misma manera. En el problema del memorizador más corto en una etapa, se utilizó esta solución.


La combinación nativa de elementos de matriz funciona a través de un ciclo aritmético de 0 a longitud y no filtra los elementos faltantes ( nulos e indefinidos ). En cambio, la concatenación ocurre con el separador . Esto lleva a lo siguiente:


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

Por lo tanto, si por una razón u otra agrega un elemento con un índice grande a la matriz (por ejemplo, esta es una identificación natural generada), en ningún caso no se una y, en consecuencia, no conduzca a una cadena sin una preparación preliminar. De lo contrario, puede haber consecuencias: longitud de cadena no válida, falta de memoria o simplemente un script colgante. Use las funciones del objeto Valores y claves del objeto para iterar sobre sus propias propiedades enumeradas del objeto solamente:


 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" 

Pero es mucho mejor evitar tal manejo de la matriz: lo más probable es que un simple objeto de valor clave le convenga como almacenamiento.


Por cierto, existe el mismo peligro cuando se serializa a través de JSON.stringify . Solo que es más grave, ya que los elementos vacíos y no compatibles ya están representados como "nulos" :


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

Concluyendo la sección, me gustaría recordarle que puede definir su método de unión para el tipo de usuario y llamar a Array.prototype.toString.call como una conversión alternativa a la cadena, pero dudo que tenga algún uso práctico.


Number.prototype.toString y parseInt


Una de mis tareas favoritas para los cuestionarios js es ¿Qué devolverá la próxima llamada de parseInt ?


 parseInt(10**30, 2) 

Lo primero que hace parseInt es emitir implícitamente un argumento a una cadena llamando a la función abstracta ToString , que, según el tipo de argumento, ejecuta la rama de conversión deseada. Para el número de tipo, se hace lo siguiente:


  1. Si el valor es NaN, 0 o Infinito , devuelve la cadena correspondiente.
  2. De lo contrario, el algoritmo devuelve el registro más conveniente para la persona del número: en forma decimal o exponencial.

No duplicaré el algoritmo para determinar la forma preferida aquí, solo notaré lo siguiente: si el número de dígitos en una notación decimal excede 21 , entonces se seleccionará una forma exponencial. Y esto significa que en nuestro caso parseInt no funciona con "100 ... 000" sino con "1e30". Por lo tanto, la respuesta no se espera en absoluto 2 ^ 30. Quién sabe la naturaleza de este número mágico 21: ¡escribe!


A continuación, parseInt analiza la base del sistema de números de radix utilizado (por defecto 10, tenemos 2) y verifica la compatibilidad de los caracteres de la cadena recibida. Habiendo encontrado 'e', ​​corta toda la cola, dejando solo "1". El resultado será un número entero obtenido mediante la conversión del sistema con la base de la raíz a decimal; en nuestro caso, es 1.


Procedimiento inverso:


 (2**30).toString(2) 

Aquí es donde se llama a la función toString desde el objeto prototipo Number , que usa el mismo algoritmo para convertir el número en una cadena. También tiene el parámetro de raíz opcional. Solo arroja un RangeError para un valor no válido (debe ser un número entero de 2 a 36 inclusive), mientras que parseInt devuelve NaN .


Vale la pena recordar el límite superior del sistema de números si planea implementar una función hash exótica: este toString puede no funcionar para usted.


La tarea de distraer por un momento:


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

¿Qué volverá y cómo solucionarlo?


Privado de atención


Examinamos toString de ninguna manera, incluso todos los objetos prototipos nativos. En parte, porque personalmente no tuve que meterme en problemas con ellos, y no hay mucho interés en ellos. Además, no tocamos la función toLocaleString , ya que sería bueno hablar de ello por separado. Si hice algo en vano sin atención, perdido de vista o mal entendido, ¡asegúrese de escribir!


Llamado a la inacción


Los ejemplos que he citado no son recetas preparadas, solo alimento para pensar. Además, me parece inútil y un poco estúpido discutir esto en entrevistas técnicas: para esto, hay temas eternos sobre cierres, uniones, un bucle de eventos, patrones de módulo / fachada / mediador y preguntas "por supuesto" sobre [el marco utilizado].


Este artículo resultó ser una mezcolanza, y espero que hayas encontrado algo interesante para ti. PD El lenguaje JavaScript: ¡increíble!


Bono


Al preparar este material para su publicación, utilicé Google Translate. Y por casualidad descubrí un efecto entretenido. Si selecciona una traducción del ruso al inglés, ingrese "toString" y comience a borrarla usando la tecla Retroceso, luego observaremos:


bono


¡Qué ironía! Creo que estoy lejos del primero, pero por si acaso les envié una captura de pantalla con un script de reproducción. Parece un inofensivo self-XSS, por eso lo comparto.

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


All Articles