BFE.dev solution for JavaScript Quiz
9. null and undefined

TL;DR

console.log(JSON.stringify([1, 2, null, 3]))
// "[1,2,null,3]" as it is
console.log(JSON.stringify([1, 2, undefined, 3]))
// "[1,2,null,3]"
// undefined in array is represented as "null",
console.log(null === undefined)
// false, null and undefined are different
console.log(null == undefined)
// true, this is by design, null and undefined are loosely equal
console.log(null == 0)
// false. For `==`, null is only loosely equal to undefined, no other values.
console.log(null < 0)
// false.  since both of them are not string, null is converted to number, which is 0. 
// 0 < 0 is false
console.log(null > 0)
// false. since both of them are not string, null is converted to number, which is 0. 
// 0 > 0 is false
console.log(null <= 0)
// true. `a <= b` is internally done by revsersing the result of `b < a`, 
// in turn it is `a < b`, since `0 <= null` is false. the result is true
console.log(null >= 0)
// true. `a >= b` is internally done by revsersing the result of `a < b`, 
// since `0 <= null` is false. the result is true
console.log(undefined == 0)
// false. For `==`, null is only loosely equal to undefined, no other values.
console.log(undefined < 0)
// false. since both of them are not string, undefined is converted to number, which is NaN.
// NaN < 0 is false
console.log(undefined > 0)
// false. since both of them are not string, undefined is converted to number, which is NaN. 
// NaN > 0 is false
console.log(undefined <= 0)
// false. since both of them are not string, undefined is converted to number, which is NaN. 
// But internally NaN in `<` comparison results in `undefined`, which is diffrent from null case. 
// `undefined` is reversed in to `true` (Yeah I know it's weird)
console.log(undefined >= 0)
// false. since both of them are not string, undefined is converted to number, which is NaN. 
// But internally NaN in `<` comparison results in `undefined`, which is diffrent from null case. 
// `undefined` is reversed in to `true` (Yeah I know it's weird)

These quizzes are quite straightforward in ECMAScript spec. Let's dive into it.

JSON.stringify

In ECMAScript spec, we can know see that

Symbolic primitive values are rendered as follows:

  • The null value is rendered in JSON text as the String value "null".
  • The undefined value is not rendered.
  • The true value is rendered in JSON text as the String value "true".
  • The false value is rendered in JSON text as the String value "false".

Also it says

Values that do not have a JSON representation (such as undefined and functions) do not produce a String. Instead they produce the undefined value. In arrays these values are represented as the String value "null". In objects an unrepresentable value causes the property to be excluded from stringification.

This explains why undefined becomes null in JSON.stringfiy.

IsStrictlyEqual ( x, y ), ===

According to ECMAScript spec Strict equal first checks types. null and undefined are not the same types, so false.

IsLooselyEqual ( x, y ), ==

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.

We can see that

  1. null and undefined are handled specially if there are compared, in step 2 and 3
  2. null and undefined cannot be compared to other values in equality check.

Relational Operators <, >

Here are the definitions of relational operators in ECMAScript spec

RelationalExpression : RelationalExpression < ShiftExpression

  1. Let lref be ? Evaluation of RelationalExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be ? Evaluation of ShiftExpression.
  4. Let rval be ? GetValue(rref).
  5. Let r be ? IsLessThan(lval, rval, true).
  6. If r is undefined, return false. Otherwise, return r.

RelationalExpression : RelationalExpression > ShiftExpression

  1. Let lref be ? Evaluation of RelationalExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be ? Evaluation of ShiftExpression.
  4. Let rval be ? GetValue(rref).
  5. Let r be ? IsLessThan(rval, lval, false).
  6. If r is undefined, return false. Otherwise, return r.

The core logic for both lies in IsLessThan() is a bit long.

