JavaScript: (a== 1 && a ==2 && a==3) 的值是true吗

Can (a== 1 && a ==2 && a==3) ever evaluate to true?

Moderator note: Please resist the urge to edit the code or remove this notice. The pattern of whitespace may be part of the question and therefore should not be tampered with unnecessarily. If you are in the"whitespace is insignificant" camp, you should be able to accept the code as is.

有没有可能(a== 1 && a ==2 && a==3)可以在javascript中对true进行评估?

这是一家大型科技公司提出的面试问题。两周前就发生了,但我仍在努力寻找答案。我知道我们在日常工作中从来没有写过这样的代码,但我很好奇。


如果利用==的工作方式,您可以简单地创建一个具有自定义toString(或valueOf函数的对象,该函数可以在每次使用时更改返回的内容,从而满足所有三个条件。

1
2
3
4
5
6
7
8
9
10
const a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}

if(a == 1 && a == 2 && a == 3) {
  console.log('Hello World!');
}

之所以这样做是因为使用了松散的相等运算符。使用松散相等时,如果其中一个操作数的类型不同于另一个操作数,则引擎将尝试将一个操作数转换为另一个操作数。如果对象在左边,数字在右边,它会试图通过首先调用valueOf将对象转换为数字,如果它是可调用的,如果它不能调用,它会调用toString。在这个例子中,我使用了toString,因为这是我想到的,valueOf会更有意义。如果我从toString返回一个字符串,那么引擎会尝试将该字符串转换为一个数字,给出相同的最终结果,尽管路径稍长一些。


我无法抗拒-其他答案毫无疑问是正确的,但你真的无法通过以下代码:

1
2
3
4
5
6
var a? = 1;
var a = 2;
var ?a = 3;
if(a?==1 && a== 2 &&?a==3) {
    console.log("Why hello there!")
}

注意if声明中奇怪的间隔(我从你的问题中复制的)。它是半宽韩文(对于不熟悉的人来说是韩文),这是一个Unicode空格字符,ECMA脚本没有将其解释为空格字符-这意味着它是标识符的有效字符。因此,有三个完全不同的变量,一个是a后面的朝鲜文,一个是a前面的,最后一个是a。为了可读性,用_替换空格,相同的代码如下:

1
2
3
4
5
6
var a_ = 1;
var a = 2;
var _a = 3;
if(a_==1 && a== 2 &&_a==3) {
    console.log("Why hello there!")
}

查看对Mathias变量名验证器的验证。如果这个奇怪的间隔真的包含在他们的问题中,我相信这是对这种答案的提示。

不要这样做。说真的。

编辑:我注意到(尽管不允许启动变量)在变量名中也允许使用零宽度连接符和零宽度非连接符字符-参见模糊化带零宽度字符的javascript-优缺点?.

如下所示:

1
2
3
4
5
6
var a= 1;
var a?= 2; //one zero-width character
var a??= 3; //two zero-width characters (or you can use the other one)
if(a==1&&a?==2&&a??==3) {
    console.log("Why hello there!")
}


这是可能的!

1
2
3
4
5
6
7
8
9
10
var i = 0;

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

这将使用with语句中的getter,让a评估为三个不同的值。

…这仍然不意味着这应该在真正的代码中使用…

更糟糕的是,这种技巧也适用于===的使用。

1
2
3
4
5
6
7
8
9
10
  var i = 0;

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


不带getter或value的示例:

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

这是因为==调用toString,后者调用.join作为数组。

另一种解决方案是使用Symbol.toPrimitive,它是ES6相当于toString/valueOf

1
2
3
4
let i = 0;
let a = { [Symbol.toPrimitive]: () => ++i };

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


如果询问是否可能(不一定),可以要求"A"返回随机数。如果按顺序生成1、2和3,则为真。

1
2
3
4
5
6
7
8
9
10
11
12
with({
  get a() {
    return Math.floor(Math.random()*4);
  }
}){
  for(var i=0;i<1000;i++){
    if (a == 1 && a == 2 && a == 3){
      console.log("after" + (i+1) +" trials, it becomes true finally!!!");
      break;
    }
  }
}


如果没有正则表达式无法执行任何操作:

1
2
3
4
5
6
7
8
9
10
var a = {
  r: /\d/g,
  valueOf: function(){
    return this.r.exec(123)[0]
  }
}

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

