【前端进阶】如何让 (a == 1 && a == 2 && a == 3) 的值为true?

背景:由高级前端工程师@刘小夕 在github上发起的一个开源项目:每个工作日发布一个前端相关的问题,每周进行一次汇总。我觉得很不错,也参与其中,如果你也感兴趣,可以一起参与,项目地址:https://github.com/YvetteLau/Step-By-Step

我觉得这道题主要考验的是==运算符的工作机制和类型转换的一些知识。==的工作机制自己是明白,也知道对象在转换为原始类型的时候会调用valueOf()toString(),但是却一直没有想到要重写这两个方法,所以这道题自己并没有解答出来,以下是参考其他小伙伴的回答加上自己的理解整理出来的,感谢其他小伙伴的解答。

首先要解答这道题,要先搞懂==的工作机制。

==运算符的工作机制

对于==来说,如果对比双方类型不一样的话,就会先进行类型转换。

假设我们需要对比xy是否相同,就会进行如下判断流程:

1.首先会判断两者类型是否相同,相同的话就比较大小了

2.类型不相同的话,那么就进行类型转换

3.判断两者类型是否为stringnumber,是的话就将字符串转换为number

1 == '1';
比较过程如下:
1 == 1; // true

4.判断其中一方是否为boolean,是的话就会把boolean转换为number再进行判断

'1' == true;
比较过程如下:
'1' == 1;
1 == 1; // true

5.判断其中一方是否为object,另一方为stringnumber、或symbol,是的话就会把object转换为原始类型再进行判断

'1' == { name: 'dazhi' }
比较过程如下:
'1' == '[object object]'

6.会先判断是否在对比nullundefined,是的话就返回true

7.要比较相等性之前,不能将nullundefined转换成其他任何值

8.如果有其中一方是NaN,则相等操作符返回false,而不相等操作符返回true

重要提示:即使两个操作数都是NaN,相等操作符也返回false了;因为按照规则,NaN不等于NaN

弄懂了==的工作机制,我们再回到题目:

(a == 1 && a == 2 && a == 3) == true

根据题目,我们可以推断a不可能是一个基本数据类型,因为a如果是nullundefined或者boolean,这个等式根本不会成立。所以a肯定是一个复杂数据类型:object,有可能是一个对象{}或者是数组[]

当一个对象object和数值做==比较的时候,会先把object转换为原始类型再进行比较。

所以,我们还需明白object到原始类型转换的一个过程:

  • 如果部署了[Symbol.toPrimitive]接口,那么调用此接口,若返回的不是基本数据类型,抛出错误。
  • 如果没有部署[Symbol.toPrimitive]接口,那么调用valueOf接口,若返回的不是基本数据类型,那么调用toString接口,若返回的还不是基本数据类型,那么抛出异常。
let obj = {
  [Symbol.toPrimitive]() {
    return 100;
  },

  valueOf() {
    return 200;
  }
}

let obj2 = {
  valueOf() {
    return 200;
  },

  toString() {
    return 300;
  }
}

console.log(obj == 100); // true
console.log(obj2 == 200); // true

上面代码说明,它们之间的一个的优先调用顺序是:[Symbol.toPrimitive] > valueOf > toString

a是一个{}

var a = {
  [Symbol.toPrimitive]: (function () {
    let i = 1;
    //闭包的特性之一:i 不会被回收
    return function () {
      return i++;
    }
  })()
}

console.log(a == 1 && a == 2 && a == 3); // true

如果没有部署[Symbol.toPrimitive]接口,则会调用valueOf接口,所以下面的代码也是可以的:

var a = {
  valueOf: (function () {
    let i = 1;
    //闭包的特性之一:i 不会被回收
    return function () {
      return i++;
    }
  })()
}

console.log(a == 1 && a == 2 && a == 3); // true

a是一个[]数组时

var a = [1,2,3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3); // true

不得不说,此方法真的是很巧妙。
数组也是一个对象,所以也遵循对象到原始类型的转化过程,然后又利用数组的Array.prototype.toString()内部调用的是Array.prototype.join(),所以把join重写为shift,这样当比较a == 1时,相当于执行了

a.shift();

由于shift()方法会改变原数组,并删除数组的第一个元素,然后把该元素返回回去。所以第一次比较删除的就是1,并把1返回回去,a == 1就为true了,然后原数组a就变成了[2,3],以此类推。

根据这个原理,下面的代码也是等价的:

var a = [3,2,1];
a.join = a.pop;
console.log(a == 1 && a == 2 && a == 3); // true

利用数据劫持

数据劫持还是第一次听到,过后需要好好补下这块相关的知识。

重写a属性的getter方法

使用Object.defineProperty定义的属性,在获取属性时,会调用get方法。利用这个特性,我们在window对象上定义a属性,如下:

let i = 1;
Object.defineProperty(window, 'a', {
  get: function() {
    return i++;
  }
})
console.log(a == 1 && a == 2 && a == 3); // true

Proxy形式实现

这个也是盲点,过后也需要好好学习下。

利用ES6新增的Proxy来实现:

const a = new Proxy({}, {
  v: 1,
  get: function() {
    return () => this.v++;
  }
});
console.log(a == 1 && a == 2 && a == 3); // true

最后

感谢各位小伙伴,学到很多知识点。希望大家能一直坚持下去,向前端专家进阶,加油!