诗号:六道同坠,魔劫万千,引渡如来。

==, === 运算符详解。

ECMAScript® 2022 Language Specification - Equality Comparison

一般推荐使用相等比较的时候使用 === 是为了避免类型强转带来未知问题,但有时候后 端返回的数据结构如果不规范就经常会出现恒等不合理情况,这篇文章会从 ECMA 标准的实 现步骤,通过伪码形式来实现和展示相等和恒等的原理。

在开始之前,要做一些准备工作。。。

严格相等 ===

1
2
3
4
function strictEqual(x, y) {
  // TODO
  return x === y;
}

将字符串转成 BigInt 类型:

标准里的描述: 7.1.14 StringToBigInt ( argument ) Apply the algorithm in 7.1.4.1 with the following changes:

Replace the StrUnsignedDecimalLiteral production with DecimalDigits to not allow Infinity, decimal points, or exponents. If the MV is NaN, return NaN, otherwise return the BigInt which exactly corresponds to the MV, rather than rounding to a Number.

实现: 通过测试,只要字符串里包含非数字的符号就会报错,这里提前拦截模拟报错,实 际最后还是通过 BigInt() 来进行转换,而对于非字符串类型最终会转成字符串之后再 处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function StringToBigInt(v) {
  if (typeof v !== "string") v = "" + v;
  if (isNaN(v)) return NaN;
  if (/[^\d]/g.test(v)) {
    // 包含非数字的都不合法报错
    throw new SyntaxError(`不能将 ${v} 转成 BigInt。`);
  }
  return MyBigInt(v); // 这里直接使用 BigInt
}

function MyBigInt(v) {
  if (this instanceof MyBigInt) {
    throw new TypeError(" 不支持 new 操作。");
  }
  let prim = toPrimitive(v, "number");
  if (typeof prim === 'number') {
    return NumberToBigInt(prim);
  }
  return BigInt(v);
}

function NumberToBigInt(v) {
  return BigInt(v);
}
function toPrimitive(v) {
  return +v
}
// 测试:
let val = StringToBigInt("100")
console.log(val);
try {
  val = StringToBigInt('100.00')
} catch(e) {
  console.log(e.message)
}
100n
不能将 100.00 转成 BigInt。
undefined

toPrimitive 将对象转成原始类型,标准的实现有点复杂,主要原理还是实现类型的 Symbol.toPrimitive

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//  简化版:将 x 转成原始类型
function toPrimitive(x, ref) {
  if (!isRef(x)) return x;
  if (isString(ref)) {
    return "" + x;
  } else if (isNum(ref)) {
    return +x;
  } else if (isBigInt(ref)) {
    if (isNum(x)) {
      throw new TypeError("BigInt 不能转成 number");
    } else if (isString(x)) {
      return "" + x;
    }
  } else if (isSymbol(ref)) {
    return Symbol(x);
  }
}
// Symbol.toPrimitive
// 实现如: obj = { [Symbol.toPrimitive](hint) { ... } }
// 这里涉及到各种对象类型转成原始类型的,涉及内容太多这里就不展开去实现了
function _ToPrimitive(input, preferredType) {
  let hint;
  if (isRef(input)) {
    let exoticToPrim = GetMethod(input, "@@toPrimitive");
    if (exoticToPrim !== undefined) {
      if (!preferredType) {
        hint = "default";
      } else if (preferredType === "string") {
        hint = "string";
      } else {
        hint = "number";
      }

      let result = exoticToPrim(input, hint);
      if (typeof result !== "object") {
        return result;
      } else {
        throw TypeError("类型不能转成原始类型");
      }
    } else {
      if (!preferredType) {
        preferredType = "number";
      }
    }
    return OrdinaryToPrimitive(input, preferredType);
  }
  return input;
}

比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === "number") {
      return 100;
    } else if (hint === "string") {
      return "foo";
    }
    return null;
  },
};
console.log(Number(obj));
console.log(+obj);
console.log(String(obj));
100
100
foo
undefined

转成数字:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 转成数字
function ToNumber(v) {
  if (v === undefined) {
    return NaN;
  } else if (v === null) {
    return +0;
  } else if (v === true) {
    return 1;
  } else if (v === false) {
    return +0;
  } else if (typeof v === "number") {
    return v;
  } else if (typeof v === "string") {
    return v; // TODO
  } else if (Array.isArray(v)) {
    return Number(v.toString());
  } else if (typeof v === "symbol") {
    throw new TypeError("Symbol 不能转成 number.");
  } else if (typeof v === "bigint") {
    throw Number(v);
  } else if (typeof v === "object") {
    return Number(v);
  }
  return v;
}

function tryCatch(fn) {
  let val;
  try {
    val = fn();
  } catch (e) {
    console.log(e.message);
  }
  return val;
}

// 测试
console.log("null: " + ToNumber(null));
console.log("unefined: " + ToNumber(undefined));
console.log("{}: " + ToNumber({}));
console.log("[]: " + ToNumber([]));
console.log("Symbol('xx'): " + tryCatch(() => ToNumber(Symbol("xx"))));
console.log("BigInt(100): " + tryCatch(() => ToNumber(BigInt(100))));