它的工作原因是自定义valueOf方法,当对象与原语(如数字)比较时调用该方法。主要的诀窍是,a.valueOf每次都返回新的值,因为它在带有g标志的正则表达式上调用exec,每次找到匹配时都会更新该正则表达式的lastIndex。所以第一次this.r.lastIndex == 0时,它匹配1并更新lastIndexthis.r.lastIndex == 1,所以下次regex将匹配2等。


它可以在全局范围内使用以下方法完成。对于nodejs,在下面的代码中使用global,而不是window

1
2
3
4
5
6
7
8
9
var val = 0;
Object.defineProperty(window, 'a', {
  get: function() {
    return ++val;
  }
});
if (a == 1 && a == 2 && a == 3) {
  console.log('yay');
}

这个答案通过定义getter来检索变量,从而滥用了执行上下文中全局范围提供的隐式变量。


这在变量a被访问的情况下是可能的,比如说2个Web工作者通过sharedraybuffer和一些主脚本进行访问。可能性很低,但当代码编译为机器代码时,Web工作者可能会及时更新变量a,从而满足a==1a==2a==3的条件。

这可以是Web工作者和JavaScript中的SharedarrayBuffer提供的多线程环境中的争用条件示例。

下面是上述的基本实现:

MIN JS

1
2
3
4
5
6
7
8
// 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)

作业工人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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')
})

JS

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

在我的MacBook Air上,第一次尝试大约100亿次迭代后会发生这种情况:

enter image description here

第二次尝试:

enter image description here

如我所说,机会很小,但只要有足够的时间,它就会达到状态。

提示:如果您的系统需要太长时间。只尝试a == 1 && a == 2,将Math.random()*3改为Math.random()*2。越来越多地添加到列表中会降低命中率。


这也可以使用一系列自重写getter:

(这类似于jontro的解决方案,但不需要计数器变量。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(() => {
   "use strict";
    Object.defineProperty(this,"a", {
       "get": () => {
            Object.defineProperty(this,"a", {
               "get": () => {
                    Object.defineProperty(this,"a", {
                       "get": () => {
                            return 3;
                        }
                    });
                    return 2;
                },
                configurable: true
            });
            return 1;
        },
        configurable: true
    });
    if (a == 1 && a == 2 && a == 3) {
        document.body.append("Yes, it’s possible.");
    }
})();


我看不到这个答案已经发布了,所以我也会把这个加入到混合中。这类似于Jeff的半宽朝鲜文答案。

1
2
3
4
5
6
var a = 1;
var= 2;
var а = 3;
if(a == 1 &&== 2 && а == 3) {
    console.log("Why hello there!")
}

你可能会注意到与第二个存在细微的差异,但第一个和第三个与肉眼相同。所有3个字符都是不同的字符:

a—拉丁文小写A—全角拉丁文小写Aа—西里尔文小写A

它的通用术语是"同形符号":不同的Unicode字符看起来相同。通常很难得到三个完全不可区分的,但在某些情况下你可以幸运。A、_、А和?效果会更好(拉丁字母a、希腊字母a、西里尔字母a和切罗基字母a;不幸的是,希腊字母和切罗基小写字母与拉丁字母a太不一样:α?,因此对上述片段没有帮助。

现在有一类完全相同的攻击,最常见的是假域名(如EDOCX1(西里尔文)vs EDOCX1(拉丁语)),但它也可以用代码显示;通常被称为欠手(如评论中提到的,[欠手]问题现在在ppcg上不再是话题,但过去是一种挑战,其中会出现类似的情况)。我用这个网站找到了这个答案的同形词。


或者,您可以使用一个类和一个实例来进行检查。

1
2
3
4
5
6
7
8
9
10
function A() {
    var value = 0;
    this.valueOf = function () { return ++value; };
}

var a = new A;

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

编辑

使用ES6类,看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A {
  constructor() {
    this.value = 0;
    this.valueOf();
  }
  valueOf() {
    return this.value++;
  };
}

let a = new A;

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


是的,这是可能的!????JavaScript

1
2
3
4
5
6
7
if?=()=>!0;
var a = 9;

if?(a==1 && a== 2 && a==3)
{
    document.write("Yes, it is possible!??")
}

以上代码是一个简短的版本(感谢@forivin在注释中的注释),以下代码是原始代码:

1
2
3
4
5
6
7
8
9
10
11
var a = 9;

if?(a==1 && a== 2 && a==3)
{
    //console.log("Yes, it is possible!??")
    document.write("Yes, it is possible!??")
}

//--------------------------------------------

function if?(){return true;}

If you just see top side of my code and run it you say WOW, how?

So I think it is enough to say Yes, it is possible to someone that said to
you: Nothing is impossible

Trick: I used a hidden character after if to make a function that its name is similar to if. In JavaScript we can not override keywords so I forced to use this way. It is a fake if, but it works for you in this case!

?C.*

我还写了一个C版本(增加属性值技术):

1
2
3
4
5
6
7
8
9
10
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!??");
    }
}

