从 (a==1&&a==2&&a==3) 成立中看javascript的隐式类型转换

简介:

最近的一道热门的题目 `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方法。我们每次比较时候都会执行方法返回 ai 属性同时也改变 i 的值,所以上面 if 执行完以后 ai 属性已经变为了 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 定义了 aget 也使得其动态返回值。和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就是其中一个,它指向一个方法,表示该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。这里就是改变这个属性,把它的值改为一个 闭包 返回的函数。

小记

现实业务中一般不会有人去写出这种代码,但是这个题目感觉还是很有意思的,每个人开发脑洞后想出来的各种答案,还是能给人很多方面的思考。

本文首发于自己的个人博客

目录
相关文章
|
3月前
|
JavaScript 前端开发
JavaScript基础语法(类型转换)
JavaScript基础语法(类型转换)
27 0
|
5月前
|
JavaScript 前端开发
js中的类型转换
js中的类型转换
33 0
|
4月前
|
JavaScript 前端开发
JavaScript变量、数据类型、运算符及类型转换
JavaScript变量、数据类型、运算符及类型转换
40 0
|
8月前
|
JavaScript 前端开发 安全
深度剖析之由浅入深揭秘JavaScript类型转换(最全总结篇)(三)
深度剖析之由浅入深揭秘JavaScript类型转换(最全总结篇)(三)
|
3月前
|
JavaScript 前端开发 编译器
彻底理解JavaScript中的类型转换(上)
彻底理解JavaScript中的类型转换
|
1月前
|
JavaScript 前端开发
javascript中的类型转换
javascript中的类型转换
|
8月前
|
JavaScript 前端开发 索引
JavaScript Day02 操作符及类型转换与流程控制语句 3
JavaScript Day02 操作符及类型转换与流程控制语句 3
42 0
|
3月前
|
JavaScript 前端开发 算法
彻底理解JavaScript中的类型转换(下)
彻底理解JavaScript中的类型转换(下)
|
3月前
|
JavaScript 前端开发
【Javascript保姆级教程】显示类型转换和隐式类型转换
【Javascript保姆级教程】显示类型转换和隐式类型转换
|
4月前
|
JavaScript
JS 类型转换
JS 类型转换