(a==1 && a==2 && a==3) true? Магия JavaScript

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Задача, которая взорвала Stack Overflow

Представьте ситуацию: вы на собеседовании, и интервьюер с хитрой улыбкой спрашивает: «Может ли выражение a == 1 && a == 2 && a == 3 когда-либо вернуть true?». Первая реакция — «Конечно нет! Одна переменная не может одновременно равняться трём разным числам».

Однако, если вы знакомы с JavaScript чуть глубже, то знаете: в этом языке возможны самые неожиданные вещи. Этот вопрос собрал тысячи просмотров на Stack Overflow и стал классикой для проверки понимания приведения типов, геттеров и объектных оберток в JS.

В этой статье мы не только докажем, что это возможно, но и разберём 5 различных способов заставить (a==1 && a==2 && a==3) вернуть true. Вы узнаете, как работает нестрогое сравнение (==), чем оно отличается от строгого (===), и как этим можно управлять.

Статья будет полезна как новичкам, которые хотят глубже понять JavaScript, так и опытным разработчикам, готовящимся к собеседованию. Поехали!

Способ №1: Магия valueOf (самый популярный)

Самый элегантный и часто цитируемый способ — использовать пользовательский объект с переопределённым методом valueOf. В JavaScript, когда вы сравниваете объект с примитивом через ==, движок автоматически вызывает valueOf() (или toString()) для преобразования объекта в примитив.

Мы создадим объект-счётчик, который при каждом вызове valueOf будет увеличивать своё значение на 1.

const a = {
  value: 1,
  valueOf() {
    return this.value++;
  }
};

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

Как это работает:

  • При первом сравнении a == 1 вызывается valueOf(), возвращается 1, и value увеличивается до 2.
  • При втором сравнении a == 2 снова вызывается valueOf(), возвращается 2, value становится 3.
  • Третье сравнение a == 3 завершает цепочку успехом.

Это идеальный пример type coercion (принудительного приведения типов) в действии. Именно этот механизм делает JavaScript одновременно гибким и опасным языком.

Способ №2: toString — близнец valueOf

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

const a = {
  value: 1,
  toString() {
    return this.value++;
  }
};

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

Разница тонкая: valueOf имеет приоритет для числовых сравнений, а toString — для строковых. Но в нашем случае оба подходят идеально.

Способ №3: Геттеры в глобальной области

Многие даже не подозревают, что глобальные переменные в JavaScript на самом деле являются свойствами глобального объекта (window в браузере, global в Node.js). А свойства могут быть определены через геттеры (getter).

Мы можем определить геттер для переменной a прямо на глобальном объекте:

let value = 1;
Object.defineProperty(window, 'a', {
  get() {
    return value++;
  }
});

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

Этот метод особенно интересен тем, что a ведёт себя как обычная переменная, хотя на самом деле это свойство с геттером. Каждый раз, когда вы обращаетесь к a, геттер возвращает новое значение.

Важно: Этот код работает только в браузерном окружении, где есть window. В Node.js нужно использовать global.

Способ №4: Proxy — современный и мощный

Начиная с ES6, у нас есть Proxy — объект, который позволяет перехватывать и переопределять базовые операции над другим объектом. Это самый наглядный и контролируемый способ.

const target = {};
const handler = {
  get(target, prop) {
    if (prop === 'valueOf' || prop === 'toString') {
      let count = 1;
      return () => count++;
    }
    return target[prop];
  }
};

const a = new Proxy(target, handler);

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

Proxy позволяет нам перехватывать вызовы методов valueOf и toString и возвращать функцию-счётчик. Это более «чистый» подход, так как мы не модифицируем сам объект, а лишь его поведение при доступе.

Этот способ демонстрирует, насколько гибким стал JavaScript в современных стандартах.

Способ №5: Массивы — неожиданный поворот

Мало кто знает, но массивы в JavaScript тоже являются объектами и поддерживают valueOf. Более того, при преобразовании в примитив массив сначала вызывает toString(), который объединяет элементы через запятую.

Мы можем использовать эту особенность:

const a = [1, 2, 3];
a.valueOf = function() {
  return this.shift();
};

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

Здесь мы заменяем стандартный valueOf массива на функцию, которая извлекает первый элемент (shift()). Каждое сравнение забирает следующее число. К концу цепочки массив становится пустым, но это уже не важно — условие выполнено.

Почему это работает? Разбор механики ==

Теперь, когда мы увидели 5 способов, давайте разберёмся, почему все они работают. Ключ к пониманию — алгоритм нестрогого сравнения (Abstract Equality Comparison).

Когда JavaScript встречает выражение x == y, он выполняет следующие шаги (упрощённо):

  1. Если типы x и y одинаковые — сравнивает как обычно (для чисел, строк, булевых).
  2. Если null == undefined — возвращает true.
  3. Если один из операндов — объект, а другой — примитив, то вызывается ToPrimitive(obj).
  4. To

Блоги

Рекомендаций по книгам