Примечание модератора: Не поддавайтесь желанию изменить код или удалить это уведомление. Образец пробелов может быть частью вопроса и поэтому не должен изменяться без надобности. Если вы находитесь в лагере «пробелы не важны», вы сможете принять код как есть.

Возможно ли, чтобы (a == 1 && a == 2 && a == 3) могло вычислить true в JavaScript?

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

Ответы (27)

Если вы воспользуетесь преимуществами того, как == работает, вы можете просто создать объект с настраиваемым toString (или valueOf), которая изменяет то, что возвращает каждый раз, когда используется, чтобы удовлетворять всем трем условиям.

const a = {
  я: 1,
  toString: function () {
    return a.i ++;
  }
}

if (a == 1 && a == 2 && a == 3) {
  console.log («Привет, мир!»);
}


Причина, по которой это работает, связана с использованием оператора свободного равенства. При использовании свободного равенства, если один из операндов имеет другой тип, чем другой, движок попытается преобразовать один в другой. В случае объекта слева и числа справа, он попытается преобразовать объект в число, сначала вызвав valueOf, если он вызывается, а в противном случае он вызовет toString. Я использовал toString в этом случае просто потому, что это то, что пришло в голову, valueOf имело бы больше смысла. Если бы я вместо этого вернул строку из toString, движок попытался бы преобразовать строку в число, дающее нам тот же конечный результат, но с немного более длинным путем.

Ладно, еще один взлом с генераторами:

const value = function * () {
  пусть i = 0;
  while (истина) yield ++ i;
} ();

Object.defineProperty (this, 'a', {
  получить() {
    возвращаемое значение.next (). значение;
  }
});

if (a === 1 && a === 2 && a === 3) {
  console.log («Йо!»);
}

Если его спросят, возможно ли это (НЕ ОБЯЗАТЕЛЬНО), он может попросить «а» вернуть случайное число. Было бы верно, если бы он генерировал последовательно 1, 2 и 3.

с ({
  получить() {
    вернуть Math.floor (Math.random () * 4);
  }
}) {
  for (var i = 0; i <1000; i ++) {
    if (a == 1 && a == 2 && a == 3) {
      console.log ("после" + (i + 1) + "испытаний, окончательно становится правдой !!!");
      ломать;
    }
  }
}

Здесь используется defineProperty с приятным побочным эффектом, вызывающим глобальную переменную!

var _a = 1

Object.defineProperty (this, "a", {
  "получить": () => {
    return _a ++;
  },
  настраиваемый: правда
});

console.log (а)
console.log (а)
console.log (а)

Правило №1 собеседований; никогда не говори невозможное.

Нет необходимости в хитрости скрытых символов.

window .__ defineGetter __ ('a', function () {
    if (typeof i! == 'число') {
        // определяем i в глобальном пространстве имен, чтобы оно не потерялось после выполнения этой функции
        я = 0;
    }
    return ++ i;
});

if (a == 1 && a == 2 && a == 3) {
    console.log ('Боже, что мы наделали?');
}

Это также возможно с использованием серии геттеров с самоперезаписью:

(Это похоже на решение jontro, но не требует переменной счетчика.)

(() => {
    "использовать строго";
    Object.defineProperty (this, "a", {
        "получить": () => {
            Object.defineProperty (this, "a", {
                "получить": () => {
                    Object.defineProperty (this, "a", {
                        "получить": () => {
                            возврат 3;
                        }
                    });
                    возврат 2;
                },
                настраиваемый: правда
            });
            возврат 1;
        },
        настраиваемый: правда
    });
    if (a == 1 && a == 2 && a == 3) {
        document.body.append («Да, это возможно»);
    }
}) ();

Использование Прокси:

var a = new Proxy({ i: 0 }, {
    get: (target, name) => name === Symbol.toPrimitive ? () => ++target.i : target[name],
});
console.log(a == 1 && a == 2 && a == 3);

Прокси-серверы в основном претендуют на роль целевого объекта (первый параметр), но перехватывают операции с целевым объектом (в данном случае операция «получить свойство»), так что есть возможность сделать что-то другое, кроме поведения объекта по умолчанию. . В этом случае действие "получить свойство" вызывается для a, когда == приводит к его типу, чтобы сравнить его с каждым числом. Так бывает:

  1. Мы создаем целевой объект, {i: 0}, где свойство i - это наш счетчик
  2. Создаем прокси для целевого объекта и назначаем его a
  3. Для каждого a == сравнения тип aприводится к примитивному значению
  4. Принуждение этого типа приводит к вызову a [Symbol.toPrimitive] () внутри
  5. Прокси-сервер перехватывает получение функции [Symbol.toPrimitive] с помощью "обработчика получения"
  6. "обработчик" прокси-сервера проверяет, что полученное свойство Symbol.toPrimitive, и в этом случае он увеличивается, а затем возвращает счетчик от целевого объекта: ++ target.i. Если извлекается другое свойство, мы просто возвращаемся к значению свойства по умолчанию, target [name]

