你确定(a == 1 && a == 2 && a == 3)不能为true?

avatar
FE @公众号: 前端胖头鱼

也许你我素未谋面,但很可能相见恨晚,我是前端胖头鱼

前言

最近遇到一个非常有意思的面试题: JavaScript中有没有可能让(a== 1 && a ==2 && a==3)返回true?

讲真刚看到这题的时候,我是用这种眼神看面试官的:你TM逗我呢? 尊重一下我可行?没10年脑血栓问不出这玩意,

但看他一脸"贱笑",一副你一定答不出来的感觉,我觉得此事定不简单...

障眼法我TM给跪了

咱们先不管面试官的意图是什么,具体考察的是什么知识,先来看看几种奇特的解法。

解法1:隐藏字符 + if


const if‌ = () => !0
const a = 9

if‌(a == 1 && a == 2 && a == 3)
{
  console.log('前端胖头鱼') // 前端胖头鱼
}

眼见为虚

我觉得此时你和我一样,在严重怀疑自己怕是个假前端if也能被改写?a明明是9却可以等于1、2、3

别急,这其实是一个障眼法,只是取巧蒙蔽了我们的双眼,请看下图

真相大白if的后面有个隐藏字符,本质上是声明了一个无论输入啥都返回true函数,而下面的代码块,更是和这个函数没半毛钱关系,怎么样都会执行!!!


{
  console.log('前端胖头鱼') // 前端胖头鱼
}

所以通过构造一个看似重写了if的代码块,仿佛真的实现了题目,实在是太骚了!!!

解法2:隐藏字符 + a变量

有了上面的经验,接下来的解法,你也不会感到奇怪了。

const aᅠ = 1
const a = 2
const ᅠa = 3

if (aᅠ == 1 && a == 2 && ᅠa == 3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

解法3:隐藏字符 + 数字变量

既然可以伪造三个a变量,那也可以伪造三个123变量嘛

const a = 1
const1 = a
const2 = a
const3 = a

if (a == ᅠ1 && a == ᅠ2 && a == ᅠ3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

大千世界,果然眼见为虚啊!!!

再来一种奇特的解法

上面几种解法本质上都没有使 a == 1 && a == 2 && a == 3true,不过是障眼法,大家笑笑就好啦!接下来我要认真起来了...

解法4:“with”

MDN上映入眼帘的是一个警告,仿佛他的存在就是个错误,我也从来没有在实际工作中用过他,但他却可以用来解决这个题目。

let i = 1

with ({
  get a() {
    return i++
  }
}) {
  if (a == 1 && a == 2 && a == 3) {
    console.log('前端胖头鱼')
  }
}

聪明的你甚至都不用我解释代码啥意思了。

隐式转换成解题的关键

上面给出的4种解法多少有点歪门邪道的意思,为了让面试官死心,接下来的才是正解之道,而JS中的隐式转换规则大概也是出这道题的初衷。

隐式转换部分规则

JS中使用==对两个值进行比较时,会进行如下操作:

  1. 将两个被比较的值转换为相同的类型。
  2. 转换后(等式的一边或两边都可能被转换)再进行值的比较。

比较的规则如下表(mdn

从表中可以得到几点信息为了让(a == 1),a只有这几种:

  1. a类型为String,并且可转换为数字1('1' == 1 => true
  2. a类型为Boolean,并且可转换为数字1 (true == 1 => true)
  3. a类型为Object,通过转换机制后,可转换为数字1 (请看下文

对象转原始类型的"转换机制"

规则1和2没有什么特殊的地方,我们来看看3:

对象转原始类型,会调用内置的[ToPrimitive]函数,逻辑大致如下:

  1. 如果有Symbol.toPrimitive方法,优先调用再返回,否则进行2。
  2. 调用valueOf,如果可以转换为原始类型,则返回,否则进行3。
  3. 调用toString,如果可以转换为原始类型,则返回,否则进行4。
  4. 如果都没有返回原始类型,会报错。
const obj = {
  value: 1,
  valueOf() {
    return 2
  },
  toString() {
    return '3'
  },
  [Symbol.toPrimitive]() {
    return 4
  }
}

obj == 4 // true
// 您可以将Symbol.toPrimitive、toString、valueOf分别注释掉验证转换规则

解法5: Symbol.toPrimitive

我们可以利用隐式转换规则3完成题目(看完答案你就知道为什么啦!

const a = {
  i: 1,
  [Symbol.toPrimitive]() {
    return this.i++
  }
}
// 每次进行a == xxx时都会先经过Symbol.toPrimitive函数,自然也就可以实现a依次递增的效果
if (a == 1 && a == 2 && a == 3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

解法6: valueOf vs toString

当然也可以利用valueOftoString

let a = {
  i: 1,
  // valueOf替换成toString效果是一样的
  // toString
  valueOf() {
    return this.i++
  }
}

if (a == 1 && a == 2 && a == 3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

解法7:Array && join

数组对象在进行隐式转换时,同样符合规则3,只是在toString时还会调用join方法。所以也可以从这里下手

let a = [1, 2, 3]

a.join = a.shift

if (a == 1 && a == 2 && a == 3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

数据劫持亦是一条出路

通过隐式转换我们做出了3种让a == 1 && a == 2 && a == 3返回true的方案,聪明的你一定想到另一种思路,数据劫持,伟大的Vue就曾使用数据劫持赢得了千万开发者的芳心,我们也试试用它来解决这道面试题

解法8:Object.defineProperty

通过劫持window对象,每次读取a属性时,都给_a 增加1

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

if (a == 1 && a == 2 && a == 3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

解法9:Proxy

当然还有另一种劫持数据的方式,Vue3也是将响应式原理中的数据劫持Object.defineProperty换成了Proxy

let a = new Proxy({ i: 1 }, {
  get(target) {
    return () => target.i++
  }
})

if (a == 1 && a == 2 && a == 3) {
  console.log('前端胖头鱼') // 前端胖头鱼
}

最后

希望能一直给大家分享实用、基础、进阶的知识点,一起早早下班,快乐摸鱼。

期待你在掘金关注我:前端胖头鱼,也可以在公众号里找到我:前端胖头鱼