如何让(a==1&&a==2&&a==3)值为true

2021/4/30 JavaScript

TIPS

问题:JavaScript环境下,如何让a == 1 && a == 2 && a == 3这个表达式返回true

参考文章

'=='与'==='的区别

# 实现方法

  1. 使用toString

    const a = {
      i: 1,
      toString() {
        return a.i++;
        // return this.a++; // 一样的
      },
    };
    
    if (a == 1 && a == 2 && a == 3) {
      console.log('Hello World!');
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  2. 使用valueOf

    const a = {
      i: 1,
      valueOf() {
        return this.i++;
      },
    };
    
    if (a == 1 && a == 2 && a == 3) {
      console.log('Hello World!');
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  3. 使用Proxy

    在ES6中JS新增了Proxy对象,能够对一个对象进行劫持,接受两个参数,第一个是需要被劫持的对象,第二个参数也是一个对象,内部也可以配置每一个元素的get方法:

    const a = new Proxy({ i: 1 }, {
      get(target) {
        return () => target.i++;
      },
    });
    
    if (a == 1 && a == 2 && a == 3) {
      console.log('Hello World!');
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    同样的,Proxy对象默认的toStringvalueOf方法会返回这个被getter劫持过的结果,也能够在宽松相等的条件下满足题意。

JS特有的,变量宽松相等判断的真值表,里面列举了所有在宽松相等比较的情况下,两种变量可能出现的类型:

image-default

在上面的表格中,ToNumber(A)尝试在比较前将参数A转换为数字,这与+A(单目运算符+)的效果相同。ToPrimitive(A)通过尝试依次调用AA.valueOf()A.toString()方法,将参数A转换为原始值(Primitive)。

从上图中我们可以看到,当操作数B类型为number时,如果希望在宽松相等的情况下整个表达式的结果返回true,操作数A必须满足下面三个条件之一:

  • 操作数A类型为string,并且调用+A的结果与B严格相等。
  • 操作数A类型为boolean,并且调用+A的结果与B严格相等。
  • 操作数A类型为object,并且调用valueOf或者toString返回的结果与B严格相等。

在这里,如果我们要改变+A操作的结果相对来说比较困难,因为我们很难在JS中去重载+操作符的运算。但是在第三种情况下,使A的类型为object,调用valueOftoString结果与B严格相等让我们自己实现就容易的多。

所以上面的答案就是新建了一个对象a,并有toString方法,当JS引擎每次读取a的值的时候,发现需要进行宽松判断一个对象和一个数字之间的结果,对于对象就会执行这里的toString方法,在这个方法内部,我们每次增加另一个变量的值并返回,就能够在这条表达式中使得a的结果有不同的值。同理,换一种写法,aobject,使用valueOf也是可以完成目标的。

# 严格相等下的实现

在ES5之后,Object新增defineProperty方法,它会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象,对于定义的这个对象有两种描述它的状态,一种称之为数据描述符,一种被称为存取描述符,分别举一个例子:

var a = {};
Object.defineProperty(a, 'value', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'static',
});
1
2
3
4
5
6
7

这四个数据描述服分别作用是

  • enumerable:是否可以枚举。
  • configurable:当前属性是否之后再被更改描述符。
  • writable:是否可以继续赋值。
  • value:这个结果的值。

经过这样的操作之后,a对象下就有了value这个key,他被赋予不可继续赋值,不可继续配置,不能被枚举,值为static,我们可以通过a.value拿到这里的static,但是不能继续通过a.value = 'relative'来继续赋值。

同样的,设置存取描述符也是四个属性:

var a = { i: 1 };
Object.defineProperty(a, 'value', {
  enumerable: false,
  configurable: false,
  get() {
    return a.i;
  },
  set() {
    a.i++;
  },
});
1
2
3
4
5
6
7
8
9
10
11

这里设置时就没有配置writablevalue属性,转而配置了getset方法,在这两种配置中,getset方法和writablevalue是不能共存的,否则就会抛出异常。类似上面这样的设置,当我们访问a.value时就会调用get方法,当我们通过a.value = 'test'时,就会执行set方法。

var i = 1;
Object.defineProperty(window, 'a', {
  get() { return i++; },
});

if (a === 1 && a === 2 && a === 3) {
  console.log('Hello World!');
}
1
2
3
4
5
6
7
8

同时,这种劫持gettersetter的方法本质上是执行了一个函数,内部除了用自增变量,还可以有更多的方法:

const value = (function* () {
  let i = 1;
  while (true) yield i++;
}());

Object.defineProperty(window, 'a', {
  get() {
    return value.next().value;
  },
});

if (a === 1 && a === 2 && a === 3) {
  console.log('Hello World!');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

对于严格相等的情况,一般来说只能通过劫持数据的getter来进行操作,但是里面具体操作的方法在上面列举的就有很多。

对于宽松相等的情况,除了劫持getter以外,因为宽松相等JS引擎的缘故,还能用ObjectProxy对象的valueOftoString方法达到目的。

# 隐式转换

常见的很简单的问题:

'1' == 1; // true
1 == '1'; // true
0 == false; // true
1
2
3

但是:

![] == []; // true
1

# '=='作用规则

a == b,如果a、b类型相同,那很简单,值相同即为true,不同即为false。所以这里只讨论a、b类型不同的情况——虽然应该避免不同类型变量相比较,但是弄明白"比较中的隐式类型转换"却非常必要。

参照MDN文档梳理了一下不同类型的的值比较的规则:

  1. 当数字与字符串进行比较时,会尝试将字符串转换为数字值;
  2. 如果操作数之一是boolean,则将布尔操作数转换为1或0;
  3. 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的valueOf()toString()方法将对象转换为数字或字符串;
  4. null == undefinedtrue,此外通常情况下nullundefined与其它对象都不相等。

可以看到,前三条规则中,都是试图转变为字符串和数字进行比较,在比较中,可以把布尔值当成数字。回到刚才的问题,![] == []就比较容易理解了,相当于false == [],有boolean操作数,先转为数字,相当于比较0 == [],而[]转为数字是0,所以返回true

TIPS

[] == ![]结果为true过程分解:

  1. !取反运算符根据规则得到[] == false
  2. 根据比较规则第2条[] == false等同于[] == 0
  3. 根据比较规则的第3条[] == 0尝试调用数组对象的[].valueOf()返回[][].toString()返回''最终得到'' == 0
  4. 根据比较规则的第1条'' == 0 尝试把空字符转为数字得到0 == 0最终结果就是为true

# 对象的valueOf和toString

Object对象在隐式转换的时候,会尝试调用valueOftoString函数,向字符串或者数字转换。那优先会采用哪一个函数的值呢?

结论:

  1. 如果valueOf或者toString返回原始值,按valueOf大于toString的优先级取得返回值;
  2. 若返回值是null或者undefined,比较返回false
  3. 否则根据另一个比较值转为字符串或者数字进行比较;
  4. 如果valueOftoString均不返回原始值,则比较操作将会报错!
const a = {};
a.valueOf = () => 1;
a.toString = () => 2;
console.log(a == 1, a == 2); // true, false

const b = {};
b.valueOf = () => null; // 优先级高于toString,比较直接返回false
b.toString = () => '1';
console.log(b == 'null', b == 1, b == '1', b == 0); // false, false, false, false

const c = {};
c.valueOf = () => ([]); // 返回非基本值,将尝试取toString比较
c.toString = () => '1';
console.log(c == 'undefined', c == '1'); // false, true

const d = {};
d.valueOf = () => ([]); // 返回非基本值
d.toString = () => ([]);
console.log(d == 'undefined', d == '1'); // Cannot convert object to primitive value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

很明显,根据valueOf大于toString的优先级可以看到,objA == 'abc'的比较并不同于简单地将objA显式转换为字符串进行比较,即:objA == 'abc'String(objeA) == 'abc'结果并不一定相同(显式转换直接走toString):

const e = {};
e.valueOf = () => 1;
e.toString = () => '2';
// true, true, false, true
console.log(e == 1, e == '1', String(e) == '1', String(e) == '2');
1
2
3
4
5

总结:

  1. 在进行强转字符串类型时将优先调用toString方法,强转为数字时优先调用valueOf

  2. 在有运算操作符的情况下,valueOf的优先级高于toString

    这两个方法一般是交由js去隐式调用,以满足不同的运算情况。

    • 在数值运算里,会优先调用valueOf,如a + b
    • 在字符串运算里,会优先调用toString,如alert(c)

# Symbol.toPrimitive属性

除了valueOftoString函数外,ES6规范提出了Symbol.toPrimitive作为对象属性名,其值是一个函数,函数定义了对象转为字符串、数字等原始值的返回值,其优先级要高于valueOftoString

Symbol.toPrimitive是一个内置的Symbol值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。该函数被调用时,会被传递一个字符串参数hint,表示要转换到的原始值的预期类型。hint参数的取值是numberstringdefault中的任意一个。对于==操作符,hint传递的值是default

const a = {};
a[Symbol.toPrimitive] = (hint) => {
  if (hint == 'number') return 1;
  if (hint == 'string') return 2;
  return 3;
};
a.valueOf = () => 4;
a.toString = () => 5;
// false, false, true, false, false
console.log(a == 1, a == 2, a == 3, a == 4, a == 5);
1
2
3
4
5
6
7
8
9
10

如果使用Number或者String强制转换a,则传入的hint会是number或者string

console.log(Number(a)); // 1
console.log(String(a)); // 2
1
2

实现以下效果:

console.log(add[1][2] + 100); // 103
1
const proxy = (value = 0) => new Proxy({}, {
  get(target, prop) {
    console.log(target, prop, value);
    // 加法时获取原始值
    if (prop === Symbol.toPrimitive) {
      return () => value;
    }
    return proxy(value + +prop);
  },
});
const add = proxy();
1
2
3
4
5
6
7
8
9
10
11
最近更新: 2025年02月20日 11:55:57