Try explain the JavaScript’s truth inconsistency

Everyone has an own version of a truth. I have the own truth, you have your own. JavaScript has many truths and it’s probably no problem until you’ll start to use it implicitly. This article shows and explains one of the popular JavaScript’s trap – inconsistent evaluation in If Statement and Equality Comparison.

Problem

We could start by running simple snippet:

if ([]) {
    console.log("Condition true, check again and is", [] == true)
}

Have you tried? Probably not just once. It’s not a joke from ECMAScript developers, just mechanisms which are used to resolving final value from Expression. In this article, I’ll explain this confusing behavior.

green stop sign

Evaluate an empty array inside If Statement

Take a look at official ECMAScript Language Specification at the 13.6.7 section – The If Statement – Runtime Semantics: Evaluation. A whole document contains a clear description of how all things works in ECMAScript. Right now, let’s focus on this specific behavior.

Our If Statement contains empty array

if ([]) { }

We would consider first two steps from the official specification. The document says:

  1. Let exprRef be the result of evaluating Expression.
  2. Let exprValue be ToBoolean(GetValue(exprRef)).

Our exprRef should be a result of evaluating Expression. However, we don’t need to evaluate anything, because we supplied an object (empty array) in place of Expression.

Subsequently, we need to obtain exprValue which is a result of the composition of two functions GetValue and ToBoolean. I don’t want to dig into the first, just assume, that the result of GetValue(exprRef) is simply an empty array.

Under the hood, the GetValue function resolve path to the value based on the reference. In this example given Expression has concrete Type (not Reference) and there is no need to resolve it.

The last thing before we got the exprValue is called a ToBoolean function with empty array passed through. This function works as a simple switch statement – It returns a boolean value based on matching argument.

Above image presents the ToBoolean results based on the supplied argument. Have you noticed, that there is no Array type in this table? It is nothing wrong with it because what we know as an Array, in JavaScript is an Object.

[] instanceof Object;   // true
typeof [];              // "object"

Based on above table, the expected result for Object is True, so that is our exprValue.

if ([]) { } // => if (true) { }

Equality Comparison with empty array on one side

The next confusing element in this example is the console.log statement containing equality comparison

// Initial state

console.log([] == true);  // false
console.log([] == false); // true

It looks logically, but how these expressions were evaluated? The algorithm is described in section 7.2.6 Abstract Equality Comparison in ECMAScript Specification:

  1. ReturnIfAbrupt(x).
  2. ReturnIfAbrupt(y).
  3. If Type(x) is the same as Type(y), then, return the result of performing Strict Equality Comparison x === y.
  4. If x is null and y is undefined, return true.
  5. If x is undefined and y is null, return true.
  6. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ToNumber(y).
  7. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.
  8. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
  9. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
  10. If Type(x) is either String, Number, or Symbol and Type(y) is Object, then return the result of the comparison x == ToPrimitive(y).
  11. If Type(x) is Object and Type(y) is either String, Number, or Symbol, then return the result of the comparison ToPrimitive(x) == y.
  12. Return false.

In our expression, we have boolean and object value. First matched condition is no 9 which converts a boolean value to Number. According to ToNumber function, for boolean it returns 1 when the argument is true, otherwise +0. Returned value is used in place of the old one, so actually, the expression looks like

// Evaluate right side of comparison 
 
console.log([] == 1);
console.log([] == 0);

ToPrimitive evaluation

Now, the conditions match to the second marked rule. It’s not possible to quickly compare both of these values because there is a comparison between an object and a primitive value. Therefore we should convert this object to the primitive using ToPrimitive function.

ToPrimitive algorithm for object parameter looks very complex. The second parameter of this function is PreferredType, which is used to set a correct hint variable. Unfortunately, there is an optional argument and it’s no provided in this function call. Instead, the algorithm use the “default” value, it performs some checks, and later on, it assigns “number” as a hint and passed it together with the object to the OrdinaryToPrimitive function. Its algorithm looks very interesting.

  1. Assert: Type(O) is Object
  2. Assert: Type(hint) is String and its value is either "string" or "number".
  3. If hint is "string", then
    1. Let methodNames be «"toString", "valueOf"».
  4. Else,
    1. Let methodNames be «"valueOf", "toString"».
  5. For each name in methodNames in List order, do
    1. Let method be Get(O, name).
    2. ReturnIfAbrupt(method).
    3. If IsCallable(method) is true, then
      1. Let result be Call(method, O).
      2. ReturnIfAbrupt(result).
      3. If Type(result) is not Object, return result.
  6. Throw a TypeError exception.

First, we have to construct a methodNames list – for “number” it would contain ["valueOf", "toString"] methods. To get the final value we should execute these methods in order until the result will be a primitive value. Function valueOf for array returns an array itself, so the toString must be called. Finally, our conditions will be executed similarly to these

// Process of evaluation the left side

console.log([].valueOf().toString() == 1); // == console.log([] == 1);
console.log([].valueOf().toString() == 0); // == console.log([] == 0);

Or simpler

// Evaluated left side of comparison

console.log("" == 1);
console.log("" == 0);

Right, it’s not finished yet but it looks more familiar and more logically. According to the Abstract Equality Comparison Algorithm:

  1. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.

A full description of converting the string to Number is in section ToNumber Applied to the String Type. In this case, an empty string is converted to the 0 value. That’s it.

// Final state

console.log(0 == 1);
console.log(0 == 0);

Conclusions

The problem with an inconsistent truth in JavaScript results from the process of conversion values. It’s not a simple casting from one type to another, rather a complex selection algorithm working behind the scene. So maybe next time when you’ll be in trouble, try to understand how specific things work. The 5 minutes spent on the investigation will be a better investment than another couple of nasty adjectives that do not give anything.

And remember this simple trick:

if ([] === true) {
    console.log("Try to guess what I mean, or...")
} else {
    console.log("Say precisely what you expect :)");
}

Resources

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.