IsLessThan ( x, y, LeftFirst )

  1. If the LeftFirst flag is true, then
    1. Let px be ? ToPrimitive(x, number).
    2. Let py be ? ToPrimitive(y, number).
  2. Else,
    1. NOTE: The order of evaluation needs to be reversed to preserve left to right evaluation.
    2. Let py be ? ToPrimitive(y, number).
    3. Let px be ? ToPrimitive(x, number).
  3. If px is a String and py is a String, then
    1. Let lx be the length of px.
    2. Let ly be the length of py.
    3. For each integer i such that 0 ≤ i < min(lx, ly), in ascending order, do
      1. Let cx be the integer that is the numeric value of the code unit at index i within px.
      2. Let cy be the integer that is the numeric value of the code unit at index i within py.
      3. If cx < cy, return true.
      4. If cx > cy, return false.
    4. If lx < ly, return true. Otherwise, return false.
  4. Else,
    1. If px is a BigInt and py is a String, then
      1. Let ny be StringToBigInt(py).
      2. If ny is undefined, return undefined.
      3. Return BigInt::lessThan(px, ny).
    2. If px is a String and py is a BigInt, then
      1. Let nx be StringToBigInt(px).
      2. If nx is undefined, return undefined.
      3. Return BigInt::lessThan(nx, py).
    3. NOTE: Because px and py are primitive values, evaluation order is not important.
    4. Let nx be ? ToNumeric(px).
    5. Let ny be ? ToNumeric(py).
    6. If Type(nx) is the same as Type(ny), then
      1. If nx is a Number, then
        1. Return Number::lessThan(nx, ny).
      2. Else,
        1. Assert: nx is a BigInt.
        2. Return BigInt::lessThan(nx, ny).
    7. Assert: nx is a BigInt and ny is a Number, or nx is a Number and ny is a BigInt.
    8. If nx or ny is NaN, return undefined.
    9. If nx is -∞𝔽 or ny is +∞𝔽, return true.
    10. If nx is +∞𝔽 or ny is -∞𝔽, return false.
    11. If ℝ(nx) < ℝ(ny), return true; otherwise return false.

Notice that both values are converted to numeric values in step 4.4 and 4.5, where null and undefined has different numeric values returned.

Also pay attention that in step 8, undefined is returned if either operand is NaN.

ToNumber ( argument )

  1. If argument is a Number, return argument.
  2. If argument is either a Symbol or a BigInt, throw a TypeError exception.
  3. If argument is undefined, return NaN.
  4. If argument is either null or false, return +0𝔽.
  5. If argument is true, return 1𝔽.
  6. If argument is a String, return StringToNumber(argument).
  7. Assert: argument is an Object.
  8. Let primValue be ? ToPrimitive(argument, number).
  9. Assert: primValue is not an Object.
  10. Return ? ToNumber(primValue).

In step 3 and 4, we can clearly see that undefined is NaN and null is 0.

This explains why null > 0, null < 0, undefined < 0, undefined > 0 are all false, but with different reasons.

Relational Operators <=, >=

Here are the definitions of relational operators in ECMAScript spec

RelationalExpression : RelationalExpression <= ShiftExpression

  1. Let lref be ? Evaluation of RelationalExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be ? Evaluation of ShiftExpression.
  4. Let rval be ? GetValue(rref).
  5. Let r be ? IsLessThan(rval, lval, false).
  6. If r is true or undefined, return false. Otherwise, return true.

RelationalExpression : RelationalExpression >= ShiftExpression

  1. Let lref be ? Evaluation of RelationalExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be ? Evaluation of ShiftExpression.
  4. Let rval be ? GetValue(rref).
  5. Let r be ? IsLessThan(lval, rval, true).
  6. If r is true or undefined, return false. Otherwise, return true.

Tricky part here is that we can see result of >= and <= are derived by reversing the result of < and >.

So null >= 0 and null <= 0 are true.

But for undefined since IsLessThan returns undefined because numeric value of undefined is NaN, step 6 treats undefined as true, so the result is false.

Thus undefined >= 0 and undefined <=0 are both false.

You might also be able to find a solution fromcommunity posts or fromAI solution.