Javascript: перегрузка оператора

95

Я работаю с JavaScript уже несколько дней и дошел до того момента, когда хочу перегрузить операторы для моих определенных объектов.

После того, как вы искали это в Google, кажется, что вы не можете официально сделать это, но есть несколько людей, которые заявляют о каком-то длинном способе выполнения этого действия.

По сути, я создал класс Vector2 и хочу иметь возможность делать следующее:

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.

Вместо этого мне нужно сделать это:

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. 

Есть ли подход, который я могу использовать для перегрузки операторов в моем классе Vector2? Поскольку это выглядит просто уродливо.

Ли Бриндли
источник
1
Только что наткнулся на библиотеку перегрузки операторов. Однако не пробовал и не знаю, насколько хорошо это работает: google.com/…
fishinear

Ответы:

105

Как вы обнаружили, JavaScript не поддерживает перегрузку операторов. Самое близкое, что вы можете придумать, - это реализовать toString(который будет вызываться, когда необходимо привести экземпляр к строке) и valueOf(который будет вызываться для приведения его к числу, например, при использовании +для сложения или во многих случаях, когда используя его для конкатенации, потому что +пытается выполнить сложение перед конкатенацией), что довольно ограничено. Ни то, ни другое не позволяет создавать в результате Vector2объект.


Vector2Тем не менее, для людей, которые задаются этим вопросом и хотят получить в качестве результата строку или число (вместо a ), вот примеры valueOfи toString. Эти примеры не демонстрируют перегрузку оператора, а просто используют встроенную обработку преобразования JavaScript в примитивы:

valueOf

В этом примере значение valсвойства объекта удваивается в ответ на принуждение к примитиву, например, через +:

Или с ES2015 class:

Или просто с объектами, без конструкторов:

toString

В этом примере значение valсвойства объекта преобразуется в верхний регистр в ответ на принуждение к примитиву, например, через +:

Или с ES2015 class:

Или просто с объектами, без конструкторов:

TJ Crowder
источник
1
Хотя это не поддерживается собственно JS, в наши дни довольно распространено расширять JS с помощью пользовательских функций и переносить обратно на простой JS, например, SweetJS стремится решить именно эту проблему.
Дмитрий Зайцев
1
Операторы сравнения в Dateклассе неявно преобразуют даты в числа с помощью valueOf? Например, вы можете это сделать, date2 > date1и это будет верно, если он date2был создан после date1.
Шон
1
@SeanLetendre: Да. >, <, >=, И <=(но не ==, ===,!= или !==) использовать абстрактную Реляционную сравнению операцию, в которой используется ToPrimitiveс подсказкой «номером». Для Dateобъекта это приводит к getTimeвозвращаемому числу (значение миллисекунд с начала эпохи).
TJ Crowder
24

Как сказал TJ, вы не можете перегружать операторы в JavaScript. Однако вы можете воспользоваться этой valueOfфункцией, чтобы написать хакер, который выглядит лучше, чем использование функций, как addкаждый раз, но налагает ограничения на вектор, заключающиеся в том, что x и y находятся в диапазоне от 0 до MAX_VALUE. Вот код:

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

Тогда вы можете написать такие уравнения:

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
источник
7
По сути, вы только что написали код для addметода OP ... То, что они не хотели делать.
Ян Бриндли
16
@IanBrindley ОП хотел перегрузить оператора, что явно подразумевает, что он планировал написать такую ​​функцию. OP беспокоился о том, чтобы вызвать «добавить», что неестественно; математически мы представляем сложение векторов +знаком. Это очень хороший ответ, показывающий, как избежать вызова неестественного имени функции для квазичисловых объектов.
Китсил
1
@Kittsil Вопрос показывает, что я уже использую функцию добавления. Хотя указанная выше функция вовсе не плохая, она не решила этот вопрос, поэтому я согласен с Яном.
Ли Бриндли
Пока это единственно возможный путь. Единственная гибкость, которую мы имеем с +оператором, - это возможность возвращать a Numberв качестве замены для одного из операндов. Поэтому любая добавляющая функциональность, которая работает с Objectэкземплярами, всегда должна кодировать объект как a Numberи в конечном итоге декодировать его.
Gershom
Обратите внимание, что это вернет неожиданный результат (вместо ошибки) при умножении двух векторов. Также координаты должны быть целыми.
user202729
8

FYI paper.js решает эту проблему, создав PaperScript, автономный javascript с ограниченной областью видимости с перегрузкой векторов операторами, которые затем обрабатываются обратно в javascript.

Но файлы документов должны быть специально указаны и обработаны как таковые.

Джошуа Пенман
источник
8

Фактически, есть один вариант JavaScript, который поддерживает перегрузку операторов. ExtendScript, язык сценариев, используемый приложениями Adobe, такими как Photoshop и Illustrator, действительно имеет перегрузку операторов. В нем вы можете написать:

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;

Более подробно это описано в «Руководстве по инструментам Adobe Extendscript JavaScript» (текущая ссылка здесь ). Синтаксис, по-видимому, был основан на (теперь давно заброшенном) проекте стандарта ECMAScript.

Дж. Петерсон
источник
11
ExtendScript! = JavaScript
Андрио
3
Почему за ответ ExtendScript проголосовали против, а за ответ PaperScript - за? ИМХО этот ответ тоже хорош.
xmedeko
5

Можно выполнять векторные математические вычисления с двумя числами, упакованными в одно. Позвольте мне сначала показать пример, прежде чем я объясню, как это работает:

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

Я использую тот факт, что если вы сдвинете два числа X раз по битам, а затем сложите или вычтите их перед обратным смещением, вы получите тот же результат, как если бы вы не сдвигали их с самого начала. Точно так же скалярное умножение и деление работают симметрично для сдвинутых значений.

Число JavaScript имеет 52 бита целочисленной точности (64-битные числа с плавающей запятой), поэтому я упакую одно число в более высокие доступные 26 бит, а другое - в более низкие. Код стал немного более запутанным, потому что я хотел поддерживать числа со знаком.

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

Единственный недостаток, который я вижу в этом, заключается в том, что x и y должны находиться в диапазоне + -33 миллиона, так как они должны соответствовать 26 битам каждый.

Stuffe
источник
Где определение vec_pack?
Отвратительно,
1
@Disgusting Хммм, извините, похоже, я забыл добавить это ... Теперь это исправлено :)
Stuffe
4

Хотя это и не является точным ответом на вопрос, можно реализовать некоторые методы python __magic__, используя символы ES6.

[Symbol.toPrimitive]()Метод не позволяет подразумевает вызов Vector.add(), но позволит вам использовать синтаксис , такие как 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

Мы можем использовать React-подобные хуки для оценки стрелочной функции с разными значениями из valueOfметода на каждой итерации.

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

Библиотека @ js-basics / vector использует ту же идею для Vector3.

FTOH
источник
2

Интересна также экспериментальная библиотека operator-overloading-js . Он выполняет перегрузку только в определенном контексте (функция обратного вызова).

xmedeko
источник
1

Я написал библиотеку, которая использует кучу злых уловок, чтобы сделать это на необработанном JS. Он позволяет такие выражения.

  • Сложные числа:

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

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

  • Автоматическая дифференциация:

    Пусть f(x) = x^3 - 5x:

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

    Теперь сопоставьте его с некоторыми значениями:

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

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

    т.е. f'(x) = 3x^2 - 5.

  • Полиномы:

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

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

Для вашей конкретной проблемы вы должны определить Vector2функцию (или, может быть, что-то более короткое) с помощью библиотеки, а затем написатьx = Vector2()(x + y);

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

Майк Стэй
источник