Javascript: sobrecarga del operador

93

He estado trabajando con JavaScript durante unos días y he llegado a un punto en el que quiero sobrecargar operadores para mis objetos definidos.

Después de una temporada en Google buscando esto, parece que no puede hacer esto oficialmente, sin embargo, hay algunas personas que afirman una forma larga de realizar esta acción.

Básicamente, hice una clase Vector2 y quiero poder hacer lo siguiente:

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.

En cambio, tengo que hacer esto:

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 algún enfoque que pueda adoptar para sobrecargar operadores en mi clase Vector2? Como esto simplemente se ve feo.

Lee Brindley
fuente
1
Me encontré con un operador sobrecargando la biblioteca. Sin embargo, no lo he probado y no sé qué tan bien funciona: google.com/…
fishinear

Respuestas:

102

Como ha descubierto, JavaScript no admite la sobrecarga de operadores. Lo más cerca que puede llegar es implementar toString(que se llamará cuando la instancia deba ser forzada a ser una cadena) y valueOf(que se llamará para forzarla a un número, por ejemplo, cuando se usa +para sumar, o en muchos casos cuando usándolo para la concatenación porque +intenta hacer una suma antes de la concatenación), que es bastante limitado. Ninguno te permite crear un Vector2objeto como resultado.


Sin Vector2embargo, para las personas que responden a esta pregunta y quieren una cadena o un número como resultado (en lugar de una ), aquí hay ejemplos de valueOfy toString. Estos ejemplos no demuestran la sobrecarga de operadores, solo aprovechan el manejo integrado de JavaScript para convertir a primitivas:

valueOf

Este ejemplo duplica el valor de la valpropiedad de un objeto en respuesta a ser coaccionado a una primitiva, por ejemplo a través de +:

O con ES2015's class:

O simplemente con objetos, sin constructores:

toString

Este ejemplo convierte el valor de la valpropiedad de un objeto a mayúsculas en respuesta a ser coaccionado a una primitiva, por ejemplo a través de +:

O con ES2015's class:

O simplemente con objetos, sin constructores:

TJ Crowder
fuente
1
Si bien no es compatible con JS propiamente dicho, es bastante común en estos días extender JS con características personalizadas y transpile de nuevo a JS simple, por ejemplo, SweetJS tiene como objetivo abordar exactamente este problema.
Dmitri Zaitsev
1
¿Los operadores de comparación de la Dateclase convierten implícitamente las fechas en números usando valueOf? Por ejemplo, puede hacerlo date2 > date1y será cierto si date2se creó después date1.
Sean Letendre
1
@SeanLetendre: Sí. >, <, >=, Y <=(pero no ==, ===, !=, o !==) utilizar el Resumen Relational Comparación operación, que utiliza ToPrimitivecon "número" pista. En un Dateobjeto, eso da como resultado el número que getTimedevuelve (el valor de milisegundos desde la época).
TJ Crowder
23

Como dijo TJ, no puede sobrecargar operadores en JavaScript. Sin embargo, puede aprovechar la valueOffunción para escribir un truco que se ve mejor que usar funciones como addsiempre, pero impone las restricciones en el vector de que xey están entre 0 y MAX_VALUE. Aquí está el 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);
};

Entonces puedes escribir ecuaciones 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]
usuario2259659
fuente
7
Básicamente, acaba de escribir el código para el addmétodo del OP ... Algo que no querían hacer.
Ian Brindley
15
@IanBrindley El OP quería sobrecargar a un operador, lo que claramente implica que planeaba escribir dicha función. La preocupación de OP era tener que llamar "agregar", lo cual no es natural; matemáticamente, representamos la suma de vectores con un +signo. Esta es una muy buena respuesta que muestra cómo evitar llamar a un nombre de función no natural para objetos cuasi numéricos.
Kittsil
1
@Kittsil La pregunta muestra que ya estoy usando una función de adición. Aunque la función anterior no es una mala función en absoluto, no abordó la pregunta, así que estoy de acuerdo con Ian.
Lee Brindley
Hasta el momento, esta es la única forma posible. La única flexibilidad que tenemos con el +operador es la capacidad de devolver un Numbercomo reemplazo de uno de los operandos. Por lo tanto, cualquier funcionalidad de adición que funcione con Objectinstancias siempre debe codificar el objeto como a Numbery, finalmente, decodificarlo.
Gershom
Tenga en cuenta que esto devolverá un resultado inesperado (en lugar de dar un error) al multiplicar dos vectores. Además, las coordenadas deben ser enteras.
user202729
8

FYI paper.js resuelve este problema mediante la creación de PaperScript, un javascript autónomo y de alcance con una sobrecarga de vectores de operador, que luego procesa de nuevo en javascript.

Pero los archivos de paperscript deben especificarse y procesarse específicamente como tales.

Joshua Penman
fuente
6

En realidad, hay una variante de JavaScript que hace la sobrecarga de operadores de soporte. ExtendScript, el lenguaje de secuencias de comandos utilizado por aplicaciones de Adobe como Photoshop e Illustrator, tiene una sobrecarga de operadores. En él, puedes escribir:

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;

Esto se describe con más detalle en la "Guía de herramientas de JavaScript Extendscript de Adobe" ( enlace actual aquí ). La sintaxis aparentemente se basó en un borrador (ahora abandonado) del estándar ECMAScript.

J. Peterson
fuente
9
ExtendScript! = JavaScript
Andrio Skur
1
¿Por qué la respuesta de ExtendScript se vota negativamente mientras que la respuesta de PaperScript se vota a favor? En mi humilde opinión, esta respuesta también es buena.
xmedeko
5

Es posible hacer matemáticas vectoriales con dos números empaquetados en uno. Permítanme mostrarles un ejemplo antes de explicar cómo 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");

Estoy usando el hecho de que si cambias dos números X veces y luego los sumas o restas antes de cambiarlos, obtendrás el mismo resultado que si no los hubieras cambiado al principio. De manera similar, la multiplicación y división escalar funciona simétricamente para valores desplazados.

Un número de JavaScript tiene 52 bits de precisión entera (flotantes de 64 bits), por lo que empaquetaré un número en los 26 bits disponibles más altos y uno en los más bajos. El código se complica un poco más porque quería admitir números firmados.

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

El único inconveniente que puedo ver con esto es que xey deben estar en el rango + -33 millones, ya que deben caber dentro de 26 bits cada uno.

Stuffe
fuente
¿Dónde está la definición de vec_pack?
Asqueroso
1
@ Asqueroso Hmm lo siento, parece que me había olvidado de agregar eso ... Eso ya está arreglado :)
Stuffe
3

Si bien no es una respuesta exacta a la pregunta, es posible implementar algunos de los métodos __magic__ de python utilizando los símbolos ES6

Un [Symbol.toPrimitive]()método no le permite implicar una llamada Vector.add(), pero le permitirá usar una sintaxis 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
fuente
2

También es interesante el operador de biblioteca experimental -overloading-js . Realiza la sobrecarga en un contexto definido (función de devolución de llamada) únicamente.

xmedeko
fuente
2

Podemos usar Hooks similares a React para evaluar la función de flecha con diferentes valores del valueOfmétodo en cada iteración.

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 usa la misma idea para Vector3.

FTOH
fuente