共计 3745 个字符,预计需要花费 10 分钟才能阅读完成。
0. 前言
Object 与 Primitive,需要 Object 转为 Primitive
String 与 Boolean,需要两个操作数同时转为 Number。
String/Boolean 与 Number,需要 String/Boolean 转为 Number。
undefined 与 null,和所有其他值比较的结果都是 false,他们之间 == 成立
ToPrimitive 是指转换为 js 内部的原始值,如果是非原始值则转为原始值,调用 valueOf() 和 toString()来实现。valueOf 返回对象的值:在控制台,当你定义一个对象按回车,控制台打印的是 Object{…},toString()返回对象转字符串的形式,打印的是 ”[object Object]”
如果参数是 Date 对象的实例,那么先 toString()如果是原始值则返回,否则再 valueOf(),如果是原始值则返回,否则报错。
如果参数不是 Date 对象的实例,同理,不过先 valueOf 再 toString()。
1. 一些例子
在浏览器控制台输入一些各种运算符的组合,会出现一些有意思的结果:
![] //false; +[] // 0 +![] // 0[]+[] // “”{}+{}//”object Object”{}+[]//0{a:0}+1 // 1[]+{}//”[object Object]”[]+![]//”false”{}+[]//0![]+[] // “false””+{} //”[object Object]”{}+” //0[][“map”]+[] //”function map() {}”[][“a”]+[] // “undefined”[][[]] + []// “undefined”+!![]+[] //”1″+!![] //11-{} //NaN1-[] //1true-1//0{}-1 //-1[]==![] //true2. 从 []==![] 开始
我们知道,[]!=[],主要是因为他们是引用类型,内存地址不同所以不相等。那么为什么加了一个!就能划上等于号了
符号的优先度
可以参考 mdn 上的这个汇总表格:https://developer.mozilla.org… 可以看见,[]==![]这个情况下先判断!再判断 = 给 [] 取反,会是布尔值,[]的取反的布尔值就是 false
2.1 []的反就是 false?常见的一些转换:
非布尔类型转布尔类型:undefined、null、0、±0、NaN、0 长度的字符串 =》false,对象 =》true 非数字类型转数字类型:undefined=》NaN,null=》0,true=》1,false=》0,字符串:字符串数字直接转数字类型、字符串非数字 =》NaN
回到 []==![] 的问题上,[]也是对象类型(typeof [] == “object”),转为布尔类型的![]就是 false
2.2 等号两边对比
我们知道,在比较类型的时候,先会进行各种各样的类型转换。从开头的表格可以看见,他们比较的时候都是先转换为数字类型。右边是布尔值 false,左边为一个空数组对象,对于左边,先进行 ToPrimitive 操作,先执行 valueOf([])返回的是 [],非原始类型,再 [].toString(),返回的是 ””,那 ToPrimitive 操作之后,结果就是 ”” 了 最后,左边 ”” 和右边 false 对比,他们再转换为数字,就是 0 == 0 的问题了
3. 更多玩法 3.1 间接获取数组方法
我们知道,数组有自己的一套方法,比如 var arr = [1,2];arr.push(1),我们可以写成 [1,2].push(1),还可以写成[1,2]’push’,那么前面抛出的问题就解决了
[]’push’ //[1][][“map”] //function map() { [native code] }[][“map”]+[] // “function map() {[native code] }”3.2 间接进行下标操作 3.2.1 数字的获取
我们可以通过类型转换,获得 0 和 1 两个数字,既然能得到这两个数字,那么也可以得到其他的一切数字了:+[] === 0; +!![] === 1 那么,+!![]+!![] ===2,+((+![])+(+!![])+[]+(+![]))===10
那么 10-1= 9 也就来了:+((+![])+(+!![])+[]+(+![]))-!![] ===9 简直就是无所不能
3.2.2 字符串下标(![]+[])[+[]] //”f”(![]+[])[+!![]] // “a”
(![]+[]) 是 ”false”,其实 (![]+[])[+[]] 就相当于 ”false”[0],第一个字母,就是 f 我们就可以从上面的那些获得单词的字符串获得其中的字母了,比如:(![]+[])[+!![]+!![]+!![]] +([]+{})[+!![]+!![]]
掌握基本套路后,我们可以随心所欲发挥,在浏览器的控制台输入一些符号的组合,然后回车看一下我们写的“密码”会转换成什么
([][[]] + [])[(+!![] + [] + [] + +![]) >> +!![]] +$$(‘*’)[~~[]].nodeName[- ~ -+~[]] +(this + [])[+!![]+[] + +[] – !!{} – !!{}] +([]+![])[+!!{} << +!![] -~[]] +({}+{})[+!![] -~[]]4. 两个面试题
曾经遇到两个这种类型的面试题:
4.1 (a==1 && a==2 && a==3) 能不能为 true
(a==1 && a==2 && a==3)或者 (a===1 && a===2 && a===3) 能不能为 true?事实上是可以的,就是因为在 == 比较的情况下,会进行类型的隐式转换。前面已经说过,如果参数不是 Date 对象的实例,就会进行类型转换,先 valueOf 再 obj.toString() 所以,我们只要改变原生的 valueOf 或者 tostring 方法就可以达到效果:
var a = {num: 0, valueOf: function() {return this.num += 1}};var eq = (a==1 && a==2 && a==3);console.log(eq);// 或者改写他的 tostring 方法 var num = 0;Function.prototype.toString = function(){ return ++num;}function a(){}// 还可以改写 ES6 的 symbol 类型的 toP 的方法 var a = {[Symbol.toPrimitive]: (function (i) {return function(){return ++i} }) (0)};
每一次进行等号的比较,就会调用一次 valueOf 方法,自增 1,所以能成立。另外,减法也是同理:
var a = {num: 4, valueOf: function() {return this.num -= 1}};var res = (a==3 && a==2 && a==1);console.log(res);
另外,如果没有类型转换,是 === 的比较,还是可以的。在 vue 源码实现双向数据绑定中,就利用了 defineProperty 方法进行观察数据被改变的时候,触发 set。每一次访问对象中的某一个属性的时候,就会调用这个方法定义的对象里面的 get 方法。每一次改变对象属性的值,就会访问 set 方法 在这里,我们自己定义自己的 get 方法:
var b = 1Object.defineProperty(window, ‘a’, { get:function() {return b++;}})var s = (a===1 && a===2 && a === 3)console.log(s)
每一次访问 a 属性,a 的属性值就会 +1,当然还是交换位置就不能为 TRUE 了
4.2 完善 Cash 打印三个 101
要求只能在 class 里面增加代码:
class Cash {}const a = new Cash(1)const b = new Cash(100)console.log(${a.add(b)},${Cash.add(a,b)},${new Cash(a+b)}
) // 101,101,101
首先,三个输出结果是以隐式转换的形式出现的,这是关键之处。a 和 b 都是 new 出来的对象,由 new Cash(a+b)可以看出构造函数传入的也是两个 Cash 的实例对象。那么 new 出来的结果肯定不是简简单单的一个 object,不然就是被转换成 '[object Object]’,但是你又不得不以 object 类型出现,那就只能魔改隐式转换用到的 toString 和 valueOf
class Cash {constructor (a) {this.m = a // 缓存真正的值 this.valueOf = function () {console.log(‘value’) return a } } add ($) {// a.add(b) return this.m + $ } toString () { // 隐式转换调用 return this.m} static add (v1, v2) {//Cash.add return v1 + v2}}END
然而,实际项目中两个数据作比较的时候,我们尽量不要写甚至完全不要写两个等号,应该写三个等号,而且 js 也慢慢有向强类型过渡的趋势,让这些骚操作回到我们的个人收藏里面去吧