(a==1&&a==2&&a==3)可以评估为true吗?

主持人备注:请抵制编辑代码或删除此通知的冲动。空格模式可能是问题的一部分,因此不应不必要地篡改。如果你属于“空格无关紧要”的阵营,你应该能够接受原样。

有没有可能(a== 1 && a ==2 && a==3)在JavaScript中评估为true

这是一家大型科技公司的面试问题。它发生在两周前,但我仍在试图找到答案。我知道我们在日常工作中从未编写过这样的代码,但我很好奇。

405890 次浏览

这是可能的!

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

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

…这仍然不意味着它应该在实际代码中使用…

更糟糕的是,这个技巧也可以使用===

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

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

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返回一个字符串,引擎会尝试将字符串转换为一个数字,给我们相同的最终结果,尽管路径稍长。

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

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

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

我无法抗拒——其他答案无疑是正确的,但你真的不能跳过以下代码:

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

请注意if语句中奇怪的行间距(我从你的问题中复制的)。它是半宽Hangul(对于不熟悉的人来说是韩语),它是一个Unicode空格字符,ECMA脚本不会将其解释为空格字符-这意味着它是标识符的有效字符。因此,有三个完全不同的变量,一个在a之后,一个在a之前,最后一个只有a。为了易读性,将空格替换为_,相同的代码如下:

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

如果这个奇怪的行间距真的包含在他们的问题中,我确信这是对这种答案的暗示。

别这样。说真的。

编辑:我注意到(虽然不允许启动变量)变量名中也允许零宽度细木工零宽度非连接器字符-参见使用零宽度字符混淆JavaScript-优点和缺点?

这看起来像以下内容:

var a= 1;var a‍= 2; //one zero-width charactervar 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!")}

如果被问到是否可能(不是必须),它可以问“a”返回一个随机数。如果它按顺序生成1、2和3,则为真。

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;}}}

这也可以使用一系列自覆盖getter:

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

(() => {"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.");}})();

或者,您可以为它使用一个类,并为检查使用一个实例。

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类,它看起来像这样

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!');}

面试的第一条规则:永远不要说不可能。

不需要隐藏的角色诡计。

