JavaScript: przeciążanie operatorów

95

Pracuję z JavaScript od kilku dni i doszedłem do punktu, w którym chcę przeciążać operatory dla moich zdefiniowanych obiektów.

Po krótkiej przerwie w wyszukiwarce Google wydaje się, że oficjalnie nie możesz tego zrobić, ale jest kilka osób, które twierdzą, że jest to długotrwały sposób wykonania tej czynności.

Zasadniczo stworzyłem klasę Vector2 i chcę móc wykonać następujące czynności:

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.

Zamiast tego muszę to zrobić:

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. 

Czy istnieje podejście, które mogę zastosować w celu przeciążenia operatorów w mojej klasie Vector2? Ponieważ to wygląda po prostu brzydko.

Lee Brindley
źródło
1
Właśnie natrafiłem na przeciążającą bibliotekę operatora. Nie próbowałem tego i nie wiem, jak dobrze to działa: google.com/…
fishinear

Odpowiedzi:

105

Jak już wiesz, JavaScript nie obsługuje przeciążania operatorów. Najbliższe możliwe jest zaimplementowanie toString(które zostanie wywołane, gdy instancja będzie musiała zostać zmuszona do bycia ciągiem znaków) i valueOf(która zostanie wywołana, aby przekształcić ją w liczbę, na przykład podczas +dodawania lub w wielu przypadkach, gdy używanie go do konkatenacji, ponieważ +próbuje dodać przed konkatenacją), co jest dość ograniczone. Żaden z nich nie pozwala Vector2w rezultacie utworzyć obiektu.


Jednak dla osób przychodzących do tego pytania, które chcą w rezultacie otrzymać ciąg lub liczbę (zamiast a Vector2), oto przykłady valueOfi toString. Te przykłady nie demonstrują przeciążenia operatorów, po prostu wykorzystują wbudowaną obsługę JavaScript do konwersji na prymitywy:

valueOf

Ten przykład podwaja wartość valwłaściwości obiektu w odpowiedzi na wymuszenie na prymityw, na przykład poprzez +:

Lub z ES2015 class:

Lub tylko z obiektami, bez konstruktorów:

toString

Ten przykład konwertuje wartość valwłaściwości obiektu na duże litery w odpowiedzi na wymuszenie na prymityw, na przykład poprzez +:

Lub z ES2015 class:

Lub tylko z obiektami, bez konstruktorów:

TJ Crowder
źródło
1
Chociaż nie jest to obsługiwane w ramach właściwego JS, w dzisiejszych czasach dość powszechne jest rozszerzanie JS o niestandardowe funkcje i transponowanie z powrotem do zwykłego JS, na przykład, SweetJS ma na celu rozwiązanie dokładnie tego problemu.
Dmitri Zaitsev
1
Czy operatory porównania w Dateklasie niejawnie konwertują daty na liczby przy użyciu valueOf? Na przykład możesz to zrobić date2 > date1i będzie to prawdą, jeśli date2został utworzony później date1.
Sean Letendre
1
@SeanLetendre: Tak. >, <, >=, I <=(ale nie ==, ===, !=, lub !==) używać Abstract relacyjny Porównanie działania, który korzysta ToPrimitivez podpowiedzi „numer”. Na Dateobiekcie daje to getTimezwracaną liczbę (wartość milisekund od początku epoki).
TJ Crowder
24

Jak powiedział TJ, nie możesz przeciążać operatorów w JavaScript. Możesz jednak skorzystać z valueOffunkcji, aby napisać hack, który wygląda lepiej niż używanie funkcji takich jak za addkażdym razem, ale nakłada na wektor ograniczenia, że ​​xiy są między 0 a MAX_VALUE. Oto kod:

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

Następnie możesz napisać takie równania:

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
źródło
7
W zasadzie właśnie napisałeś kod metody OP add... Coś, czego nie chcieli zrobić.
Ian Brindley
16
@IanBrindley OP chciał przeciążać operatora, co jasno wskazuje, że planował napisać taką funkcję. OP obawiał się konieczności wywołania „add”, co jest nienaturalne; matematycznie dodawanie wektorów reprezentujemy +znakiem. To bardzo dobra odpowiedź pokazująca, jak uniknąć wywoływania nienaturalnej nazwy funkcji dla obiektów quasi-numerycznych.
Kittsil
1
@Kittsil Pytanie pokazuje, że używam już funkcji dodawania. Chociaż powyższa funkcja wcale nie jest zła, nie rozwiązała problemu, więc zgodziłbym się z Ianem.
Lee Brindley,
Na razie jest to jedyny możliwy sposób. Jedyną elastycznością jaką mamy w przypadku +operatora jest możliwość zwrócenia a Numberjako zamiennika jednego z operandów. Dlatego każda funkcja dodawania, która działa w Objectinstancjach, musi zawsze zakodować obiekt jako a Numberi ostatecznie go zdekodować.
Gershom
Zwróć uwagę, że zwróci to nieoczekiwany wynik (zamiast dać błąd) podczas mnożenia dwóch wektorów. Również współrzędne muszą być liczbami całkowitymi.
user202729
8

