也许你我素未谋面,但很可能相见恨晚,我是前端胖头鱼
前言
最近遇到一个非常有意思的面试题: 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
变量,那也可以伪造三个1
、2
、3
变量嘛
const a = 1
const ᅠ1 = a
const ᅠ2 = a
const ᅠ3 = a
if (a == ᅠ1 && a == ᅠ2 && a == ᅠ3) {
console.log('前端胖头鱼') // 前端胖头鱼
}
大千世界,果然眼见为虚啊!!!
再来一种奇特的解法
上面几种解法本质上都没有使 a == 1 && a == 2 && a == 3
为true
,不过是障眼法,大家笑笑就好啦!接下来我要认真起来了...
解法4:“with”
MDN上映入眼帘的是一个警告,仿佛他的存在就是个错误,我也从来没有在实际工作中用过他,但他却可以用来解决这个题目。
let i = 1
with ({
get a() {
return i++
}
}) {
if (a == 1 && a == 2 && a == 3) {
console.log('前端胖头鱼')
}
}
聪明的你甚至都不用我解释代码啥意思了。
隐式转换成解题的关键
上面给出的4种解法多少有点歪门邪道的意思,为了让面试官死心,接下来的才是正解之道,而JS中的隐式转换规则大概也是出这道题的初衷。
隐式转换部分规则
JS中使用==
对两个值进行比较时,会进行如下操作:
- 将两个被比较的值转换为相同的类型。
- 转换后(等式的一边或两边都可能被转换)再进行值的比较。
比较的规则如下表(mdn)
从表中可以得到几点信息为了让(a == 1
),a只有这几种:
- a类型为String,并且可转换为数字1(
'1' == 1 => true
) - a类型为Boolean,并且可转换为数字1 (
true == 1 => true
) - a类型为Object,通过
转换机制
后,可转换为数字1 (请看下文
)
对象转原始类型的"转换机制"
规则1和2没有什么特殊的地方,我们来看看3:
对象转原始类型,会调用内置的[ToPrimitive]函数,逻辑大致如下:
- 如果有Symbol.toPrimitive方法,优先调用再返回,否则进行2。
- 调用valueOf,如果可以转换为原始类型,则返回,否则进行3。
- 调用toString,如果可以转换为原始类型,则返回,否则进行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
当然也可以利用valueOf
和toString
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('前端胖头鱼') // 前端胖头鱼
}
最后
希望能一直给大家分享实用、基础、进阶的知识点,一起早早下班,快乐摸鱼。
期待你在掘金关注我:前端胖头鱼,也可以在公众号里找到我:前端胖头鱼。