https://habrahabr.ru/company/ruvds/blog/347530/- Разработка веб-сайтов
- JavaScript
- Блог компании RUVDS.com
Недавно по
Твиттеру и
Реддиту гулял интересный кусок кода на 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 не всё является тем, чем кажется.
Уважаемые читатели! Если вы знаете о каких-нибудь курьёзах из области JavaScript — просим ими поделиться.