文章来自:https://zhangzhao.name/posts/make-a-1-a-2-a-3-evaluate-true/以下为原文:前两天在网上看到一个很有意思的话题。题目大致是这样写的:在JS环境下,如何让表达式a==1&&a==2&&a==3返回true?.这个问题乍一看似乎不太可能,因为一般情况下,如果不手动修改变量的值,它在表达式中是不会改变的。当时我也琢磨了很久,甚至一度怀疑这个问题的答案是否定的。直到我在stackoverflow上真正找到解决方案can-a-1-a-2-a-3-ever-evaluate-to-true。使这个表达式为真的关键是这里的松散相等。JS在处理松散相等时会隐式转换一些变量。在这种隐式转换的作用下,真的可以让一个变量在一个表达式中变成不同的值。松等式下真值表最高票答案给出的解为:consta={i:1,toString:function(){returna.i++;}}if(a==1&&a==2&&a==3){console.log('HelloWorld!');}看到这个答案,突然意识到这道题的考点竟然是JS获取一个变量需要做的操作和一些细节。JS中判断两个变量是否相等有===和==两种方式。对JS稍有了解的人都知道,===是严格相等,不仅要求两个变量的值相同,而且类型相同,而==是松散相等,只能通过相同的值,松散相等是严格相等的一个子集。所以在JS中,两个严格相等的变量也一定是松散相等的,但是两个松散相等的变量在大多数情况下并不严格相等。例如:null==undefined//truenull===undefined//false1=='1'//true1==='1'//false这里出现了JS特有的变量松散相等判断真值表,列出两个变量在松等比较情况下所有可能的类型:上表中,ToNumber(A)试图在比较之前将参数A转换为数字,这与+A(一元运算符+)相同同样的效果。ToPrimitive(A)通过尝试顺序调用A的A.toString()和A.valueOf()方法,将参数A转换为原始值(Primitive)。从上图我们可以看出,当操作数B是Number类型时,如果希望整个表达式的结果在松散相等的情况下返回true,则操作数A必须满足以下三个条件之一:操作数A类型是String,调用+A的结果严格等于B。OperandA是Boolean类型,调用+A的结果严格等于B。OperandA是Object类型,调用toStringor的结果ValueOf严格等于B。这里如果我们想改变+A操作的结果,是比较困难的,因为我们很难在JS中重载+操作符的操作。但在第三种情况下,使A成为Object类型,调用toString或ValueOf导致与B严格相等,这对我们自己实现起来要容易得多。所以上面的答案是用toString方法创建一个新对象a。JS引擎每次读取a的值时,发现需要在object和number之间做一个松散的判断。对于对象来说,这里会执行的toString方法,在这个方法中,我们每次增加另一个变量的值并返回,就可以让a的结果在这个表达式中有不同的值。同样,换一种写法,a是Object,使用valueOf也可以达到目的:cosnta={i:1,valueOf(){returnthis.i++}}if(a==1&&a==2&&a==3){console.log('HelloWorld!');}松散相等下的Proxy对象有上面的思路,下面的实现就容易多了。在ES6中,JS增加了一个Proxy对象,可以劫持一个对象,接受两个参数。第一个是要劫持的对象,第二个参数也是一个对象。每个元素的get方法也可以在内部配置。:vara=newProxy({i:1},{get(target){return()=>target.i++}});if(a==1&&a==2&&a==3){console.log('HelloWorld!');}同样,Proxy对象默认的toString和valueOf方法会返回getter劫持的结果,在松散相等的条件下也能满足题意。严格相等条件下的实现以上方法是利用JS在松相等条件下的一些特殊表现实现的。它们在===严格相等条件下是不能满足的,因为在严格相等条件下,不会对两个操作数做任何处理,直接比较它们值的大小,所以上面的做法不会成功.但这种方法为我们提供了一个好主意。在处理类似的问题时,我们可以从JS中获取一个变量的执行过程开始思考。那么接下来,如果把题目中的松散相等换成严格相等,这样的例子还存在吗?if(a===1&&a===2&&a===3){console.log('HelloWorld!');}答案显而易见,这个时候当然不能用hack对象或Proxy的toString或ValueOf方法来执行此操作。从JS中获取变量的过程开始,数据的getter和setter方法马上就可以想当然了。通过这样的hack,绝对可以满足题目严格的相等性要求。ES5之后,Object新增了一个defineProperty方法,可以直接在一个对象上定义一个新的属性,或者修改一个对象已有的属性,并返回该对象。有两种状态来描述定义的对象,一种称为数据描述符,另一种称为访问描述符。下面是一个例子:vara={}Object.defineProperty(a,'value',{enumerable:false,configurable:false,writable:false,value:"static"})这四个数据描述符的函数可枚举到判断是否可以枚举,configurable判断描述符后当前属性是否可以改变,writable判断是否可以继续赋值,value判断结果的值。这样操作之后,a对象下就有了value键。它被分配了一个非连续的赋值,非连续的配置,并且不能被枚举。该值为“静态”。我们这里可以通过a.value'得到'static',但是不能通过a.value='relative'继续赋值。同样,设置访问描述符也是四个属性:vara={i:1}Object.defineProperty(a,'value',{enumerable:false,configurable:false,get(){returna.i}set(){a.i++}})这里设置的时候没有配置writable和value属性,而是配置了get和set方法。在这两种配置中,getset方法和writablevalue不能共存,否则会抛出异常。和上面的设置类似,当我们访问a.value时会调用get方法,当我们传递a.value='test'时会执行set方法。那么言归正传,当我们访问一个设置了访问描述符的元素时,如果我们在get方法中做一些操作,就可以巧妙地让最终的结果符合预期:vari=1Object.defineProperty(window,'a',{get(){returni++}})if(a===1&&a===2&&a===3){console.log('HelloWorld!');}同时,this这个方法被劫持getters和setters本质上是执行一个函数。除了使用自增变量,还有更多方法:constvalue=function*(){leti=1while(true)yieldi++}()Object.defineProperty(window,'a',{get(){returnvalue.next().value}})if(a===1&&a===2&&a===3){控制台。log('HelloWorld!');}总结在严格相等的情况下,一般来说只能通过劫持datagetter来进行操作,但是上面列举了很多具体的操作方法。对于松散相等,除了劫持getter,因为松散相等的JS引擎,还可以使用Object和Proxy对象的valueOf和toString方法来达到目的。当然,stackoverflow中有人提出了另一种做法,就是在a变量前后使用不同的字符来达到目的。原理是有些字符是肉眼看不到的,所以虽然看起来像a,变量实际的区别也能满足题目的要求,但这不在本文讨论范围之内。文章来源:https://zhangzhao.name/posts/make-a-1-a-2-a-3-evaluate-true/
