You'll often hear people say "everything is an object in JavaScript". This simply isn't true! We touched on this last week when we learned why the .at() method exists.

Primitive values

Primitive values are not objects. They have no properties or methods. At the time of writing, there are seven types of primitive value in JavaScript:

Everything else is a type of object. This includes arrays; they aren't primitive values! The MDN Web Docs explain it well:

Arrays are regular objects for which there is a particular relationship between integer-keyed properties and the length property.

Additionally, arrays inherit from Array.prototype, which provides to them a handful of convenient methods to manipulate arrays. For example, indexOf() (searching a value in the array) or push() (adding an element to the array), and so on. This makes Arrays a perfect candidate to represent lists or sets.

Wrapper objects

You might reasonably ask: how come I can call methods on primitive values, such as String.prototype.trim()? Ah, but you can't! Not directly. Let's revisit the snippet from the MDN Web Docs that I shared last week:

Primitives have no methods but still behave as if they do. When properties are accessed on primitives, JavaScript auto-boxes the value into a wrapper object and accesses the property on that object instead. For example, "foo".includes("f") implicitly creates a String wrapper object and calls String.prototype.includes() on that object. This auto-boxing behavior is not observable in JavaScript code but is a good mental model of various behaviors — for example, why "mutating" primitives does not work (because str.foo = 1 is not assigning to the property foo of str itself, but to an ephemeral wrapper object).

Let's see some examples of what might be happening when you try to access a property or invoke a method on a primitive value. I say "might" because, as the snippet above says, the auto-boxing behavior is not observable in JavaScript code. But we can do it explicitly to give ourselves an idea of what's happening. We'll do this using the Object() constructor. Note that you should almost never do this in reality; we're only doing it here for learning purposes.

String objects

// Auto-boxing
let food = "pizza";
food.length; // 5

// Explicit boxing
let food = "pizza";
Object(food).length; // 5

Number objects

// Auto-boxing
let numCards = 52;
numCards.toFixed(2); // "52.00"

// Explicit boxing
let numCards = 52;
Object(numCards).toFixed(2); // "52.00"

Boolean objects

// Auto-boxing
let georgeBoole = true;
georgeBoole.toString(); // "true"

// Explicit boxing
let georgeBoole = true;
Object(georgeBoole).toString(); // "true"

Symbol objects

// Auto-boxing
let hieroglyph = Symbol("hieroglyph");
hieroglyph.description; // "hieroglyph"

// Explicit boxing
let hieroglyph = Symbol("hieroglyph");
Object(hieroglyph).description; // "hieroglyph"

BigInt objects

// Auto-boxing
let veryBigInt = 18014398509481982n;
veryBigInt.toString(); // "18014398509481982"

// Explicit boxing
let veryBigInt = 18014398509481982n;
Object(veryBigInt).toString(); // "18014398509481982"

The new operator

Older wrapper objects, such as String, Number, and Boolean, can be constructed using the new operator:

let food = "pizza";
new String(food); // String {"pizza"}

When invoked without the new operator, these functions perform type conversion:

let numCards = "52";
Number(numCards); // 52

Newer wrapper objects, such as Symbol and BigInt, cannot be constructed using the new operator. You'll get a TypeError if you try:

let hieroglyph = Symbol("hieroglyph");
new Symbol(hieroglyph); // Uncaught TypeError: Symbol is not a constructor

let veryBigInt = 18014398509481982n;
new BigInt(veryBigInt); // Uncaught TypeError: BigInt is not a constructor

Invoking these functions without the new operator does not perform type conversion; it creates a primitive value:

Symbol("hieroglyph"); // Symbol(hieroglyph)
BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n

Note that calling Symbol("hieroglyph") does not convert the string "hieroglyph" to a symbol. The string is just a description of the symbol which can be used for debugging but not to access the symbol itself (MDN).

Why can't newer wrapper objects be constructed? Because it's an anti-pattern. I found an answer on Stack Overflow that addresses this:

The functions for new primitive types (Symbol, BigInt) don't get construction signatures because TC39 doesn't think they need them. On this issue on the BigInt proposal, Jordan Harband (TC39 member and former editor of the specification) said:

Just like Symbol, it doesn't make sense to use new with things that produce a primitive. Since BigInt (like Symbol) is a primitive, it should only be invoked as a function.

Pre-ES6 primitive constructors unfortunately must retain their new-ability, for back compat.

and from this one (also Jordan):

...users should never use new with a Number object tho, because it's widely considered a very bad practice to ever use boxed primitives.

The new paradigm (no pun intended) is the one Symbol follows - if you want an object, you have to explicitly pass the primitive into Object. new Primitive() is a footgun, and new primitives should not continue this legacy pattern.

The use cases for new String, etc., are very few and far between (if there even are any that aren't better solved in other ways). Having construction signatures for Number and String and Boolean is confusing (do you create a string just by using a literal, or by using new String? why is it new String("x") === "x" is false? etc.). It looks like the committee decided to avoid that kind of confusion with newer primitives. (They still have object counterparts, it's just slightly less obvious how you get them, making it less likely to cause confusion.)

null and undefined

The primitive values null and undefined don't have corresponding wrapper objects. Passing them to the Object() constructor returns an empty object:

Object(null); // {}
Object(undefined); // {}

The primitive value null is used to represent the intentional absence of any … value (MDN), while the primitive value undefined is automatically assigned to variables that have just been declared, or to formal arguments for which there are no actual arguments (MDN).