现场演示


JavaScriptA==A+ 1

在javascript中,没有整数,只有Numbers,它们被实现为双精度浮点数。

这意味着,如果一个数字a足够大,它可以被视为等于三个连续整数:

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

的确,这不是面试官所要求的(它不适用于a=0),但它不涉及隐藏函数或运算符过载的任何技巧。

其他语言

作为参考,Ruby和Python中有a==1 && a==2 && a==3解决方案。稍加修改,Java也是可能的。

红宝石

有了自定义==

1
2
3
4
5
6
7
8
9
10
11
class A
  def ==(o)
    true
  end
end

a = A.new

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

或增加的a

1
2
3
4
5
6
7
8
def a
  @a ||= 0
  @a += 1
end

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

Python

1
2
3
4
5
6
7
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 EDCOX1×14缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.");
        }
    }
}


这是@jeff's answer*的倒装版本,其中使用隐藏字符(u+115f、u+1160或u+3164)创建类似于123的变量。

1
2
3
4
5
var  a = 1;
var ?1 = a;
var ?2 = a;
var ?3 = a;
console.log( a ==?1 && a ==?2 && a ==?3 );

*这个答案可以通过使用零宽度非连接符(U+200C)和零宽度连接符(U+200D)来简化。这两个字符都允许出现在标识符内,但不能出现在开头:

1
2
3
4
5
6
7
8
9
10
11
var a = 1;
var a? = 2;
var a? = 3;
console.log(a == 1 && a? == 2 && a? == 3);

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