window.__defineGetter__( 'a', function(){if( typeof i !== 'number' ){// define i in the global namespace so that it's not lost after this function runsi = 0;}return ++i;});
if( a == 1 && a == 2 && a == 3 ){console.log( 'Oh dear, what have we done?' );}

我没有看到这个答案已经发布了,所以我也会把这个扔进混音中。这类似于半宽韩文空间的杰夫的回答

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

您可能会注意到与第二个略有不同,但第一个和第三个与肉眼相同。所有三个都是不同的字符:

a-拉丁文小写字母A
-全宽拉丁小写字母A
а-西里尔小写字母A

这种情况的通用术语是“同形文字”:看起来相同的不同Unicode字符。通常很难找到完全无法区分的,但在某些情况下你可以幸运。, Α, А, 和会更好(拉丁-A,希腊阿尔法西里尔字母-A切诺基-A;不幸的是,希腊和切诺基的小写字母与拉丁a差异太大:α,因此对上述片段没有帮助)。

有一整类同形文字攻击,最常见的是假域名(例如wikipediа.org(西里尔文)vswikipedia.org(拉丁文)),但它也可以出现在代码中;通常被称为卑鄙(正如评论中提到的,[卑鄙]问题现在在PPCG上偏离了主题,但过去是一种挑战类型,这类事情会出现)。我用本网站找到了用于这个答案的同形文字。

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

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

如果没有正则表达式就不能做任何事情:

var a = {r: /\d/g,valueOf: function(){return this.r.exec(123)[0]}}
if (a == 1 && a == 2 && a == 3) {console.log("!")}

它之所以有效,是因为当Object与原始(例如Number)进行比较时调用的自定义a.valueOf0方法。主要技巧是a.valueOf每次都返回新值,因为它使用g标志在正则表达式上调用exec,这导致每次找到匹配时更新该正则表达式的a.valueOf1。所以第一次this.r.lastIndex == 0,它匹配1并更新lastIndexthis.r.lastIndex == 1,所以下次regex将匹配2等等。

实际上,在每种编程语言中,问题第一部分的答案都是“是”。例如,这是在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;}

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

const a = {n: [3,2,1],toString: function () {return a.n.pop();}}
if(a == 1 && a == 2 && a == 3) {console.log('Yes');}

好吧,另一个黑客与发电机:

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!');}

老实说,不管有没有办法让它评估为真或假(正如其他人所示,有多种方法),作为一个进行了数百次采访的人,我要寻找的答案是:

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

我想有些面试官会因为提出一个显然非常棘手的问题而生气,但我不介意有意见的开发人员,尤其是当他们可以用合理的想法来支持它,并且可以将我的问题与关于他们自己的有意义的陈述相吻合时。

这在变量a被访问的情况下是可能的,例如2个Web工作人员通过SharedArrayBuffer以及一些主脚本访问。可能性很低,但有可能当代码被编译为机器代码时,Web工作人员及时更新变量a,因此条件a==1a==2a==3得到满足。

这可以是Web Worker和JavaScript中的SharedArrayBuffer提供的多线程环境中的竞争条件的示例。

下面是上面的基本实现:

main.js

// Main Thread
const worker = new Worker('worker.js')const modifiers = [new Worker('modifier.js'), new Worker('modifier.js')] // Let's use 2 workersconst 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 = 0do {var res = a == 1 && a == 2 && a == 3++count} while(res == false) // just for clarity. !res is fineconsole.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})})

在我的MacBook Air上,它发生在第一次尝试大约100亿次迭代之后:

在此处输入图片描述

第二次尝试:

在此处输入图片描述

正如我所说,机会会很低,但如果有足够的时间,它会达到条件。

提示:如果在你的系统上花费太长时间。只尝试a == 1 && a == 2并将Math.random()*3更改为Math.random()*2。添加更多列表会降低命中的机会。

javascript

a==a+1

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

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

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

诚然,这并不完全是面试官所问的(它不适用于a=0),但它不涉及任何隐藏函数或运算符重载的技巧。

其他语言

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

ruby

使用自定义==

class Adef ==(o)trueendend
a = A.new
if a == 1 && a == 2 && a == 3puts "Don't do this!"end

或者增加a

def a@a ||= 0@a += 1end
if a == 1 && a == 2 && a == 3puts "Don't do this!"end

python

您可以为新类定义==

class A:def __eq__(self, who_cares):return Truea = A()
if a == 1 and a == 2 and a == 3:print("Don't do that!")

或者,如果你喜欢冒险,重新定义整数的值

import ctypes
def deref(addr, typ):return ctypes.cast(addr, ctypes.POINTER(typ))
deref(id(2), ctypes.c_int)[6] = 1deref(id(3), ctypes.c_int)[6] = 1deref(id(4), ctypes.c_int)[6] = 1
print(1 == 2 == 3 == 4)# True

它可能会分段故障,具体取决于您的系统/解释器。

python控制台因上述代码而崩溃,因为23可能在后台使用。如果您使用不太常见的整数,它可以正常工作:

>>> import ctypes>>>>>> def deref(addr, typ):...     return ctypes.cast(addr, ctypes.POINTER(typ))...>>> deref(id(12), ctypes.c_int)[6] = 11>>> deref(id(13), ctypes.c_int)[6] = 11>>> deref(id(14), ctypes.c_int)[6] = 11>>>>>> print(11 == 12 == 13 == 14)True

Java

可以修改Java#0缓存

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.");}}}

没有getter或value的示例:

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

这是有效的,因为==调用toString,它为数组调用.join

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

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

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

  1. 编码:在这种情况下,您正在查看的变量不是您认为的那个变量。如果您故意使用同形文字空格字符来处理Unicode,则可能会发生这种情况使变量的名称看起来像另一个变量,但编码问题也可能意外引入,例如当从Web复制和粘贴包含意外Unicode代码点的代码时(例如,因为内容管理系统做了一些“自动格式化”,例如将fl替换为Unicode'LATIN SMALL LIGATURE FL'(U+FB02))。

  2. 比赛条件:可能会出现竞争条件的情况,即代码没有按照开发人员预期的顺序执行。竞争条件经常发生在多线程代码中,但多线程不是竞争条件成为可能的要求——异步就足够了(不要混淆,async并不意味着在引擎盖下使用多个线程)。

    请注意,因此JavaScript也不能仅仅因为它是单线程的而免于竞争条件。请参阅这里了解一个简单的单线程但异步的示例。然而,在单个语句的上下文中,竞争条件在JavaScript中很难达到。

    带有Web Worker的JavaScript有点不同,因为您可以有多个线程。@mehulmpt向我们展示了一个很棒的使用Web Worker进行概念验证

  3. 副作用:相等比较操作的副作用(不需要像这里的示例中那样明显,通常副作用非常微妙)。

这些问题可能出现在许多编程语言中,不仅仅是JavaScript,所以我们在这里没有看到经典的JavaScript WTF1

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

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

1例如,你可以在一个完全不同的编程语言(C#)中找到一个例子,它展示了一个副作用(一个明显的副作用)在这里

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

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答案:

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

由于==的使用,JavaScript应该强制a进入接近第二个操作数的东西(在这种情况下是123)。但是在JavaScript试图自己理解强制之前,它试图调用#5。如果你提供Symbol.toPrimitive,JavaScript将使用你的函数返回的值。如果不是,JavaScript将调用#7

这是@陈志立*的倒置版本,其中隐藏字符(U+115F、U+1160或U+3164)用于创建看起来像123的变量。

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\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)。

使用代理

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. 代理拦截使用“get handler”获取a[Symbol.toPrimitive]函数
  6. 代理的“get处理程序”检查正在获取的属性是否为Symbol.toPrimitive,在这种情况下,它会递增,然后从目标对象返回计数器:++target.i。如果正在检索不同的属性,我们只需返回默认属性值target[name]

所以:

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

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

是的,有可能!😎

”JavaScript

if‌=()=>!0;var a = 9;
if‌(a==1 && a== 2 && a==3){document.write("<h1>Yes, it is possible!😎</h1>")}

上面的代码是一个简短的版本(感谢@Forivin在评论中的注释),以下代码是原创的:

var a = 9;
if‌(a==1 && a== 2 && a==3){//console.log("Yes, it is possible!😎")document.write("<h1>Yes, it is possible!😎</h1>")}
//--------------------------------------------
function if‌(){return true;}

如果你只是看到我的代码的顶部并运行它,你会说哇,怎么做?

所以我认为这是足够的说是的,这是可能的的人说你:没有什么是不可能的

技巧:我在if之后使用了一个隐藏字符来创建一个名称类似于if的函数。在JavaScript中,我们不能覆盖关键字,所以我强制使用这种方式。这是一个假的if,但在这种情况下它适用于你!


"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!😎");}}

