Javascript: Überladung des Operators

92

Ich arbeite jetzt seit einigen Tagen mit JavaScript und habe einen Punkt erreicht, an dem ich Operatoren für meine definierten Objekte überladen möchte.

Nach einer kurzen Zeit bei Google, in der Sie danach gesucht haben, können Sie dies anscheinend nicht offiziell tun, aber es gibt einige Leute da draußen, die eine langwierige Art und Weise behaupten, diese Aktion auszuführen.

Grundsätzlich habe ich eine Vector2-Klasse erstellt und möchte Folgendes tun können:

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.

Stattdessen muss ich Folgendes tun:

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. 

Gibt es einen Ansatz, mit dem ich Operatoren in meiner Vector2-Klasse überladen kann? Da sieht das einfach nur hässlich aus.

Lee Brindley
quelle
1
Ich bin gerade auf einen Operator gestoßen, der die Bibliothek überlastet. Ich habe es noch nicht ausprobiert und weiß nicht, wie gut es funktioniert: google.com/…
fishinear

Antworten:

100

Wie Sie festgestellt haben, unterstützt JavaScript das Überladen von Operatoren nicht. Das nächste, was Sie erreichen können, ist die Implementierung toString(die aufgerufen wird, wenn die Instanz zu einer Zeichenfolge gezwungen werden muss) und valueOf(die aufgerufen wird, um sie zu einer Zahl zu zwingen, z. B. wenn sie +zum Hinzufügen verwendet wird, oder in vielen Fällen, wenn Verwenden Sie es für die Verkettung, da +versucht wird, vor der Verkettung eine Addition vorzunehmen, was ziemlich begrenzt ist. Sie können auch kein Vector2Objekt als Ergebnis erstellen .


Für Leute, die zu dieser Frage kommen und als Ergebnis eine Zeichenfolge oder Zahl (anstelle von a Vector2) möchten , sind hier Beispiele für valueOfund toString. Diese Beispiele zeigen keine Operatorüberladung, sondern nutzen lediglich die integrierte Verarbeitung von JavaScript zur Konvertierung in Grundelemente:

valueOf

In diesem Beispiel wird der Wert der valEigenschaft eines Objekts verdoppelt , wenn es zu einem Grundelement gezwungen wird, beispielsweise über +:

Oder mit ES2015 class:

Oder nur mit Objekten, keine Konstruktoren:

toString

In diesem Beispiel wird der Wert der valEigenschaft eines Objekts in Großbuchstaben umgewandelt, wenn er zu einem Grundelement gezwungen wird, beispielsweise über +:

Oder mit ES2015 class:

Oder nur mit Objekten, keine Konstruktoren:

TJ Crowder
quelle
1
Obwohl es in JS nicht unterstützt wird, ist es heutzutage durchaus üblich, JS mit benutzerdefinierten Funktionen zu erweitern und wieder auf einfaches JS zu übertragen. SweetJS zielt beispielsweise darauf ab, genau dieses Problem zu lösen.
Dmitri Zaitsev
1
DateKonvertieren die Vergleichsoperatoren der Klasse implizit Datumsangaben mit in valueOf? Zum Beispiel können Sie tun date2 > date1und es wird wahr sein, wenn date2nach erstellt wurde date1.
Sean Letendre
1
@ SeanLetendre: Ja. >, <, >=, Und <=(aber nicht ==, ===, !=, oder !==verwenden) , um die Zusammenfassung Relational Vergleich Operation, die Anwendungen ToPrimitivemit Hinweis „Nummer“. Bei einem DateObjekt ergibt sich die zurückgegebene Zahl getTime(der Millisekunden-seit-der-Epoche-Wert).
TJ Crowder
23

Wie TJ sagte, können Sie Operatoren in JavaScript nicht überladen. Sie können die valueOfFunktion jedoch nutzen, um einen Hack zu schreiben, der besser aussieht als die Verwendung von Funktionen wie addjedes Mal, aber dem Vektor die Einschränkungen auferlegt, dass x und y zwischen 0 und MAX_VALUE liegen. Hier ist der 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);
};