Итак:

var a = ...; // a.valueOf == target.i == 0
a == 1 && // a == ++target.i == 1
a == 2 && // a == ++target.i == 2
a == 3    // a == ++target.i == 3

Как и в большинстве других ответов, это работает только со свободной проверкой равенства (==), потому что строгие проверки равенства (===) не выполняют принуждение типа что прокси может перехватить.

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

var a = 1;
var a = 2;
вар а = 3;
if (a == 1 && a == 2 && а == 3) {
    console.log ("Здравствуйте!")
}

Вы можете заметить небольшое расхождение со вторым, но первое и третье невооруженным глазом идентичны. Все 3 разных символа:

a - строчные латинские буквы A
- Строчные латинские буквы на всю ширину
а - строчная кириллица A

Общий термин для этого - «гомоглифы»: разные символы Юникода, которые выглядят одинаково. Обычно сложно получить три, которые невозможно отличить друг от друга, но в некоторых случаях вам может повезти. A, Α, А и Ꭺ подойдут лучше (Latin-A, Greek Alpha, Cyrillic-Aи Cherokee-A соответственно; к сожалению, греческий и строчные буквы Cherokee слишком отличаются от латинских a: α,, и поэтому не помогает в приведенном выше фрагменте).

Существует целый класс гомоглифических атак, чаще всего с использованием поддельных доменных имен (например, wikipediа.org (кириллица) против wikipedia.org (латиница)), но он также может отображаться в коде; обычно называются закулисными (как упоминалось в комментарии, [закулисные] вопросы теперь не по теме PPCG, но раньше были своего рода вызовами, где такие вещи появится). Я использовал этот веб-сайт, чтобы найти гомоглифы, используемые для этого ответа.

Пример без геттеров или valueOf:

a = [1,2,3];
a.join = a.shift;
console.log (a == 1 && a == 2 && a == 3);

Это работает, потому что == вызывает toString, который вызывает .join для массивов.

Другое решение, использующее Symbol.toPrimitive, который в ES6 эквивалентен toString / valueOf:

пусть i = 0;
let a = {[Symbol.toPrimitive]: () => ++ i};

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

Это перевернутая версия @ ответа Джеффа*, где скрытый символ (U + 115F, U + 1160 или U + 3164) используется для создания переменных, которые выглядят как 1, 2 и 3.

var a = 1;
var ᅠ 1 = a;
var ᅠ 2 = a;
var ᅠ 3 = a;
console.log (a == ᅠ 1 && a == ᅠ 2 && a == ᅠ 3);

* Этот ответ можно упростить, используя средство объединения с нулевой шириной (U + 200C) и средство объединения с нулевой шириной (U + 200D). Оба этих символа разрешены внутри идентификаторов, но не в начале:

var a = 1;
var a‌ = 2;
var a‍ = 3;
console.log (a == 1 && a‌ == 2 && a‍ == 3);

/ ****
var a = 1;
var a = 2;
var a = 3;
console.log (a == 1 && a \ u200c == 2 && a == 3);
**** /