现场演示

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

i=0,a={valueOf:()=>++i}
if (a == 1 && a == 2 && a == 3) {console.log('Mind === Blown');}

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

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

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,依此类推,因为每次调用valueOf时,a的值都会递增。

因此,console.log将触发并输出(无论如何在我的终端中)Thing: { value: 4},指示条件为真。

正如我们已经知道的,松散等号操作符(==)的秘密将尝试将两个值转换为通用类型。因此,将调用一些函数。

ToPrimitive(A)尝试将其对象参数转换为原语值,通过调用A.toStringA.valueOf的不同序列方法在A.

因此,与使用整数中的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('Hello World!');}

通过这种方式,我们可以处理这样的文本

let a = { array: ["World", "Hello"], toString: () => a.array.pop() };
if(a == "Hello" && a == "World") {console.log('Hello World!');}

是的,您可以,请参阅以下javascript代码:

let a = 0 // Create a variable and give it a value    
if( a !== 1 && a !== 2 && a !== 3 ){console.log("true")}

解决方案说明:

简单地,我们添加不等号在==符号之前,以便我们告诉语言这些值是不等于变量中的值

感谢

令人惊讶的是,是的。JS中的==松散等号操作符调用要比较的对象的valueOf()方法。因此,您可以创建一个返回内部值的类,然后每次调用时都会增加该间隔值。像这样:

class AClass {constructor(initalVal) {this.val = initalVal;}  
valueOf() {return this.val++;}}
const a = new AClass(1);console.log(a==1 && a==2 && a==3)

我知道这个问题还有很多其他答案,但这就是您使用ES6语法的方式。

说明:如果你不希望这种情况发生,那么你应该使用===运算符来检查严格。像这样:

class AClass {constructor(initalVal) {this.val = initalVal;}  
valueOf() {return this.val++;}}
const a = new AClass(1);console.log(a===1 && a===2 && a===3)