转换规则:

类型结果-
null0
undefinedNaN先转成 "undefined"
[]0String([]) -> ''
{}NaNString({}) -> [object Object]
"xx"NaN-
100n100BigInt 可以转成 Number
SymbolTypeError不能转
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 非严格相等, == 的实现
function equal(x, y) {
  // 类型一样就直接返回 x === y 的结果
  if (Type(x) === Type(y)) {
    return strictEqual(x, y);
  }

  // 1. 即 null == undefined => true
  if ((x === null && y === undefined) || (x === undefined && y === null)) {
    return true;
  }

  // 2. 内部属性无法直接获取,这里到时候使用对象普通属性模拟
  if (isObject(x) && hasOwn(x, "[[IsHTMLDDA]]")) {
    if (y === null || y === undefined) {
      return true;
    }
  }

  if (isObject(y) && hasOwn(y, "[[IsHTMLDDA]]")) {
    if (x === null || x === undefined) {
      return true;
    }
  }

  // 3. 如果其中有一个是字符串,将字符串转成数字之后进行比较
  if (isNum(x) && isString(y)) {
    return strictEqual(x, ToNumber(y));
  }

  if (isString(x) && isNum(y)) {
    return strictEqual(ToNumber(x), y);
  }

  // 4. BigInt 类型和字符串比较,将字符串转成 BigInt 类型
  if (isBigInt(x) && isString(y)) {
    let n = StringToBigInt(y);
    if (isNaN(n)) {
      return false;
    }
    return equal(x, n);
  }

  if (isString(x) && isBigInt(y)) {
    return equal(y, x);
  }

  // 5. 如果是布尔类型转成数字再进行比较
  if (isBool(x)) {
    return equal(ToNumber(x), y);
  }
  if (isBool(y)) {
    return equal(x, ToNumber(y));
  }

  // 6. 如果其中有一个是引用类型,将其转成原始类型再比较
  if (isRef(y) && (isString(x) || isNum(x) || isBigInt(x) || isSymbol(x))) {
    return equal(x, ToPrimitive(y));
  }

  if (isRef(x) && (isString(y) || isNum(y) || isBigInt(y) || isSymbol(y))) {
    return equal(ToPrimitive(x), y);
  }

  // 7. BigInt 和 Number 类型
  if (isBigInt(x) && isNum(y)) {
    if (isNaN(x) || strictEqual(x, +Infinity) || strictEqual(x === -Infinity)) {
      return false;
    }
    if (isNaN(y) || strictEqual(y, +Infinity) || strictEqual(y === -Infinity)) {
      return false;
    }

    return equal(R(x), R(y));
  }

  return false;
}

这里主要有 8 种情况(equal(x, y)):

  1. 如果 x, y 类型一样,直接返回 x === y 结果,不需要进行类型转换

  2. 如果 null == undefinedundefined == null 进行比较,直接返回 true

  3. 如果 x 是对象且有 [[IsHTMLDDA]] 内部属性,且 y 是 null 或 undefined 直接返 回 true, 反之亦然。

  4. 如果 x number, y string 则将其中字符串转成 number 再进行 x === y 比较,反之 亦然。

  5. 如果 x BigInt, y string 则将其中字符串转成 BigInt 再进行 x === y 比较,反之 亦然。

  6. 如果 x boolean 将 x 转成 number 再 equal(number(x), y) 重新比较,反之亦然。

  7. 如果 x 是引用类型(即 typeof x === 'object'),而 y 是普通类型(String, Number, BigInt, Symbol),那么将 x 转成原始类型再比较,即 equal(toPrimitive(x), y), 反之亦然

  8. 如果 x BigInt, y number 又区分几种情况

    • 如果 x: NaN 返回 NaN

    • 如果 x: +Infinity 返回 false

    • 如果 x: -Infinity 返回 false

    • 否则返回 equal(R(x), R(y)) R 不太清楚啥意思?

测试:可以通过表格下面的输入框输入左右值点击提交会更新表格,有两个结果,一个是 equal(x,y) 是该文根据 ECMA 标准实现的相等比较, x==y 是直接使用 == 符号得 到的结果为了形成对比,最后一列信息是每一行的数据在执行 equal(x,y) 过程中标识了 哪个值进行了类型转换(/js/tests/web/equal.js), 表格采用 Vue + ElementPlus 生成, 源码文件: /js/tests/X6j10iPkmj.js

最终总结(类型比较):

类型1类型2需要强转类型
numberstringstring -> number
nullundefined直接返回 true
bigintstringstring -> bigint
booleannumberboolean -> number, false-0, true-1
object普通类型object -> 普通类型

根据上表的类型转换,可以轻松判断出一些诡异的现象,比如:

0 == [0] : [0] 先转字符串即 "0" 然后再转成数字 0 结果: true

10 == ["10"] => ["10"] => "10" => 10

false == 0, true == 1 结果都是 true 都是因为 boolean 转成了 0 或 1