JavaScript равенство

Неверное равенство верно

Может ли в JavaScript конструкция (a==1 && a==2 && a==3) оказаться равной true
Сегодня мы разберём этот код и постараемся его понять.

    const a = {
        num: 0,
        valueOf: function() {
            return this.num += 1
        }
    };
    const equality = (a==1 && a==2 && a==3);
    console.log(equality); // true

Если вы используете Google Chrome, откройте консоль инструментов разработчика с помощью комбинации клавиш Ctrl + Shift + J в Windows, или Cmd + Opt + J в macOS. Скопируйте этот код, вставьте в консоль и убедитесь в том, что на выходе и правда получается true.

Где подвох

На самом деле, ничего удивительного тут нет. Просто этот код использует две базовые концепции JavaScript:

  • Оператор нестрогого равенства
  • Метод объекта valueOf()

Оператор нестрогого равенства

Обратите внимание на то, что в исследуемом выражении, (a==1 && a==2 && a==3), применяется оператор нестрогого равенства. Это означает, что в ходе вычисления значения этого выражения будет использоваться приведение типов, то есть, с помощью == сравнивать можно значения разных типов. Я уже много об этом писал, поэтому не буду тут вдаваться в подробности. Если вам нужно вспомнить особенности работы операторов сравнения в JS — обратитесь к этому материалу.

Метод valueOf()

В JavaScript имеется встроенный метод для преобразования объекта в примитивное значение: Object.prototype.valueOf(). По умолчанию этот метод возвращает объект, для которого он был вызван.
Создадим объект:

    const a = {
        num: 0
    }

Как сказано выше, когда мы вызываем valueOf() для объекта a, он просто возвращает сам объект:

    a.valueOf();
    // {num: 0}

Кроме того, мы можем использовать typeOf() для проверки того, действительно ли valueOf() возвращает объект:

    typeof a.valueOf();
    // "object"

Пишем свой valueOf()

Самое интересное при работе с valueOf() заключается в том, что мы можем этот метод переопределить для того, чтобы конвертировать с его помощью объект в примитивное значение. Другими словами, можно использовать valueOf() для возврата вместо объектов строк, чисел, логических значений, и так далее. Взгляните на следующий код:

    a.valueOf = function() {
        return this.num;
    }

Здесь мы заменили стандартный метод valueOf() для объекта a. Теперь при вызове valueOf() возвращается значение a.num.
Всё это ведёт к следующему:

    a.valueOf();
    // 0

Как видно, теперь valueOf() возвращает 0! Самое главное здесь то, что 0 — это то значение, которое назначено свойству объекта a.num. Мы можем в этом удостовериться, выполнив несколько тестов:

    typeof a.valueOf();
    // "number"
    a.num == a.valueOf()
    // true

Теперь поговорим о том, почему это важно.

Операция нестрогого равенства и приведение типов

При вычислении результата операции нестрогого равенства для операндов различных типов JavaScript попытается произвести приведение типов — то есть он сделает попытку привести (конвертировать) операнды к похожим типам или к одному и тому же типу.

В нашем выражении, (a==1 && a==2 && a==3), JavaScript попытается привести объект a к числовому типу перед сравнением его с числом. При выполнении операции приведения типа для объекта JavaScript, в первую очередь, попытается вызвать метод valueOf().

Так как мы изменили стандартный метод valueOf() так, что теперь он возвращает значение a.num, которое является числом, теперь мы можем сделать следующее:

    a == 0
    // true

Неужто задача решена? Пока нет, но осталось — всего ничего.

Оператор присваивания со сложением

Теперь нам нужен способ систематически увеличивать значение a.num каждый раз, когда вызывается valueOf(). К счастью, в JavaScript есть оператор присваивания со сложением, или оператор добавочного присваивания (+=).

Этот оператор просто добавляет значение правого операнда к переменной, которая находится слева, и присваивает этой переменной полученное значение. Вот простой пример:

    let b = 1
    console.log(b+=1); // 2
    console.log(b+=1); // 3
    console.log(b+=1); // 4

Как видите, каждый раз, когда мы используем оператор присваивания со сложением, значение переменной увеличивается! Используем эту идею в нашем методе valueOf():

    a.valueOf = function() {
        return this.num += 1;
    }

Вместо того чтобы просто возвращать this.num, мы теперь, при каждом вызове valueOf(), будем возвращать значение this.num, увеличенное на 1 и записывать новое значение в this.num.
После того, как в код внесено это изменение, мы наконец можем всё опробовать:

    const equality = (a==1 && a==2 && a==3);
    console.log(equality); // true

Пошаговый разбор

Помните о том, что при использовании оператора нестрогого равенства JS пытается выполнить приведение типов. Наш объект вызывает метод valueOf(), который возвращает a.num += 1, другими словами, возвращает значение a.num, увеличенное на единицу при каждом его вызове. Теперь остаётся лишь сравнить два числа. В нашем случае все сравнения выдадут true.

    a                     == 1   -> 
    a.valueOf()           == 1   -> 
    a.num += 1            == 1   -> 
    0     += 1            == 1   ->
    1                     == 1   -> true
    a                     == 2   -> 
    a.valueOf()           == 2   -> 
    a.num += 1            == 2   -> 
    1     += 1            == 2   ->
    2                     == 2   -> true
    a                     == 3   -> 
    a.valueOf()           == 3   -> 
    a.num += 1            == 3   -> 
    2     += 1            == 3   ->
    3                     == 3   -> true

Итоги

Полагаем, примеры, подобные разобранному выше, помогают, во-первых, лучше изучить базовые возможности JavaScript, а во-вторых — не дают забыть о том, что в JS не всё является тем, чем кажется.

Другие реализации равенства

    var i = 0;
    
    with({
      get a() {
        return ++i;
      }
    }) {
      if (a == 1 && a == 2 && a == 3)
        console.log("wohoo");
    }
    a = [1,2,3];
    a.join = a.shift;
    console.log(a == 1 && a == 2 && a == 3);
    var val = 0;
    Object.defineProperty(window, 'a', {
      get: function() {
        return ++val;
      }
    });
    if (a == 1 && a == 2 && a == 3) {
      console.log('yay');
    }
    if‌=()=>!0;
    var a = 9;
    
    if‌(a==1 && a== 2 && a==3)
    {
        console.log("Yes, it is possible")
    }
    var a = 9;
    
    if‌(a==1 && a== 2 && a==3)
    {
        //console.log("Yes, it is possible!)
        console.log("Yes, it is possible!")
    }
    
    function if‌(){return true;}