Extension des types de base sans modification du prototype

Comment étendre les types de JavaScript de base (String, Date, etc.) sans modifier leurs prototypes? Par exemple, supposons que je voulais créer une classe de chaîne dérivée avec des méthodes de commodité:

function MyString() { } MyString.prototype = new String(); MyString.prototype.reverse = function() { return this.split('').reverse().join(''); }; var s = new MyString("Foobar"); // Hmm, where do we use the argument? s.reverse(); // Chrome - TypeError: String.prototype.toString is not generic // Firefox - TypeError: String.prototype.toString called on incompatible Object 

L'erreur semble provenir de méthodes de base de chaîne, probablement "fractionnées" dans ce cas, car ses méthodes sont appliquées à un objet non-chaîne. Mais si nous ne pouvons pas appliquer les objets non-string, puis-on les réutiliser automatiquement?

[Modifier]

De toute évidence, ma tentative est défectueuse de plusieurs façons, mais je pense qu'elle démontre mon intention. Après une certaine réflexion, il semble que nous ne pouvons pas réutiliser aucune des fonctions de l'objet prototype String sans les appeler explicitement sur une Chaîne.

Est-il possible d'étendre les types de base en tant que tels?

2 ans plus tard: changer n'importe quoi dans le cadre global est une idée terrible

Original:

Il y a quelque chose de "faux" avec l'extension des prototypes natifs est FUD dans les navigateurs ES5.

 Object.defineProperty(String.prototype, "my_method", { value: function _my_method() { ... }, configurable: true, enumerable: false, writeable: true }); 

Cependant, si vous devez supporter les navigateurs ES3, il existe des problèmes avec les personnes qui utilisent for ... in boucles sur des chaînes.

Mon opinion est que vous pouvez changer les prototypes natifs et devrait cesser d'utiliser un code mal écrit qui se casse

Mise à jour: même ce code n'élargit pas complètement le type de String natif (la propriété length ne fonctionne pas).

Imo, il ne vaut pas la peine de suivre cette approche. Il y a trop de choses à considérer et vous devez investir trop de temps pour vous assurer que cela fonctionne pleinement ( si ce n'est pas le cas). @Raynos offre une autre approche intéressante .

Néanmoins, voici l'idée:


Il semble que vous ne pouvez pas appeler String.prototype.toString sur autre chose qu'une chaîne réelle. Vous pouvez remplacer cette méthode:

 // constructor function MyString(s) { String.call(this, s); // call the "parent" constructor this.s_ = s; } // create a new empty prototype to *not* override the original one tmp = function(){}; tmp.prototype = String.prototype; MyString.prototype = new tmp(); MyString.prototype.constructor = MyString; // new method MyString.prototype.reverse = function() { return this.split('').reverse().join(''); }; // override MyString.prototype.toString = function() { return this.s_; }; MyString.prototype.valueOf = function() { return this.s_; }; var s = new MyString("Foobar"); alert(s.reverse()); 

Comme vous le voyez, j'ai également dû annuler valueOf pour le faire fonctionner.

Mais: Je ne sais pas si ce sont les seules méthodes que vous devez remplacer et pour d'autres types intégrés, vous devrez peut-être remplacer d'autres méthodes. Un bon départ serait de prendre la spécification ECMAScript et d'examiner la spécification des méthodes.

Par exemple, la deuxième étape de l'algorithme String.prototype.split est:

Soit S le résultat de l'appel de ToString, lui donnant la valeur de cette valeur comme argument.

Si un objet est transmis à ToString , il appelle essentiellement la méthode toString de cet objet. Et c'est pourquoi cela fonctionne lorsque nous annulons toString .

Mise à jour: Ce qui ne fonctionne pas est en s.length . Donc, bien que vous pourriez être en mesure de faire fonctionner les méthodes , d'autres propriétés semblent être plus difficiles.

Tout d'abord, dans ce code:

 MyString.prototype = String.prototype; MyString.prototype.reverse = function() { this.split('').reverse().join(''); }; 

Les variables MyString.prototype et String.prototype font référence au même objet! Affecter à un est assigner à l'autre. Lorsque vous avez laissé tomber une méthode reverse dans MyString.prototype vous l'étiez également dans String.prototype . Essayez donc ceci:

 MyString.prototype = String.prototype; MyString.prototype.charAt = function () {alert("Haha");} var s = new MyString(); s.charAt(4); "dog".charAt(3); 

Les deux dernières lignes alerent tous deux parce que leurs prototypes sont le même objet. Vous avez réellement étendu String.prototype .

Maintenant à propos de votre erreur. Vous avez appelé l' reverse sur votre objet MyString . Où cette méthode est-elle définie? Dans le prototype, qui est identique à String.prototype . Vous avez écrasé l' reverse . Quelle est la première chose à faire? Il appelle split sur l'objet cible. Maintenant, la chose est, afin que String.prototype.split fonctionne, il doit appeler String.prototype.toString . Par exemple:

 var s = new MyString(); if (s.split("")) {alert("Hi");} 

Ce code génère une erreur:

 TypeError: String.prototype.toString is not generic 

Ce que cela signifie, c'est que String.prototype.toString utilise la représentation interne d'une chaîne pour faire son objet (à savoir renvoyer sa chaîne primitive interne) et ne peut pas être appliquée à des objets cibles arbitraires qui partagent le prototype de chaîne . Donc, lorsque vous avez appelé split , la mise en œuvre de split a déclaré "oh, ma cible n'est pas une chaîne, permettez-moi d'appeler toString ", mais ensuite, toString déclaré: "ma cible n'est pas une chaîne et je ne suis pas générique", donc il a lancé TypeError .

Si vous souhaitez en savoir plus sur les génériques en JavaScript, vous pouvez voir cette section MDN sur les génériques Array et String .

En ce qui concerne l'obtention de ce travail sans l'erreur, voir la réponse d'Alxandr.

En ce qui concerne l'extension des types intégrés exacts tels que String et Date et ainsi de suite sans changer leurs prototypes, vous ne le faites pas vraiment, sans créer des wrappers ou des délégués ou des sous-classes. Mais cela n'autorisera pas la syntaxe comme

 d1.itervalTo(d2) 

d1 et d2 sont des instances de la classe de Date intégrée dont le prototype ne s'est pas étendu . 🙂 JavaScript utilise des chaînes de prototypes pour ce type de méthode de syntaxe d'appel. Il le fait tout simplement. Excellente question cependant … mais est-ce ce que vous avez pensé?

Vous n'avez qu'une bonne partie ici. MyString.prototype ne doit pas être String.prototype, il devrait être comme ceci:

 function MyString(s) { } MyString.prototype = new String(); MyString.prototype.reverse = function() { this.split('').reverse().join(''); }; var s = new MyString("Foobar"); s.reverse(); 

[Modifier]

Pour répondre mieux à votre question, il ne devrait pas être possible. Si vous examinez ceci: https://developer.mozilla.org/fr/JavaScript/Reference/Global_Objects/Object/constructor, il explique que vous ne pouvez pas modifier le type sur bools, ints et strings, donc ils ne peuvent pas être "Sous-classé".

Je pense que la réponse de base est que vous ne pouvez probablement pas. Ce que vous pouvez faire, c'est ce qu'est Sugar.js: créez un objet semblable à un objet et profitez de cela:

http://sugarjs.com/

Sugar.js concerne toutes les extensions d'objets natifs, et ils n'étendent pas Object.prototype.