Возможны и другие приемы, использующие ту же идею, например. с помощью селекторов вариантов Unicode для создания абсолютно одинаковых переменных (a︀ = 1; a︁ = 2; a︀ == 1 && a︁ == 2; // true).

Вот еще один вариант, использующий массив для извлечения любых значений, которые вы хотите.

const a = {
  n: [3,2,1],
  toString: function () {
    return a.n.pop ();
  }
}

if (a == 1 && a == 2 && a == 3) {
  console.log («Да»);
}

Однако, если честно, независимо от того, есть ли способ оценить его как истинное или нет (и, как показали другие, есть несколько способов), я бы искал ответ, выступая как человек, который провел сотни интервью. , будет что-то вроде:

"Ну, может быть, да, при каких-то странных обстоятельствах, которые не сразу очевидны для меня ... но если бы я столкнулся с этим в реальном коде, я бы использовал общие методы отладки, чтобы выяснить, как и почему он делает то, что он делал, а затем немедленно реорганизовал код, чтобы избежать этой ситуации ... но что более важно: я бы вообще НИКОГДА не писал этот код, потому что это само определение запутанного кода, и я стремлюсь никогда не писать запутанный код " .

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

IT IS POSSIBLE!

var i = 0;

with({
  get a() {
    return ++i;
  }
}) {
  if (a == 1 && a == 2 && a == 3)
    console.log("wohoo");
}

This uses a getter inside of a with statement to let a evaluate to three different values.

... this still does not mean this should be used in real code...

Even worse, this trick will also work with the use of ===.

  var i = 0;

  with({
    get a() {
      return ++i;
    }
  }) {
    if (a !== a)
      console.log("yep, this is printed.");
  }

Это может быть выполнено с помощью следующего в глобальной области видимости. Для nodejs используйте global вместо window в приведенном ниже коде.

var val = 0;
Object.defineProperty (окно, 'а', {
  get: function () {
    return ++ val;
  }
});
if (a == 1 && a == 2 && a == 3) {
  console.log ('ура');
}

Этот ответ злоупотребляет неявными переменными, предоставляемыми глобальной областью видимости в контексте выполнения, путем определения геттера для извлечения переменной.

Я не удержался - другие ответы, несомненно, верны, но вы действительно не можете пройти мимо следующего кода:

var a ᅠ = 1;
var a = 2;
var ᅠ a = 3;
if (a ᅠ == 1 && a == 2 && ᅠ a == 3) {
    console.log ("Здравствуйте!")
}

Обратите внимание на странный интервал в операторе if (который я скопировал из вашего вопроса). Это хангыль половинной ширины (корейский для тех, кто не знаком), который представляет собой символ пробела Unicode, который не интерпретируется сценарием ECMA как символ пробела - это означает, что это действительный символ для идентификатора. Следовательно, есть три совершенно разные переменные: одна с хангыль после а, вторая с ним перед и последняя только с а. Заменив пробел на _ для удобства чтения, тот же код будет выглядеть так:

var a_ = 1;
var a = 2;
var _a = 3;
if (a _ == 1 && a == 2 && _ a == 3) {
    console.log ("Здравствуйте!")
}

Проверьте валидацию на валидаторе имени переменной Матиаса. Если этот странный интервал действительно был включен в их вопрос, я уверен, что это подсказка для такого рода ответа.

Не делай этого. Серьезно.

Изменить: я обратил внимание на то, что (хотя и не разрешено запускать переменную) символы объединяющие символы нулевой ширины и символы не объединяемые нулевой шириной также разрешены в имена переменных - см. Обфускация JavaScript символами нулевой ширины - плюсы и минусы?.

Это будет выглядеть так:

var a = 1;
var a‍ = 2; // один символ нулевой ширины
var a‍‍ = 3; // два символа нулевой ширины (или вы можете использовать другой)
if (a == 1 && a‍ == 2 && a‍‍ == 3) {
    console.log ("Здравствуйте!")
}

Когда без регулярных выражений ничего не обойтись:

var a = {
  г: / \ д / г,
  valueOf: function () {
    вернуть this.r.exec (123) [0]
  }
}

if (a == 1 && a == 2 && a == 3) {
    console.log ("!")
}

Это работает из-за специального метода valueOf, который вызывается при сравнении Object с примитивом (например, Number). Основная хитрость заключается в том, что a.valueOf каждый раз возвращает новое значение, потому что он вызывает exec в регулярном выражении с флагом g, что вызывает обновление lastIndex этого регулярного выражения каждый раз при обнаружении совпадения. Итак, в первый раз this.r.lastIndex == 0, он соответствует 1 и обновляет lastIndex: this.r.lastIndex == 1, поэтому в следующий раз регулярное выражение будет соответствовать 2 и т. д.

В качестве альтернативы вы можете использовать класс для этого и экземпляр для проверки.

функция A () {
    значение var = 0;
    this.valueOf = function () {return ++ value; };
}

var a = new A;

if (a == 1 && a == 2 && a == 3) {
    console.log ('бинго!');
}

ИЗМЕНИТЬ

Используя классы ES6, это будет выглядеть так

класс А {
  constructor () {
    this.value = 0;
    this.valueOf ();
  }
  значение() {
    вернуть this.value ++;
  };
}

пусть а = новый А;

if (a == 1 && a == 2 && a == 3) {
  console.log ('бинго!');
}

This is possible in case of variable a being accessed by, say 2 web workers through a SharedArrayBuffer as well as some main script. The possibility is low, but it is possible that when the code is compiled to machine code, the web workers update the variable a just in time so the conditions a==1, a==2 and a==3 are satisfied.

This can be an example of race condition in multi-threaded environment provided by web workers and SharedArrayBuffer in JavaScript.

Here is the basic implementation of above:

main.js

// Main Thread

const worker = new Worker('worker.js')
const modifiers = [new Worker('modifier.js'), new Worker('modifier.js')] // Let's use 2 workers
const sab = new SharedArrayBuffer(1)

modifiers.forEach(m => m.postMessage(sab))
worker.postMessage(sab)

worker.js

let array

Object.defineProperty(self, 'a', {
  get() {
    return array[0]
  }
});

addEventListener('message', ({data}) => {
    array = new Uint8Array(data)
    let count = 0
    do {
        var res = a == 1 && a == 2 && a == 3
        ++count
    } while(res == false) // just for clarity. !res is fine
    console.log(`It happened after ${count} iterations`)
    console.log('You should\'ve never seen this')
})

modifier.js

addEventListener('message' , ({data}) => {
    setInterval( () => {
        new Uint8Array(data)[0] = Math.floor(Math.random()*3) + 1
    })
})

On my MacBook Air, it happens after around 10 billion iterations on the first attempt:

enter image description here

Second attempt:

enter image description here

As I said, the chances will be low, but given enough time, it'll hit the condition.

Tip: If it takes too long on your system. Try only a == 1 && a == 2 and change Math.random()*3 to Math.random()*2. Adding more and more to list drops the chance of hitting.

JavaScript

a == a +1

In JavaScript, there are no integers but only Numbers, which are implemented as double precision floating point numbers.

It means that if a Number a is large enough, it can be considered equal to three consecutive integers:

a = 100000000000000000
if (a == a+1 && a == a+2 && a == a+3){
  console.log("Precision loss!");
}

True, it's not exactly what the interviewer asked (it doesn't work with a=0), but it doesn't involve any trick with hidden functions or operator overloading.