其他技巧也可以使用相同的思想,例如使用Unicode变体选择器创建完全相同的变量(a? = 1; a? = 2; a? == 1 && a? == 2; // true)。


面试的第一条规则;绝不说不可能。

不需要隐藏人物的诡计。

1
2
3
4
5
6
7
8
9
10
11
window.__defineGetter__( 'a', function(){
    if( typeof i !== 'number' ){
        // define i in the global namespace so that it's not lost after this function runs
        i = 0;
    }
    return ++i;
});

if( a == 1 && a == 2 && a == 3 ){
    alert( 'Oh dear, what have we done?' );
}


老实说,不管有没有一种方法可以让它评价为真(正如其他人所展示的,有多种方法),我要寻找的答案是,作为一个已经进行了数百次采访的人来说,应该是这样的:

"嗯,也许是的,在一些我不太清楚的奇怪情况下……但是,如果我在真实代码中遇到这种情况,那么我将使用常见的调试技术来弄清楚它是如何做的以及为什么要做它正在做的事情,然后立即重构代码以避免这种情况……但更重要的是:我绝对不会首先编写代码,因为这正是复杂代码的定义,我努力避免编写复杂代码"。

我想有些面试官会对一个显然是非常棘手的问题而生气,但我不介意有意见的开发人员,特别是当他们能够用合理的想法来支持它,并能将我的问题融入到一个有意义的关于他们自己的陈述中时。


这是另一个变体,使用数组弹出您想要的任何值。

1
2
3
4
5
6
7
8
9
10
const a = {
  n: [3,2,1],
  toString: function () {
    return a.n.pop();
  }
}

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


如果你遇到这样一个面试问题(或者注意到你的代码中有一些同样意想不到的行为),想想什么样的事情可能导致乍一看是不可能的行为:

  • 编码:在这种情况下,您看到的变量不是您认为的那个变量。如果您有意使用同形符号或空格字符来混淆Unicode,使变量名称看起来像另一个变量,则可能会发生这种情况,但也可能会意外引入编码问题,例如,从包含意外Unicode代码点的Web复制和粘贴代码时(例如,由于内容管理系统Did一些"自动格式化",例如用unicode"拉丁文小型连字fl"(u+fb02)替换fl

  • 竞态条件:可能会发生竞态条件,即代码没有按照开发人员预期的顺序执行的情况。多线程代码中经常会出现争用条件,但多个线程不是争用条件可能实现的要求——异步性足够(不要混淆,异步并不意味着多个线程是在后台使用的)。

    请注意,因为JavaScript是单线程的,所以它也不是没有竞争条件的。请参阅这里的一个简单的单线程但异步的例子。然而,在单个语句的上下文中,竞态条件在JavaScript中很难达到。

    Web工作者的JavaScript有点不同,因为您可以有多个线程。@Mehulmpt已经向我们展示了使用Web工作者的一个很好的概念证明。

  • 副作用:相等比较操作的副作用(不必像这里的例子那样明显,通常副作用非常微妙)。

  • 这些问题可以出现在许多编程语言中,不仅仅是JavaScript,因此我们在这里没有看到经典的JavaScript WTF。

    当然,这里的面试问题和样品看起来都很做作。但它们提醒我们:

    • 副作用会变得非常严重,一个设计良好的程序应该没有不必要的副作用。
    • 多线程和可变状态可能存在问题。
    • 不正确地进行字符编码和字符串处理会导致严重的错误。

    1例如,您可以找到一个完全不同的编程语言(c)的示例,其中显示了一个副作用(明显的一个)。


    好吧,另一个关于发电机的黑客:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const value = function* () {
      let i = 0;
      while(true) yield ++i;
    }();

    Object.defineProperty(this, 'a', {
      get() {
        return value.next().value;
      }
    });

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


    使用代理:

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

    代理基本上假装为目标对象(第一个参数),但截取目标对象上的操作(在本例中是"get property"操作),这样就有机会执行默认对象行为以外的操作。在这种情况下,当==强制其类型以将其与每个数字进行比较时,对a调用"get property"操作。这种情况发生了:

  • 我们创建了一个目标对象,{ i: 0 },其中i属性是我们的计数器。
  • 我们为目标对象创建一个代理并将其分配给a
  • 对于每个a ==比较,a的类型被强制为原始值。
  • 这种类型的强制导致在内部调用a[Symbol.toPrimitive]()
  • 代理程序使用"get handler"截取获取a[Symbol.toPrimitive]函数。
  • 代理的"get handler"检查正在获取的属性是Symbol.toPrimitive,在这种情况下,它会递增,然后从目标对象返回计数器:++target.i。如果检索到不同的属性,我们只返回默认属性值,target[name]
  • 所以:

    1
    2
    3
    4
    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

    与大多数其他答案一样,这只适用于松散的相等检查(==),因为严格的相等检查(===不执行代理可以截取的类型强制。


    实际上,问题的第一部分的答案在每种编程语言中都是"是"。例如,在C/C++的情况下:

    1
    2
    3
    4
    5
    6
    7
    #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;
    }


    相同,但不同,但仍然相同(可以多次"测试"):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const a = { valueOf: () => this.n = (this.n || 0) % 3 + 1}
       
    if(a == 1 && a == 2 && a == 3) {
      console.log('Hello World!');
    }

    if(a == 1 && a == 2 && a == 3) {
      console.log('Hello World!');
    }

    我的想法是从数字对象类型方程的工作原理开始的。


    使用符号的ECMAScript 6答案:

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

    由于==的使用,javascript应该将a强制为接近第二个操作数的对象(在本例中为123)。但在javascript试图自己进行强制之前,它试图调用Symbol.toPrimitive。如果提供Symbol.toPrimitivejavascript,则将使用函数返回的值。否则,javascript将调用valueOf


    我认为这是实现它的最小代码:

    1
    2
    3
    4
    5
    i=0,a={valueOf:()=>++i}

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

    使用自定义valueOf创建一个虚拟对象,在每次调用时增加一个全局变量i。23个字!


    这一个使用了一个很好的副作用导致全局变量的定义属性!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var _a = 1

    Object.defineProperty(this,"a", {
     "get": () => {
        return _a++;
      },
      configurable: true
    });

    console.log(a)
    console.log(a)
    console.log(a)


    通过在类声明中重写valueOf,可以做到:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Thing {
        constructor() {
            this.value = 1;
        }

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

    const a = new Thing();

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

    实际情况是,在每个比较运算符中调用valueOf。第一种情况下,a等于1,第二种情况下,a等于2,依此类推,因为每次调用valueOfa的值都会增加。

    因此,console.log将(无论如何)启动并输出Thing: { value: 4},表明条件为真。