Javascript: surcharge des opérateurs

93

Je travaille avec JavaScript depuis quelques jours maintenant et je suis arrivé à un point où je veux surcharger les opérateurs pour mes objets définis.

Après un passage sur Google à la recherche de cela, il semble que vous ne puissiez pas le faire officiellement, mais il y a quelques personnes qui revendiquent une manière de longue haleine d'effectuer cette action.

En gros, j'ai créé une classe Vector2 et je souhaite pouvoir effectuer les opérations suivantes:

var x = new Vector2(10,10);
var y = new Vector2(10,10);

x += y; //This does not result in x being a vector with 20,20 as its x & y values.

Au lieu de cela, je dois faire ceci:

var x = new Vector2(10,10);
var y = new Vector2(10,10);

x = x.add(y); //This results in x being a vector with 20,20 as its x & y values. 

Existe-t-il une approche que je peux adopter pour surcharger les opérateurs dans ma classe Vector2? Comme cela semble tout simplement moche.

Lee Brindley
la source
2
duplication possible des opérateurs arithmétiques
Pointy
1
Je viens de tomber sur une bibliothèque de surcharge d'opérateur. Je ne l'ai pas essayé et je ne sais pas si cela fonctionne bien: google.com
...

Réponses:

102

Comme vous l'avez constaté, JavaScript ne prend pas en charge la surcharge d'opérateurs. Le plus proche que vous pouvez venir est d'implémenter toString(qui sera appelé lorsque l'instance doit être forcée à être une chaîne) et valueOf(qui sera appelé pour la forcer à un nombre, par exemple lors de l'utilisation +pour l'addition, ou dans de nombreux cas lorsque l'utiliser pour la concaténation car +essaie de faire une addition avant la concaténation), ce qui est assez limité. Ni l'un ni l'autre ne vous permet de créer un Vector2objet en conséquence.


Pour les personnes qui viennent à cette question et qui veulent une chaîne ou un nombre comme résultat (au lieu d'un Vector2), cependant, voici des exemples de valueOfet toString. Ces exemples ne montrent pas la surcharge des opérateurs, profitant simplement de la gestion intégrée de JavaScript lors de la conversion en primitives:

valueOf

Cet exemple double la valeur de la valpropriété d'un objet en réponse à une contrainte sur une primitive, par exemple via +:

Ou avec ES2015 class:

Ou juste avec des objets, pas de constructeurs:

toString

Cet exemple convertit la valeur de la valpropriété d'un objet en majuscules en réponse à une contrainte sur une primitive, par exemple via +:

Ou avec ES2015 class:

Ou juste avec des objets, pas de constructeurs:

TJ Crowder
la source
1
Bien qu'il ne soit pas pris en charge dans JS proprement dit, il est assez courant de nos jours d'étendre JS avec des fonctionnalités personnalisées et de le transpiler en JS ordinaire, par exemple, SweetJS vise à résoudre exactement ce problème.
Dmitri Zaitsev
1
Les opérateurs de comparaison de la Dateclasse convertissent-ils implicitement les dates en nombres en utilisant valueOf? Par exemple, vous pouvez le faire date2 > date1et ce sera vrai si a date2été créé après date1.
Sean Letendre
1
@SeanLetendre: Oui. >, <, >=Et <=(mais pas ==, ===, !=ou !==) utiliser le Résumé Relational Comparaison opération, qui utilise ToPrimitiveavec indication « nombre ». Sur un Dateobjet, cela donne le nombre qui getTimerenvoie (la valeur en millisecondes depuis l'époque).
TJ Crowder
23

Comme TJ l'a dit, vous ne pouvez pas surcharger les opérateurs en JavaScript. Cependant, vous pouvez profiter de la valueOffonction pour écrire un hack qui semble mieux que d'utiliser des fonctions comme à addchaque fois, mais qui impose des contraintes sur le vecteur que les x et y sont compris entre 0 et MAX_VALUE. Voici le code:

var MAX_VALUE = 1000000;

var Vector = function(a, b) {
    var self = this;
    //initialize the vector based on parameters
    if (typeof(b) == "undefined") {
        //if the b value is not passed in, assume a is the hash of a vector
        self.y = a % MAX_VALUE;
        self.x = (a - self.y) / MAX_VALUE;
    } else {
        //if b value is passed in, assume the x and the y coordinates are the constructors
        self.x = a;
        self.y = b;
    }

    //return a hash of the vector
    this.valueOf = function() {
        return self.x * MAX_VALUE + self.y;
    };
};

var V = function(a, b) {
    return new Vector(a, b);
};

Ensuite, vous pouvez écrire des équations comme celle-ci:

var a = V(1, 2);            //a -> [1, 2]
var b = V(2, 4);            //b -> [2, 4]
var c = V((2 * a + b) / 2); //c -> [2, 4]
user2259659
la source
7
En gros, vous venez d'écrire le code de la addméthode de l'OP ... Quelque chose qu'ils ne voulaient pas faire.
Ian Brindley
15
@IanBrindley L'OP voulait surcharger un opérateur, ce qui implique clairement qu'il prévoyait d'écrire une telle fonction. Le souci d'OP était de devoir appeler «ajouter», ce qui n'est pas naturel; mathématiquement, nous représentons l'addition de vecteurs avec un +signe. C'est une très bonne réponse qui montre comment éviter d'appeler un nom de fonction non naturel pour des objets quasi-numériques.
Kittsil
1
@Kittsil La question montre que j'utilise déjà une fonction d'ajout. Bien que la fonction ci-dessus ne soit pas du tout une mauvaise fonction, elle n'a pas répondu à la question, je suis donc d'accord avec Ian.
Lee Brindley le
Pour l'instant, c'est le seul moyen possible. La seule flexibilité dont nous disposons avec l' +opérateur est la possibilité de renvoyer a Numberen remplacement de l'un des opérandes. Par conséquent, toute fonctionnalité d'ajout qui fonctionne avec des Objectinstances doit toujours encoder l'objet en tant que Number, et finalement le décoder.
Gershom
Notez que cela renverra un résultat inattendu (au lieu de donner une erreur) lors de la multiplication de deux vecteurs. Les coordonnées doivent également être des nombres entiers.
user202729 le
8

FYI paper.js résout ce problème en créant PaperScript, un javascript autonome et à portée avec une surcharge d'opérateurs de vecteurs, qu'il traite ensuite en javascript.

Mais les fichiers paperscript doivent être spécifiquement spécifiés et traités comme tels.

Joshua Penman
la source
6

En fait, il existe une variante de JavaScript qui prend en charge la surcharge des opérateurs. ExtendScript, le langage de script utilisé par les applications Adobe telles que Photoshop et Illustrator, a une surcharge d'opérateurs. Dans celui-ci, vous pouvez écrire:

Vector2.prototype["+"] = function( b )
{
  return new Vector2( this.x + b.x, this.y + b.y );
}

var a = new Vector2(1,1);
var b = new Vector2(2,2);
var c = a + b;

Ceci est décrit plus en détail dans le "Guide des outils JavaScript d'Adobe Extendscript" ( lien actuel ici ). La syntaxe était apparemment basée sur un projet (maintenant abandonné depuis longtemps) de la norme ECMAScript.

J. Peterson
la source
9
ExtendScript! = JavaScript
Andrio Skur
1
Pourquoi la réponse ExtendScript est refusée alors que la réponse PaperScript est votée positivement? IMHO cette réponse est également bonne.
xmedeko le
5

Il est possible de faire des mathématiques vectorielles avec deux nombres regroupés en un seul. Permettez-moi d'abord de montrer un exemple avant d'expliquer comment cela fonctionne:

let a = vec_pack([2,4]);
let b = vec_pack([1,2]);

let c = a+b; // Vector addition
let d = c-b; // Vector subtraction
let e = d*2; // Scalar multiplication
let f = e/2; // Scalar division

console.log(vec_unpack(c)); // [3, 6]
console.log(vec_unpack(d)); // [2, 4]
console.log(vec_unpack(e)); // [4, 8]
console.log(vec_unpack(f)); // [2, 4]

if(a === f) console.log("Equality works");
if(a > b) console.log("Y value takes priority");

J'utilise le fait que si vous décalez un peu deux nombres X fois, puis ajoutez ou soustrayez-les avant de les recaler, vous obtiendrez le même résultat que si vous ne les aviez pas décalés au départ. De même, la multiplication et la division scalaires fonctionnent symétriquement pour les valeurs décalées.

Un nombre JavaScript a 52 bits de précision entière (flottants de 64 bits), donc je vais emballer un nombre dans les 26 bits disponibles les plus élevés, et un dans le plus bas. Le code est un peu plus compliqué car je voulais prendre en charge les nombres signés.

function vec_pack(vec){
    return vec[1] * 67108864 + (vec[0] < 0 ? 33554432 | vec[0] : vec[0]);
}

function vec_unpack(number){
    switch(((number & 33554432) !== 0) * 1 + (number < 0) * 2){
        case(0):
            return [(number % 33554432),Math.trunc(number / 67108864)];
        break;
        case(1):
            return [(number % 33554432)-33554432,Math.trunc(number / 67108864)+1];
        break;
        case(2):
            return [(((number+33554432) % 33554432) + 33554432) % 33554432,Math.round(number / 67108864)];
        break;
        case(3):
            return [(number % 33554432),Math.trunc(number / 67108864)];
        break;
    }
}

Le seul inconvénient que je peux voir avec cela est que le x et le y doivent être compris entre + -33 millions, car ils doivent tenir dans 26 bits chacun.

Stuffe
la source
Où est la définition de vec_pack?
Dégoûtant le
1
@Disgusting Hmm désolé, on dirait que j'avais oublié d'ajouter ça ... C'est maintenant corrigé :)
Stuffe
3

Bien que ce ne soit pas une réponse exacte à la question, il est possible d'implémenter certaines des méthodes python __magic__ en utilisant les symboles ES6

Une [Symbol.toPrimitive]()méthode ne vous permet pas d'impliquer un appel Vector.add(), mais vous permettra d'utiliser une syntaxe telle que Decimal() + int.

class AnswerToLifeAndUniverseAndEverything {
    [Symbol.toPrimitive](hint) {
        if (hint === 'string') {
            return 'Like, 42, man';
        } else if (hint === 'number') {
            return 42;
        } else {
            // when pushed, most classes (except Date)
            // default to returning a number primitive
            return 42;
        }
    }
}
James McGuigan
la source
2

Nous pouvons utiliser des Hooks de type React pour évaluer la fonction de flèche avec différentes valeurs de valueOfméthode à chaque itération.

const a = Vector2(1, 2) // [1, 2]
const b = Vector2(2, 4) // [2, 4]    
const c = Vector2(() => (2 * a + b) / 2) // [2, 4]
// There arrow function will iterate twice
// 1 iteration: method valueOf return X component
// 2 iteration: method valueOf return Y component

Library @ js-basics / vector utilise la même idée pour Vector3.

FTOH
la source