Dann können Sie Gleichungen wie folgt schreiben:

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
quelle
6
Sie haben im Grunde nur den Code für die OP- addMethode geschrieben ... Etwas, das sie nicht tun wollten.
Ian Brindley
15
@IanBrindley Das OP wollte einen Operator überlasten, was eindeutig impliziert, dass er vorhatte, eine solche Funktion zu schreiben. OPs Anliegen war es, "add" nennen zu müssen, was unnatürlich ist; Mathematisch stellen wir die Vektoraddition mit einem +Vorzeichen dar. Dies ist eine sehr gute Antwort, die zeigt, wie Sie vermeiden können, einen unnatürlichen Funktionsnamen für quasi-numerische Objekte aufzurufen.
Kittsil
1
@ Kittsil Die Frage zeigt, dass ich bereits eine Add-Funktion verwende. Obwohl die obige Funktion überhaupt keine schlechte Funktion ist, hat sie die Frage nicht beantwortet, also würde ich Ian zustimmen.
Lee Brindley
Bisher ist dies der einzig mögliche Weg. Die einzige Flexibilität, die wir mit dem +Operator haben, ist die Möglichkeit, a Numberals Ersatz für einen der Operanden zurückzugeben. Daher muss jede Addierfunktion, die ObjectInstanzen funktioniert, das Objekt immer als Numberund schließlich codieren .
Gershom
Beachten Sie, dass dies beim Multiplizieren von zwei Vektoren ein unerwartetes Ergebnis zurückgibt (anstatt einen Fehler zu geben). Auch die Koordinaten müssen ganzzahlig sein.
user202729
8

FYI paper.js löst dieses Problem, indem PaperScript erstellt wird, ein in sich geschlossenes Javascript mit Gültigkeitsbereich und Überladung von Vektoren durch den Operator, das dann wieder in Javascript verarbeitet wird.

Die Paperscript-Dateien müssen jedoch speziell spezifiziert und als solche verarbeitet werden.

Joshua Penman
quelle
6

Tatsächlich gibt es eine Variante von JavaScript, die das Überladen von Operatoren unterstützt. ExtendScript, die Skriptsprache, die von Adobe-Anwendungen wie Photoshop und Illustrator verwendet wird, ist vom Operator überladen. Darin können Sie schreiben:

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;

Dies wird ausführlicher im "Adobe Extendscript JavaScript Tools Guide" (aktueller Link hier ) beschrieben. Die Syntax basierte offenbar auf einem (inzwischen längst aufgegebenen) Entwurf des ECMAScript-Standards.

J. Peterson
quelle
8
ExtendScript! = JavaScript
Andrio Skur
1
Warum wird die ExtendScript-Antwort abgelehnt, während die PaperScript-Antwort positiv bewertet wird? IMHO ist diese Antwort auch gut.
xmedeko
4

Es ist möglich, Vektormathematik mit zwei Zahlen in einer zu machen. Lassen Sie mich zunächst ein Beispiel zeigen, bevor ich erkläre, wie es funktioniert:

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");

Ich benutze die Tatsache, dass Sie das gleiche Ergebnis erhalten, wenn Sie zwei Zahlen X-mal verschieben und sie dann addieren oder subtrahieren, bevor Sie sie zurückschieben, als hätten Sie sie zunächst nicht verschoben. In ähnlicher Weise funktioniert die skalare Multiplikation und Division für verschobene Werte symmetrisch.

Eine JavaScript-Zahl hat eine Ganzzahlgenauigkeit von 52 Bit (64-Bit-Gleitkommazahlen), daher packe ich eine Zahl in die höheren verfügbaren 26 Bits und eine in die niedrigeren. Der Code ist etwas chaotischer, weil ich signierte Nummern unterstützen wollte.

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;
    }
}

Der einzige Nachteil, den ich dabei sehen kann, ist, dass x und y im Bereich von + -33 Millionen liegen müssen, da sie jeweils in 26 Bit passen müssen.

Stuffe
quelle
Wo ist die Definition von vec_pack?
Ekelhafter
1
@ Ekelhaft Hmm sorry, sieht so aus, als hätte ich vergessen das hinzuzufügen ... Das ist jetzt behoben :)
Stuffe
2

Interessant ist auch das experimentelle Überladen von js durch Bibliotheksoperatoren . Es wird nur in einem definierten Kontext (Rückruffunktion) überladen.

xmedeko
quelle
2

Wir können reaktionsähnliche Hooks verwenden, um die Pfeilfunktion bei valueOfjeder Iteration mit anderen Werten als die Methode zu bewerten .

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 -grundlagen / vector verwendet dieselbe Idee für Vector3.

FTOH
quelle
2

Obwohl dies keine genaue Antwort auf die Frage ist, ist es möglich, einige der Python __magic__ -Methoden mithilfe von ES6-Symbolen zu implementieren

Mit einer [Symbol.toPrimitive]()Methode können Sie keinen Aufruf implizieren Vector.add(), aber Sie können Syntax wie z 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
quelle