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) orpush()
(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 aString
wrapper object and callsString.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 (becausestr.foo = 1
is not assigning to the propertyfoo
ofstr
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 theBigInt
proposal, Jordan Harband (TC39 member and former editor of the specification) said:Just like
Symbol
, it doesn't make sense to usenew
with things that produce a primitive. SinceBigInt
(likeSymbol
) 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 aNumber
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 intoObject
.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 forNumber
andString
andBoolean
is confusing (do you create a string just by using a literal, or by usingnew String
? why is itnew 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).