
La fonction toString en JavaScript est probablement la plus "implicite" discutée à la fois parmi les développeurs js eux-mêmes et parmi les observateurs externes. Elle est à l'origine de nombreuses blagues et mèmes sur de nombreuses opérations arithmétiques suspectes, transformations qui entrent dans une stupeur [objet Object] . Il n'est concédé, peut-être, que pour surprendre en travaillant avec float64.
Des cas intéressants que j'ai dû observer, utiliser ou surmonter, m'ont motivé à écrire un vrai débriefing. Nous galoperons sur la spécification du langage et utiliserons les exemples pour analyser les fonctionnalités non évidentes de toString .
Si vous attendez des conseils utiles et suffisants, alors ceci , ceci et ce matériel vous conviennent mieux. Si votre curiosité l'emporte toujours sur le pragmatisme, alors s'il vous plaît, sous cat.
Tout ce que vous devez savoir
La fonction toString est une propriété de l'objet prototype Object, en termes simples sa méthode. Il est utilisé pour la conversion de chaîne d'un objet et doit renvoyer une valeur primitive dans le bon sens. Les objets prototypes ont également leurs implémentations: Function, Array, String, Boolean, Number, Symbol, Date, RegExp, Error . Si vous implémentez votre objet prototype (classe), toString sera une bonne forme pour lui.
JavaScript est un langage avec un système de type faible: ce qui signifie qu'il nous permet de mélanger différents types, d'effectuer implicitement de nombreuses opérations. Dans les conversions, toString est associé à valueOf pour réduire l'objet à la primitive nécessaire à l'opération. Par exemple, l'opérateur d'addition se transforme en concaténation s'il y a au moins une ligne parmi les opérateurs. Certaines fonctions standard du langage avant leur travail conduisent à un argument à la chaîne: parseInt, decodeURI, JSON.parse, btoa et ainsi de suite.
Beaucoup a été dit et ridiculisé sur le casting implicite. Nous considérerons les implémentations de toString des objets prototypes de langage clé.
Object.prototype.toString
Si nous passons à la section correspondante de la spécification, nous constatons que la tâche principale de toString par défaut est d'obtenir la soi-disant balise à concaténer à la chaîne résultante:
"[object " + tag + "]"
Pour ce faire:
- Un appel au symbole toStringTag interne (ou à la pseudo-propriété [[Class]] dans l'ancienne édition) se produit: il possède de nombreux objets prototypes intégrés ( Map, Math, JSON et autres).
- S'il manque ou non une chaîne, il énumère un certain nombre d'autres pseudo-propriétés et méthodes internes qui signalent le type de l'objet: [[Call]] pour Function , [[DateValue]] pour Date, et ainsi de suite.
- Eh bien, si rien du tout, la balise est "Object" .
Ceux qui sont concernés par la réflexion noteront immédiatement la possibilité d'obtenir le type d'un objet avec une opération simple (non recommandée par la spécification, mais possible):
const getObjT = obj => Object.prototype.toString.call(obj).match(/\[object\s(\w+)]/)[1];
La particularité de toString par défaut est qu'il fonctionne avec n'importe quelle valeur. S'il s'agit d'une primitive, elle sera convertie en objet ( nul et non défini sont vérifiés séparément). Aucune erreur de type :
[Infinity, null, x => 1, new Date, function*(){}].map(getObjT); > ["Number", "Null", "Function", "Date", "GeneratorFunction"]
Comment cela peut-il être utile? Par exemple, lors du développement d'outils pour l'analyse de code dynamique. Grâce à un pool impromptu de variables utilisées pendant le travail de l'application, vous pouvez collecter des statistiques homogènes utiles au moment de l'exécution.
Cette approche présente un inconvénient majeur: les types d'utilisateurs. Il n'est pas difficile de deviner que pour leurs instances, nous obtenons simplement "Object" .
Symbol.toStringTag et Function.name personnalisés
La POO en JavaScript est basée sur des prototypes, et non sur des classes (comme en Java), et nous n'avons pas de méthode getClass () prête à l' emploi . Une définition explicite du caractère toStringTag pour un type d'utilisateur aidera à résoudre le problème:
class Cat { get [Symbol.toStringTag]() { return 'Cat'; } }
ou en style prototype:
function Dog(){} Dog.prototype[Symbol.toStringTag] = 'Dog';
Il existe une solution alternative via la propriété en lecture seule Function.name , qui ne fait pas encore partie de la spécification, mais qui est prise en charge par la plupart des navigateurs. Chaque instance de l'objet / classe prototype a un lien vers la fonction constructeur avec laquelle il a été créé. On peut donc trouver le nom du type:
class Cat {} (new Cat).constructor.name < 'Cat'
ou en style prototype:
function Dog() {} (new Dog).constructor.name < 'Dog'
Bien sûr, cette solution ne fonctionne pas pour les objets créés à l'aide d'une fonction anonyme ( "anonyme" ) ou Object.create (null) , ou pour les primitives sans objet wrapper ( null, non défini ).
Ainsi, pour une manipulation fiable des types de variables, il convient de combiner des techniques bien connues, basées principalement sur la tâche à accomplir. Dans la grande majorité des cas, typeof et instanceof suffisent.
Function.prototype.toString
Nous étions un peu distraits, mais en conséquence, nous sommes arrivés à des fonctions qui ont leur propre chaîne intéressante. Tout d'abord, jetez un œil au code suivant:
(function() { console.log('(' + arguments.callee.toString() + ')()'); })()
Beaucoup ont probablement deviné qu'il s'agissait d'un exemple de Quine . Si vous chargez un script avec un tel contenu dans le corps de la page, une copie exacte du code source sera affichée dans la console. Cela est dû à l'appel à Strings depuis la fonction arguments.callee .
L' implémentation utilisée de l' objet prototype toString de la fonction renvoie une représentation sous forme de chaîne du code source de la fonction, en conservant la syntaxe utilisée dans sa définition: FunctionDeclaration, FunctionExpression, ClassDeclaration, ArrowFunction , etc.
Par exemple, nous avons une fonction de flèche:
const bind = (f, ctx) => function() { return f.apply(ctx, arguments); }
L'appel de bind.toString () nous renverra une représentation sous forme de chaîne de ArrowFunction :
"(f, ctx) => function() { return f.apply(ctx, arguments); }"
Et appeler toString à partir d'une fonction encapsulée est déjà une représentation sous forme de chaîne de FunctionExpression :
"function() { return f.apply(ctx, arguments); }"
Cet exemple de liaison n'est pas accidentel, car nous avons une solution prête à l'emploi avec la liaison de contexte Function.prototype.bind , et en ce qui concerne les fonctions liées natives , il existe une fonctionnalité de Function.prototype.toString qui fonctionne avec elles. Selon l'implémentation, une représentation de la fonction encapsulée elle-même et de la fonction cible peut être obtenue. V8 et SpiderMonkey dernières versions de chrome et ff:
function getx() { return this.x; } getx.bind({ x: 1 }).toString() < "function () { [native code] }"
Par conséquent, la prudence doit être exercée avec les éléments décorés de manière native.
Entraînez-vous à utiliser f.toString
Il existe de nombreuses options pour utiliser la chaîne toString en question, mais elle n'est urgente qu'en tant qu'outil de métaprogrammation ou de débogage. Avoir une application typique similaire dans la logique métier conduira tôt ou tard à un creux cassé non pris en charge.
La chose la plus simple qui me vient à l'esprit est de déterminer la durée de la fonction :
f.toString().replace(/\s+/g, ' ').length
L'emplacement et le nombre de caractères d' espacement du résultat toString sont donnés par la spécification à l'achat d'une implémentation spécifique, par conséquent, pour la propreté, nous supprimons d'abord l'excédent, ce qui conduit à une vue générale. Soit dit en passant, dans les anciennes versions du moteur Gecko, la fonction avait un paramètre d' indentation spécial qui aide au formatage des retraits.
La définition des noms de paramètres de fonction vient immédiatement à l'esprit, ce qui peut être utile pour la réflexion:
f.toString().match(/^function(?:\s+\w+)?\s*\(([^\)]+)/m)[1].split(/\s*,\s*/)
Cette solution de genou convient à la syntaxe FunctionDeclaration et FunctionExpression . Si vous en avez besoin d'un plus détaillé et précis, je vous recommande de rechercher des exemples du code source de votre framework préféré, qui a probablement une sorte d'injection de dépendance sous le capot, basé sur les noms des paramètres déclarés.
Une option dangereuse et intéressante pour remplacer une fonction via 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
Connaissant la structure de la fonction d'origine, nous en avons créé une nouvelle en remplaçant l'opérateur d'addition utilisé dans son corps par des arguments avec multiplication. Dans le cas du code généré par logiciel ou de l'absence d'une interface d'extension de fonction, cela peut être magiquement utile. Par exemple, si vous recherchez un modèle mathématique, sélectionnez une fonction appropriée, jouez avec les opérateurs et les coefficients.
Une utilisation plus pratique est la compilation et la distribution de modèles . De nombreuses implémentations de moteur de modèle compilent le code source d'un modèle et fournissent une fonction de données qui forme déjà le code HTML final (ou autre). Voici un exemple de la fonction _.template :
const helloJst = "Hello, <%= user %>" _.template(helloJst)({ user: 'admin' }) < "Hello, admin"
Mais que se passe-t-il si la compilation du modèle nécessite des ressources matérielles ou si le client est très léger? Dans ce cas, nous pouvons compiler le modèle côté serveur et donner aux clients non pas le texte du modèle, mais une représentation sous forme de chaîne de la fonction terminée. De plus, vous n'avez pas besoin de charger la bibliothèque de modèles sur le client.
const helloStr = _.template(helloJst).toString() helloStr < "function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { __p += 'Hello, ' + ((__t = ( user )) == null ? '' : __t); } return __p }"
Maintenant, nous devons exécuter ce code sur le client avant utilisation. Qu'à la compilation, il n'y avait pas de SyntaxError à cause de la syntaxe FunctionExpression :
const helloFn = eval(helloStr.replace(/^function\(obj\)/, 'obj=>'));
ou alors:
const helloFn = eval(`const f = ${helloStr};f`);
Ou comme vous le souhaitez. En tout cas:
helloFn({ user: 'admin' }) < "Hello, admin"
Ce n'est peut-être pas la meilleure pratique pour compiler des modèles côté serveur et les distribuer davantage aux clients. Juste un exemple utilisant un tas de Function.prototype.toString et eval .
Enfin, l'ancienne tâche de définition d'un nom de fonction (avant l'apparition de la propriété Function.name ) via toString :
f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1]
Bien sûr, cela fonctionne bien avec la syntaxe FunctionDeclaration . Une solution plus intelligente nécessitera une expression régulière ou une correspondance de modèle astucieuse.
Internet regorge de solutions intéressantes basées sur Function.prototype.toString , il suffit de demander. Partagez votre expérience dans les commentaires: très intéressant.
Array.prototype.toString
L'implémentation de la toString d' un objet prototype Array est générique et peut être appelée pour n'importe quel objet. Si l'objet a une méthode de jointure , le résultat de toString sera son appel, sinon Object.prototype.toString .
Array , logiquement, a une méthode de jointure qui concatène la représentation sous forme de chaîne de tous ses éléments via le séparateur passé en paramètre (la valeur par défaut est une virgule).
Supposons que nous ayons besoin d'écrire une fonction qui sérialise une liste de ses arguments. Si tous les paramètres sont des primitives, dans de nombreux cas, nous pouvons nous passer de JSON.stringify :
function seria() { return Array.from(arguments).toString(); }
ou alors:
const seria = (...a) => a.toString();
N'oubliez pas que la chaîne «10» et le numéro 10 seront sérialisés de la même manière. Dans le problème du mémoizer le plus court à un moment donné, cette solution a été utilisée.
La jointure native des éléments du tableau fonctionne à travers un cycle arithmétique de 0 à la longueur et ne filtre pas les éléments manquants ( null et indéfini ). Au lieu de cela, la concaténation se produit avec le séparateur . Cela conduit à ce qui suit:
const ar = new Array(1000); ar.toString() < ",,,...,,,"
Par conséquent, si pour une raison ou une autre vous ajoutez un élément avec un grand index au tableau (par exemple, c'est un identifiant naturel généré), en aucun cas ne vous joignez et, par conséquent, ne conduisez pas à une chaîne sans préparation préalable. Sinon, il peut y avoir des conséquences: longueur de chaîne non valide, mémoire insuffisante ou simplement un script pendant. Utilisez les fonctions des valeurs et des clés de l'objet pour parcourir uniquement ses propres propriétés énumérées de l'objet:
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"
Mais il vaut mieux éviter une telle manipulation du tableau: très probablement un simple objet clé-valeur vous conviendrait comme stockage.
Soit dit en passant, le même danger existe lors de la sérialisation via JSON.stringify . Plus grave encore, car les éléments vides et non pris en charge sont déjà représentés comme "null" :
const ar = new Array(1000); JSON.stringify(ar); < "[null,null,null,...,null,null,null]" // 1000 times
Pour conclure la section, je voudrais vous rappeler que vous pouvez définir votre méthode de jointure pour le type d'utilisateur et appeler Array.prototype.toString.call en tant que distribution alternative à la chaîne, mais je doute qu'elle ait une utilité pratique.
Number.prototype.toString et parseInt
L'une de mes tâches préférées pour les questionnaires js est Qu'est-ce qui retournera le prochain appel parseInt ?
parseInt(10**30, 2)
La première chose que fait parseInt est de lancer implicitement un argument dans une chaîne en appelant la fonction abstraite ToString , qui, selon le type d'argument, exécute la branche de distribution souhaitée. Pour le numéro de type, les opérations suivantes sont effectuées:
- Si la valeur est NaN, 0 ou Infinity , renvoyez la chaîne correspondante.
- Sinon, l'algorithme renvoie l'enregistrement du nombre le plus pratique pour l'homme: sous forme décimale ou exponentielle.
Je ne dupliquerai pas l' algorithme pour déterminer la forme préférée ici, je noterai seulement ce qui suit: si le nombre de chiffres dans une notation décimale dépasse 21 , alors une forme exponentielle sera sélectionnée. Et cela signifie que dans notre cas, parseInt ne fonctionne pas avec "100 ... 000" mais avec "1e30". Par conséquent, la réponse n'est pas du tout attendue 2 ^ 30. Qui connaît la nature de ce numéro magique 21 - écrivez!
Ensuite, parseInt examine la base du système de nombre de radix utilisé (par défaut 10, nous en avons 2) et vérifie la compatibilité des caractères de la chaîne reçue. Après avoir rencontré 'e', il coupe toute la queue, ne laissant que "1". Le résultat sera un entier obtenu en convertissant du système avec la base radix en décimal - dans notre cas, c'est 1.
Procédure inverse:
(2**30).toString(2)
C'est là que la fonction toString est appelée à partir de l'objet prototype Number , qui utilise le même algorithme pour transtyper nombre en chaîne. Il a également le paramètre optionnel radix . Seulement, elle renvoie une RangeError pour une valeur non valide (ce doit être un entier de 2 à 36 inclus), tandis que parseInt renvoie NaN .
Il convient de se rappeler la limite supérieure du système numérique si vous prévoyez d'implémenter une fonction de hachage exotique: cette chaîne toString peut ne pas fonctionner pour vous.
La tâche de distraire un instant:
'3113'.split('').map(parseInt)
Qu'est-ce qui reviendra et comment y remédier?
Privé d'attention
Nous n'avons examiné toString en aucun cas même tous les objets prototypes natifs. En partie, parce que personnellement je n'ai pas eu de problème avec eux, et ils n'ont pas grand chose d'intéressant. De plus, nous n'avons pas touché la fonction toLocaleString , car ce serait bien d'en parler séparément. Si j'ai fait quelque chose en vain privé d'attention, perdu de vue ou mal compris - assurez-vous d'écrire!
Appel à l'inaction
Les exemples que j'ai cités ne sont en aucun cas des recettes toutes faites - seulement de la matière à réflexion. De plus, je trouve inutile et un peu stupide de discuter de cela lors des entretiens techniques: pour cela, il y a des sujets éternels sur les fermetures, la jointure, une boucle d'événement, les modèles de module / façade / médiateur et des questions «bien sûr» sur [le cadre utilisé].
Cet article s'est avéré être un méli-mélo, et j'espère que vous avez trouvé quelque chose d'intéressant pour vous-même. PS Le langage JavaScript - incroyable!
Bonus
Pour préparer ce document en vue de sa publication, j'ai utilisé Google Translate. Et tout à fait par accident, j'ai découvert un effet divertissant. Si vous sélectionnez une traduction du russe vers l'anglais, entrez "toString" et commencez à l'effacer à l'aide de la touche Retour arrière, nous observerons alors:

Quelle ironie! Je pense que je suis loin du premier, mais juste au cas où je leur enverrais une capture d'écran avec un script de lecture. Il ressemble à un auto-XSS inoffensif, c'est pourquoi je le partage.