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.



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:


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]

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.


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.


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.


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

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.



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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.