Javascript: sobrecarga de operador

93

Estou trabalhando com JavaScript há alguns dias e cheguei a um ponto em que quero sobrecarregar os operadores dos meus objetos definidos.

Depois de um período no Google procurando por isso, parece que você não pode fazer isso oficialmente, mas existem algumas pessoas por aí que afirmam que é uma forma prolixa de realizar essa ação.

Basicamente, criei uma classe Vector2 e quero ser capaz de fazer o seguinte:

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.

Em vez disso, estou tendo que fazer isso:

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 uma abordagem que posso adotar para sobrecarregar os operadores na minha classe Vector2? Como isso parece simplesmente feio.

Lee Brindley
fonte
2
possível duplicata de Overloading Arithmetic Operators in JavaScript?
Pointy
1
Acabei de encontrar uma biblioteca que sobrecarrega o operador. Ainda não experimentei e não sei se funciona bem: google.com/…
fishinear

Respostas:

102

Como você descobriu, o JavaScript não oferece suporte à sobrecarga de operador. O mais próximo que você pode chegar é implementar toString(que será chamado quando a instância precisar ser coagida a ser uma string) e valueOf(que será chamado para forçá-la a um número, por exemplo, ao usar +para adição, ou em muitos casos quando usá-lo para concatenação porque +tenta fazer adição antes da concatenação), que é bastante limitado. Nenhum dos dois permite criar um Vector2objeto como resultado.


Para as pessoas que chegam a esta questão e desejam uma string ou número como resultado (em vez de a Vector2), porém, aqui estão alguns exemplos de valueOfe toString. Esses exemplos não demonstram a sobrecarga do operador, apenas aproveitando as vantagens da manipulação integrada do JavaScript na conversão em primitivos:

valueOf

Este exemplo dobra o valor da valpropriedade de um objeto em resposta a ser forçado a um primitivo, por exemplo, por meio de +:

Ou com ES2015's class:

Ou apenas com objetos, sem construtores:

toString

Este exemplo converte o valor da valpropriedade de um objeto em maiúsculas em resposta a ser forçado a um primitivo, por exemplo, por meio de +:

Ou com ES2015's class:

Ou apenas com objetos, sem construtores:

TJ Crowder
fonte
1
Embora não seja compatível com o JS, é bastante comum hoje em dia estender o JS com recursos personalizados e transpilar de volta para o JS simples, por exemplo, o SweetJS tem como objetivo tratar exatamente desse problema.
Dmitri Zaitsev
1
Os operadores de comparação na Dateclasse convertem implicitamente datas em números usando valueOf? Por exemplo, você pode fazer date2 > date1e será verdade se date2foi criado depois date1.
Sean Letendre
1
@SeanLetendre: Sim. >, <, >=, E <=(mas não ==, ===, !=, ou !==) use o Resumo relacional Comparação operação, que usa ToPrimitivecom "número" dica. Em um Dateobjeto, isso resulta no número que getTimeretorna (o valor em milissegundos desde a época).
TJ Crowder
23

Como TJ disse, você não pode sobrecarregar operadores em JavaScript. No entanto, você pode aproveitar a vantagem da valueOffunção para escrever um hack que parece melhor do que usar funções como addtodas as vezes, mas impõe as restrições ao vetor de que xey estão entre 0 e MAX_VALUE. Aqui está o código:

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

Então você pode escrever equações como esta:

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
fonte
7
Você basicamente acabou de escrever o código para o addmétodo do OP ... Algo que eles não queriam fazer.
Ian Brindley
15
@IanBrindley O OP queria sobrecarregar um operador, o que implica claramente que ele planejava escrever tal função. A preocupação de OP era ter que chamar "adicionar", o que não é natural; matematicamente, representamos a adição de vetores com um +sinal. Esta é uma resposta muito boa que mostra como evitar chamar um nome de função não natural para objetos quase numéricos.
Kittsil de
1
@Kittsil A pergunta mostra que já estou usando uma função de adição. Embora a função acima não seja uma função ruim, ela não respondeu à pergunta, então eu concordo com Ian.
Lee Brindley
Por enquanto, esta é a única maneira possível. A única flexibilidade que temos com o +operador é a capacidade de retornar um Numberem substituição a um dos operandos. Portanto, qualquer funcionalidade adicional que funcione como Objectinstâncias deve sempre codificar o objeto como um Numbere, eventualmente, decodificá-lo.
Gershom
Observe que isso retornará um resultado inesperado (em vez de dar um erro) ao multiplicar dois vetores. Além disso, as coordenadas devem ser inteiras.
user202729
8

FYI paper.js resolve esse problema criando PaperScript, um javascript autocontido com escopo e sobrecarga de operadores de vetores, que é então processado de volta para javascript.

Mas os arquivos paperscript precisam ser especificamente especificados e processados ​​como tal.

Joshua Penman
fonte
6

Na verdade, existe uma variante de JavaScript que oferece suporte à sobrecarga de operador. ExtendScript, a linguagem de script usada por aplicativos Adobe, como Photoshop e Illustrator, tem sobrecarga de operador. Nele, você pode escrever:

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;

Isso é descrito com mais detalhes no "Guia de ferramentas Adobe Extendscript JavaScript" ( link atual aqui ). A sintaxe foi aparentemente baseada em um rascunho (agora abandonado) do padrão ECMAScript.

J. Peterson
fonte
9
ExtendScript! = JavaScript
Andrio Skur
1
Por que a resposta de ExtendScript foi rejeitada enquanto a resposta de PaperScript foi rejeitada? IMHO esta resposta também é boa.
xmedeko
4

É possível fazer matemática vetorial com dois números agrupados em um. Deixe-me mostrar um exemplo antes de explicar como funciona:

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

Estou usando o fato de que, se você deslocar os bits dois números X vezes e, em seguida, adicionar ou subtraí-los antes de deslocá-los de volta, obterá o mesmo resultado como se não tivesse mudado para começar. Da mesma forma, a multiplicação e divisão escalar funcionam simetricamente para valores deslocados.

Um número JavaScript tem 52 bits de precisão inteira (flutuantes de 64 bits), então empacotarei um número nos 26 bits mais altos disponíveis e um nos mais baixos. O código está um pouco mais confuso porque eu queria oferecer suporte a números assinados.

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

A única desvantagem que vejo com isso é que x e y devem estar na faixa de + -33 milhões, já que eles devem caber em 26 bits cada.

Stuffe
fonte
Onde está a definição de vec_pack?
Nojento
1
@Disgusting Hmm desculpe, parece que esqueci de adicionar isso ... Isso agora foi corrigido :)
Coisas de
3

Embora não seja uma resposta exata para a pergunta, é possível implementar alguns dos métodos python __magic__ usando os símbolos ES6

Um [Symbol.toPrimitive]()método não permite que você implique uma chamada Vector.add(), mas permitirá que você use uma sintaxe como 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
fonte
2

Interessante também é a biblioteca experimental operator-overheading-js . Ele faz sobrecarga em um contexto definido (função de retorno de chamada) apenas.

xmedeko
fonte
2

Podemos usar Ganchos semelhantes ao React para avaliar a função da seta com diferentes valores do valueOfmétodo em cada iteração.

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

A Library @ js-basics / vector usa a mesma ideia para o Vector3.

FTOH
fonte