javascript

Может ли в JavaScript конструкция (a==1 && a==2 && a==3) оказаться равной true?

  • пятница, 26 января 2018 г. в 03:13:54
https://habrahabr.ru/company/ruvds/blog/347530/
  • Разработка веб-сайтов
  • JavaScript
  • Блог компании RUVDS.com


Недавно по Твиттеру и Реддиту гулял интересный кусок кода на JavaScript. Вопрос, связанный с ним, заключался в следующем: «Может ли выражение (a==1 && a==2 && a==3) вернуть true?». Ответ на вопрос, как ни странно, был положительным.

image

Сегодня мы разберём этот код и постараемся его понять.

Вот он:

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 не всё является тем, чем кажется.

Уважаемые читатели! Если вы знаете о каких-нибудь курьёзах из области JavaScript — просим ими поделиться.