最近的一道热门的题目 `Can (a ==1 && a== 2 && a==3) ever evaluate to true?
`, 引起了很多人的关注与讨论, 自己可以很容易想到一种实现, 但是看大家讨论出来的答案, 其中有很多有意思的, 不得不佩服一些人的脑洞, 其中很多原理也值得探讨.
很多其他语言的程序员对于这样的结果, 很多都归结于 果然 javascript 之类, 的确与一些语言不同, javascript 中除了 == 外还有 ===,=== 叫做严格运算符,== 叫做相等运算符.
关于 === 于 ==
其实 JavaScript 一共提供了 8 个比较运算符.
< 小于运算符
> 大于运算符
<= 小于或等于运算符
= 大于或等于运算符
== 相等运算符
=== 严格相等运算符
!= 不相等运算符
!== 严格不相等运算符
比较操作涉及多不同类型的值时候, 会涉及到很多隐式转换, 其中规则繁多即便是经验老道的程序员也没办法完全记住, 特别是用到 == 和 != 运算时候. 所以一些团队规定禁用 == 运算符换用 === 严格相等. 以工程标准衡量,== 带来的便利性抵不上其带来的成本, 团队协作时候你看到别人代码中的 ==, 有些时候需要判断清楚作者的代码意图是确实需要转型, 还是无所谓要不要转型只是随手写了, 增加了一些额外的成本. 但是我比较喜欢的一本书 You don't know JS, 中作者也写道过一个我比较赞同的观点
Many developers feel that === is more predictable, so they advocate always using that form and staying away from ==. I think this view is very shortsighted. I believe == is a powerful tool that helps your program, if you take the time to learn how it works.
简译为
很多开发者认为 === 的行为更加容易预测, 从而主张使用 === 而远离 ==. 我认为这种观点是非常短视的, 如果你花点时间去搞清楚它的工作原理,== 将是你开发的强大工具.
究竟谁对谁错也没有定论, 但是去了解一些隐式转换的规则, 对我们开发和调试 bug 都非常有帮助. 这些规则规定很烦多, 一个个看过来很无趣也不能全记住. 我们下面从这个问题的一些答案中去探究其中的一小部分规则.
题目一些答案
大家来找茬?
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
console.log("Why hello there!")
}
看到这个答案时候曾一度怀疑自己学的是假的 javascript, 这个答案和隐式转换没有关系, 可以说它能考察你的找茬能力, 注意 if 里面的空格, 它是一个 Unicode 空格字符, 不被 ECMA 脚本解释为空格字符
(这意味着它是标识符的有效字符)
. 所以它可以解释为
var a_ = 1;
var a = 2;
var _a = 3;
if (a_ == 1 && a == 2 && _a == 3) {
console.log("Why hello there!")
}
运算子是对象时候的 valueOf toString 方法
const a = {
i: 1,
toString: function() {
return a.i++;
}
}
if (a == 1 && a == 2 && a == 3) {
console.log('Hello World!');
}
如果原始类型的值和对象比较, 对象会转为原始类型的值, 再进行比较.
(我想到的也是这种方法)
, 对象转换成原始类型的值, 算法是先调用 valueOf 方法; 如果返回的还是对象, 再接着调用 toString 方法. 我们每次比较时候都会执行方法返回 a 的 i 属性同时也改变 i 的值, 所以上面 if 执行完以后 a 的 i 属性已经变为了 4, 这里也表现出了 == 比较是有可能会对变量带来副作用的
利用数组的特性
var a = [1, 2, 3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3);
这个答案还是比较巧妙的, 我们知道 array 也属于对象, 应该和对象的规则一样. 关于 array 的原型链上的 toString 方法
对于数组对象, toString 方法返回一个字符串, 该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接 (由逗号隔开) 组成.
可以看到数组 toString 会调用本身的 join 方法, 这里把自己的 join 方法该写为 shift, 每次返回第一个元素, 而且原数组删除第一个值, 正好可以使判断成立. 这里 == 比较也带来的副作用
利用 with 关键字
var i = 0;
with({
get a() {
return++i;
}
}) {
if (a == 1 && a == 2 && a == 3) console.log("wohoo");
}
with 也是被严重建议不使用的对象, 这里也是利用它的特性在代码块里面利用对象的 get 方法动态返回 i.
和 with 类似修改 window 的 get 方法
var val = 0;
Object.defineProperty(window, 'a', {
get: function() {
return++val;
}
});
if (a == 1 && a == 2 && a == 3) {
console.log('yay');
}
我们知道我们用的全局变量也相当于 window 对象上的一个属性, 这里用 defineProperty 定义了 a 的 get 也使得其动态返回值. 和 with 有一些类似.
es6 的 Symbol 特性
let a = {[Symbol.toPrimitive]: ((i) => () => ++i) (0)};
console.log(a == 1 && a == 2 && a == 3);
ES6 引入了一种新的原始数据类型 Symbol, 表示独一无二的值. 我们之前在定义类的内部私有属性时候习惯用 __xxx , 这种命名方式避免别人定义相同的属性名覆盖原来的属性, 有了 Symbol 之后我们完全可以用 Symbol 值来代替这种方法, 而且完全不用担心被覆盖.
除了定义自己使用的 Symbol 值以外, ES6 还提供了 11 个内置的 Symbol 值, 指向语言内部使用的方法. Symbol.toPrimitive 就是其中一个, 它指向一个方法, 表示该对象被转为原始类型的值时, 会调用这个方法, 返回该对象对应的原始类型值. 这里就是改变这个属性, 把它的值改为一个 闭包 返回的函数.
小记
现实业务中一般不会有人去写出这种代码, 但是这个题目感觉还是很有意思的, 每个人开发脑洞后想出来的各种答案, 还是能给人很多方面的思考.
本文首发于自己的 个人博客
来源: http://click.aliyun.com/m/41009/