FYI paper.js rozwiązuje ten problem, tworząc PaperScript, samowystarczalny javascript o określonym zakresie z przeciążaniem operatorów wektorów, który następnie przetwarza z powrotem do javascript.

Ale pliki paperscript muszą być szczegółowo określone i przetworzone jako takie.

Joshua Penman
źródło
8

Właściwie istnieje jeden wariant JavaScript, który obsługuje przeciążanie operatorów. ExtendScript, język skryptowy używany przez aplikacje Adobe, takie jak Photoshop i Illustrator, ma przeciążenie operatorów. Możesz w nim napisać:

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;

Jest to opisane bardziej szczegółowo w „Przewodniku po narzędziach JavaScript Adobe Extendscript” (aktualne łącze tutaj ). Składnia najwyraźniej opierała się na (już dawno porzuconym) projekcie standardu ECMAScript.

J. Peterson
źródło
11
ExtendScript! = JavaScript
Andrio Skur
3
Dlaczego odpowiedź ExtendScript jest odrzucana, a odpowiedź PaperScript jest głosowana za? IMHO ta odpowiedź jest również dobra.
xmedeko
5

Możliwe jest wykonanie matematyki wektorowej z dwiema liczbami spakowanymi w jedną. Najpierw pokażę przykład, zanim wyjaśnię, jak to działa:

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

Korzystam z faktu, że jeśli przesuniesz nieco dwie liczby X razy, a następnie dodasz lub odejmiesz je przed przesunięciem z powrotem, otrzymasz ten sam wynik, jakbyś ich nie przesunął na początku. Podobnie mnożenie i dzielenie przez skalar działa symetrycznie dla przesuniętych wartości.

Liczba JavaScript ma 52 bity z dokładnością do liczb całkowitych (64-bitowe liczby zmiennoprzecinkowe), więc umieszczę jedną liczbę w wyższych dostępnych 26 bitach, a jedną w niższych. Kod jest trochę bardziej niechlujny, ponieważ chciałem obsługiwać podpisane numery.

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

Jedynym minusem, jaki widzę w tym przypadku, jest to, że x i y muszą mieścić się w zakresie + -33 miliony, ponieważ muszą mieścić się w 26 bitach każdy.

Rzeczy
źródło
Gdzie jest definicja vec_pack?
Obrzydliwe
1
@ Obrzydliwe Hmm przepraszam, wygląda na to, że zapomniałem to dodać ... To jest już naprawione :)
Stuffe
4

Chociaż nie jest to dokładna odpowiedź na pytanie, możliwe jest zaimplementowanie niektórych metod __magic__ w Pythonie przy użyciu symboli ES6

[Symbol.toPrimitive]()Metoda nie pozwala oznaczać wezwanie Vector.add(), ale pozwoli Ci używać składni takich jak 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
źródło
3

Możemy użyć hooków podobnych do Reacta, aby ocenić funkcję strzałki z różnymi wartościami z valueOfmetody w każdej iteracji.

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 używa tego samego pomysłu dla Vector3.

FTOH
źródło
2

Ciekawe jest również eksperymentalne przeciążanie operatorów bibliotek -js . Przeładowuje tylko w określonym kontekście (funkcja wywołania zwrotnego).

xmedeko
źródło
1

Napisałem bibliotekę, która wykorzystuje kilka złych hacków, aby zrobić to w surowym JS. Pozwala na takie wyrażenia.

  • Liczby zespolone:

    >> Complex()({r: 2, i: 0} / {r: 1, i: 1} + {r: -3, i: 2}))

    <- {r: -2, i: 1}

  • Automatyczne różnicowanie:

    Niech f(x) = x^3 - 5x:

    >> var f = x => Dual()(x * x * x - {x:5, dx:0} * x);

    Teraz zmapuj to na niektóre wartości:

    >> [-2,-1,0,1,2].map(a=>({x:a,dx:1})).map(f).map(a=>a.dx)

    <- [ 7, -2, -5, -2, 7 ]

    to znaczy f'(x) = 3x^2 - 5.

  • Wielomiany:

    >> Poly()([1,-2,3,-4]*[5,-6]).map((c,p)=>''+c+'x^'+p).join(' + ')

    <- "5x^0 + -16x^1 + 27x^2 + -38x^3 + 24x^4"

W przypadku konkretnego problemu zdefiniowałbyś Vector2funkcję (lub może coś krótszego) za pomocą biblioteki, a następnie napisałx = Vector2()(x + y);

https://gist.github.com/pyrocto/5a068100abd5ff6dfbe69a73bbc510d7

Mike Zostań
źródło