Other languages

For reference, there are a==1 && a==2 && a==3 solutions in Ruby and Python. With a slight modification, it's also possible in Java.

Ruby

With a custom ==:

class A
  def ==(o)
    true
  end
end

a = A.new

if a == 1 && a == 2 && a == 3
  puts "Don't do this!"
end

Or an increasing a:

def a
  @a ||= 0
  @a += 1
end

if a == 1 && a == 2 && a == 3
  puts "Don't do this!"
end

Python

class A:
    def __eq__(self, who_cares):
        return True
a = A()

if a == 1 and a == 2 and a == 3:
    print("Don't do that!")

Java

It's possible to modify Java Integer cache:

package stackoverflow;

import java.lang.reflect.Field;

public class IntegerMess
{
    public static void main(String[] args) throws Exception {
        Field valueField = Integer.class.getDeclaredField("value");
        valueField.setAccessible(true);
        valueField.setInt(1, valueField.getInt(42));
        valueField.setInt(2, valueField.getInt(42));
        valueField.setInt(3, valueField.getInt(42));
        valueField.setAccessible(false);

        Integer a = 42;

        if (a.equals(1) && a.equals(2) && a.equals(3)) {
            System.out.println("Bad idea.");
        }
    }
}

If you ever get such an interview question (or notice some equally unexpected behavior in your code) think about what kind of things could possibly cause a behavior that looks impossible at first glance:

  1. Encoding: In this case the variable you are looking at is not the one you think it is. This can happen if you intentionally mess around with Unicode using homoglyphs or space characters to make the name of a variable look like another one, but encoding issues can also be introduced accidentally, e.g. when copying & pasting code from the Web that contains unexpected Unicode code points (e.g. because a content management system did some "auto-formatting" such as replacing fl with Unicode 'LATIN SMALL LIGATURE FL' (U+FB02)).

  2. Race conditions: A race-condition might occur, i.e. a situation where code is not executing in the sequence expected by the developer. Race conditions often happen in multi-threaded code, but multiple threads are not a requirement for race conditions to be possible – asynchronicity is sufficient (and don't get confused, async does not mean multiple threads are used under the hood).

    Note that therefore JavaScript is also not free from race conditions just because it is single-threaded. See here for a simple single-threaded – but async – example. In the context of an single statement the race condition however would be rather hard to hit in JavaScript.

    JavaScript with web workers is a bit different, as you can have multiple threads. @mehulmpt has shown us a great proof-of-concept using web workers.

  3. Side-effects: A side-effect of the equality comparison operation (which doesn't have to be as obvious as in the examples here, often side-effects are very subtle).

These kind of issues can appear in many programming languages, not only JavaScript, so we aren't seeing one of the classical JavaScript WTFs here1.

Of course, the interview question and the samples here all look very contrived. But they are a good reminder that:

  • Side-effects can get really nasty and that a well-designed program should be free from unwanted side-effects.
  • Multi-threading and mutable state can be problematic.
  • Not doing character encoding and string processing right can lead to nasty bugs.

1 For example, you can find an example in a totally different programming language (C#) exhibiting a side-effect (an obvious one) here.

То же, но разные, но все те же (можно «тестировать» несколько раз):

const a = {valueOf: () => this.n = (this.n || 0)% 3 + 1}
    
if (a == 1 && a == 2 && a == 3) {
  console.log («Привет, мир!»);
}

if (a == 1 && a == 2 && a == 3) {
  console.log («Привет, мир!»);
}

Моя идея началась с того, как работает уравнение числового типа объекта.

By overriding valueOf in a class declaration, it can be done:

class Thing {
    constructor() {
        this.value = 1;
    }

    valueOf() {
        return this.value++;
    }
}

const a = new Thing();

if(a == 1 && a == 2 && a == 3) {
    console.log(a);
}

What happens is that valueOf is called in each comparison operator. On the first one, a will equal 1, on the second, a will equal 2, and so on and so forth, because each time valueOf is called, the value of a is incremented.

Therefore the console.log will fire and output (in my terminal anyways) Thing: { value: 4}, indicating the conditional was true.

Думаю, это минимальный код для его реализации:

i = 0, a = {valueOf: () => ++ i}

if (a == 1 && a == 2 && a == 3) {
  console.log ('Разум === Взорван');
}

Создание фиктивного объекта с настраиваемым valueOf, который увеличивает глобальную переменную i при каждом вызове. 23 символа!

Да, это возможно! 😎

»JavaScript

if‌ = () =>! 0;
var a = 9;

if‌ (a == 1 && a == 2 && a == 3)
{
    document.write ("

Да, это возможно! 😎

") }

Приведенный выше код является сокращенной версией (спасибо @Forivin за его примечание в комментариях), а следующий код является оригинальным:

var a = 9;

if‌ (a == 1 && a == 2 && a == 3)
{
    //console.log( "Да, это возможно! 😎")
    document.write ("

Да, это возможно! 😎

") } // -------------------------------------------- функция if‌ () {return true;}

Если вы просто видите верхнюю часть моего кода и запускаете его, вы говорите ВАУ, как?

Так что я думаю, что достаточно сказать Да, возможно тому, кто сказал вы: Нет ничего невозможного

Уловка: я использовал скрытый символ после if, чтобы сделать функцию, имя которой похоже на if. В JavaScript мы не можем переопределить ключевые слова, поэтому я вынужден был использовать этот способ. Это подделка если, но в этом случае у вас работает!


»C #

Также я написал версию C # (с техникой увеличения значения свойства):

static int _a;
public static int a => ++_a;

public static void Main()
{
    if(a==1 && a==2 && a==3)
    {
        Console.WriteLine("Yes, it is possible!😎");
    }
}

Живая демонстрация

Как мы уже знаем, секрет оператора свободного равенства (==) попытается преобразовать оба значения в общий тип. В результате будут вызваны некоторые функции.

ToPrimitive (A) пытается преобразовать свой аргумент объекта в примитив value, вызывая различные последовательности A.toString и A.valueOf методы на А.

Так же, как и другие ответы, используя Symbol.toPrimitive, .toString, .valueOf из целого числа. Я бы предложил решение, используя массив с таким Array.pop.

let a = {array: [3, 2, 1], toString: () => a.array.pop ()};

if (a == 1 && a == 2 && a == 3) {
  console.log («Привет, мир!»);
}

Таким образом, мы можем работать с таким текстом

let a = {array: ["World", "Hello"], toString: () => a.array.pop ()};

if (a == "Hello" && a == "World") {
  console.log («Привет, мир!»);
}

Ответ ECMAScript 6, в котором используются символы:

const a = {value: 1};
a[Symbol.toPrimitive] = function() { return this.value++ };
console.log((a == 1 && a == 2 && a == 3));

Из-за использования == предполагается, что JavaScript принудит a к чему-то близкому ко второму операнду (1, 2, В данном случае 3). Но прежде чем JavaScript попытается самостоятельно вычислить принуждение, он пытается вызвать Symbol.toPrimitive. Если вы укажете Symbol.toPrimitive, JavaScript будет использовать значение, возвращаемое вашей функцией. В противном случае JavaScript вызовет valueOf.

Собственно ответ на первую часть вопроса «Да» на каждом языке программирования. Например, это в случае C / C ++:

#define a   (b++)
int b = 1;
if (a ==1 && a== 2 && a==3) {
    std::cout << "Yes, it's possible!" << std::endl;
} else {
    std::cout << "it's impossible!" << std::endl;
}

2022 WebDevInsider