BFE.dev solution for JavaScript Quiz
63. in

TL;DR

in operator checks if a property name is present in the target object(including its prototype chain), if the property name is not Symbol nor string, it is coerced to string.

['foo'] is coerced to 'foo', so the result of both are true.

const obj = {
  foo: 'bar'
}

console.log('foo' in obj) // true
console.log(['foo'] in obj) // true

Below is longer version explaining the true logic behind this.

in operator in ECMAScript spec

According to the spec, below is how in works.

  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. If rval is not an Object, throw a TypeError exception.
  6. Return ? HasProperty(rval, ? ToPropertyKey(lval)).

We have 2 steps to run the check

  1. ToPropertyKey - convert the propert name to a valid property key
  2. HasProperty - check if the property exists

ToPropertyKey

This is straightforward from ECMAScript spec.

  1. Let key be ? ToPrimitive(argument, string).
  2. If key is a Symbol, then
    1. Return key.
  3. Return ! ToString(key).

The property name is converted to primitives. If it is Symbol, use it, otherwise convert it to string.

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;

ToString

The ECMAScript spec is simply a mapping table.

Argument Type Result
Undefined Return "undefined".
Null Return "null".
Boolean If argument is true, return "true". If argument is false, return "false".
Number Return Number::toString(argument, 10).
String Return argument.
Symbol Throw a TypeError exception.
BigInt Return BigInt::toString(argument, 10).
Object Apply the following steps: 1. Let primValue be ? ToPrimitive(argument, string). 2. Return ? ToString(primValue).

HasProperty

HasProperty is fairly simple

The abstract operation HasProperty takes arguments O (an Object) and P (a property key) and returns either a normal completion containing a Boolean or a throw completion. It is used to determine whether an object has a property with the specified property key. The property may be either own or inherited. It performs the following steps when called: Return ? O.[[HasProperty]](P).

So it is native implementation and it checks the prototype chain.

Step-by-step analysis.

const obj = {
  foo: 'bar'
}

console.log('foo' in obj) // true

This is super easy, 'foo' is string and in the object, so true.

console.log(['foo'] in obj) // true

Internally following steps are how ['foo'] is converted to string

  1. there is no Symbol.toPrimitive defined in Array, continue.
  2. since we expect string, toString() is tried before valueOf(), 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().

Thus ['foo'] is converted to 'foo' and 'foo' in object is true.

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