BFE.dev solution for JavaScript Quiz
10. Equal

TL;DR

For == it loosly checks equality with rules including:

  1. null and undefined are equal
  2. for string and number, string is converted to number first
  3. for bigint and string, string is converted to bigint first
  4. if either is boolean, boolean is converted to number
  5. if either is primitive and the other is Object, convert the Object to primitive first

Above is not a full list, please refer to the detailed spec explanation in the section after this.

console.log(0 == false)
// true
// booleans are converted to number first, 0 == 0
console.log('' == false)
// true
// booleans are converted to number first, '' == 0
// string vs number, string is converted to number. 0 == 0
console.log([] == false)
// true
// booleans are converted to number first, [] == 0
// 0 is primitive, so object(the array) is converted to primitive
// there is no preferred type here and [] is converted to '', '' == 0
// see details at https://bfe.dev/quiz/Implicit-Conversion-II/solution
// again, '' is converted to 0, 0 == 0
console.log(undefined == false)
// false
// booleans are converted to number first, undefined == 0
// no cases is defined for `undefined` vs `number`, so false
console.log(null == false)
// false, same as above
console.log('1' == true)
// true
// booleans are converted to number first, '1' == 1
// string vs number, string is converted to number. 1 == 0
console.log(1n == true)
// true
// booleans are converted to number first, 1n == 1
// bigint vs number, they are equal in value, so true
console.log(' 1     ' == true)
// true
// booleans are converted to number first, ' 1     ' == 1
// string vs number, string is converted to number. 1 == 1

ECMAScript Spec about loosely equal ==

From ECMAScript spec, loosely equal check works as below.

  1. If Type(x) is the same as Type(y), then
    1. Return IsStrictlyEqual(x, y).
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. NOTE: This step is replaced in section B.3.6.2.
  5. If x is a Number and y is a String, return ! IsLooselyEqual(x, ! ToNumber(y)).
  6. If x is a String and y is a Number, return ! IsLooselyEqual(! ToNumber(x), y).
  7. If x is a BigInt and y is a String, then
    1. Let n be StringToBigInt(y).
    2. If n is undefined, return false.
    3. Return ! IsLooselyEqual(x, n).
  8. If x is a String and y is a BigInt, return ! IsLooselyEqual(y, x).
  9. If x is a Boolean, return ! IsLooselyEqual(! ToNumber(x), y).
  10. If y is a Boolean, return ! IsLooselyEqual(x, ! ToNumber(y)).
  11. If x is either a String, a Number, a BigInt, or a Symbol and y is an Object, return ! IsLooselyEqual(x, ? ToPrimitive(y)).
  12. If x is an Object and y is either a String, a Number, a BigInt, or a Symbol, return ! IsLooselyEqual(? ToPrimitive(x), y).
  13. If x is a BigInt and y is a Number, or if x is a Number and y is a BigInt, then
    1. If x is not finite or y is not finite, return false.
    2. If ℝ(x) = ℝ(y), return true; otherwise return false.
  14. Return false.

The steps are pretty straightforward it explains itself.

One thing is about ToPrimitive() though, let's continue.

ToPrimitive() in ECMAScript Spec

This defines how we get primitive values from any data types, according to the ECMAScript spec:

  1. If input is an Object, then
  2. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
  3. If exoticToPrim is not undefined, then
    1. If preferredType is not present, let hint be "default".
    2. Else if preferredType is string, let hint be "string".
    3. Else,
      1. Assert: preferredType is number.
      2. Let hint be "number".
    4. Let result be ? Call(exoticToPrim, input, « hint »).
    5. If result is not an Object, return result.
    6. Throw a TypeError exception.
  4. If preferredType is not present, let preferredType be number.
  5. Return ? OrdinaryToPrimitive(input, preferredType).
  6. Return input.

Here it says, if Symbol.toPrimitive is defined in the object, then the method will be used in ToPrimitive(). This is pretty obvious, below is an example.

const obj =
  {
    [Symbol.toPrimitive](hint) {
      if (hint === 'number') {
        return 100
      }
      return 'obj'
    }
  } + obj // 100

If Symbol.toPrimitive is not defined, the default OrdinaryToPrimitive() is used.

OrdinaryToPrimitive() in ECMAScript spec

This is the default process before Symbol.toPrimitive is a thing. According to the ECMAScript spec:

  1. If hint is string, then
  2. Let methodNames be « "toString", "valueOf" ».
  3. Else,
  4. Let methodNames be « "valueOf", "toString" ».
  5. For each element name of methodNames, do
  6. Let method be ? Get(O, name).
  7. If IsCallable(method) is true, then
  8. Let result be ? Call(method, O).
  9. If result is not an Object, return result.
  10. Throw a TypeError exception.

What it says is basically

  1. if needs a string, first toString() then valueOf() is tried and return the first primitive value;
  2. if needs a number, first valueOf() then toString() is tried and return the first primitive value;

For empty array [] in our question

  1. there is no Symbol.toPrimitive defined in Array, continue.
  2. Array inherits Object.prototype.valueOf() is tried, but it just returns the array itself, not a primitive value, continue
  3. Different from Object.prototype.toString(), Array has its own Array.prototype.toString()
    1. we can see from the spec that Array.prototype.join() is used here.
    2. in Array.prototype.join(), it tries to convert all elements to string, which recursively calls Array.prototype.toString().
You might also be able to find a solution fromcommunity posts or fromAI solution.