Primitives have no properties of their own. When you write 'abc'.toUpperCase(), the spec's property-read step (GetValue) calls ToObject on the base value. For a string, ToObject returns a new String object whose [[StringData]] slot holds the primitive; Number, Boolean, Symbol and BigInt work the same way with their own wrapper types. The property lookup then runs on that object, finds toUpperCase on String.prototype, and calls it with the original primitive as this. Nothing is stored afterwards. MDN calls this auto-boxing.
Consequences:
- Writes are lost.
PutValuealso converts the base withToObject, sostr.foo = 1writes onto a throwaway wrapper. In strict mode (modules, classes) the failed write throws aTypeError; in sloppy mode it's silently ignored. nullandundefinedhave no wrapper.ToObjectthrows aTypeErrorfor them, which is whyundefined.xthrows (V8 words it "Cannot read properties of undefined").- Engines may skip the allocation. The spec notes that the object "is not accessible outside" the operation, so an implementation may avoid creating it.
thisis the one place boxing can show. A sloppy-mode function called on a primitive receives a wrapper object asthis; a strict function receives the primitive unchanged.
Don't create wrappers yourself. new String('a') is an object: typeof is "object", new String('') is truthy, and new String('a') === new String('a') is false. Called without new, String(x), Number(x) and Boolean(x) are plain conversions that return primitives.