Exploring the ECMAScript Specification: Primitives
2018. 2. 23.

Questions About Primitives
Currently, the seniors within the team are improving the team's personnel hiring process. Among the processes, we are currently rewriting the written test questions, which is proving more difficult than expected, leading to daily disagreements. Then, one day, while discussing primitives, a debate arose about whether it's acceptable to explain that JavaScript primitives are used like objects. My opinion was that since they can be used like objects on the surface, it's fine to describe them as being treated like objects. Another member argued that because a primitive wrapper is involved internally, such an explanation is inappropriate. One focused on the surface behavior, the other on the internal mechanism. However, during the discussion, I felt that perhaps I had too superficial an understanding of primitives. While this detail might not significantly impact development work, our team duties include internal JavaScript training, so I thought a deeper, more accurate understanding was necessary. Therefore, I decided to delve into the specification's definition and actual behavior.
This article covers what primitives are and how primitive wrappers are involved, based on the specification document and some experiments. I decided to stick with the term "primitive" rather than a Korean equivalent to avoid clashing with other technical terms used. While it covers some well-known concepts, it's written for fellow front-end developers who want a slightly deeper understanding of primitives. This might be information that general web developers don't necessarily need to know. However, if you're someone who likes to be precisely clear when discussing things, read on, as it's a short article.
What Are Primitive Types?
It's a familiar topic, but let's review it. JavaScript has a total of six primitive types: undefined, null, boolean, string, number, and the lastly added symbol. Add the object type (which is not a primitive), and you have all the language types provided by ECMAScript.
The specification overview describes primitive values as follows:
"A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, String, and Symbol …"
A primitive value belongs to one of the built-in types. This is a separate concept from the built-in type constructors. These are the familiar values we commonly use:
var numberPrimitive = 10;
var booleanPrimitive = false;
var stringPrimitivie = 'string boy';
typeof
The typeof
keyword allows you to determine the type of a target. Using it directly on a primitive value reveals the type of that value. Functions are classified as callable objects, belonging to the object type. Therefore, they possess all the characteristics of objects. typeof
can distinguish between ECMAScript language types. A peculiarity is that typeof
applied to a function returns "function", even though "function" is not an official ECMAScript language type. Briefly explaining the mechanism of typeof
, if something is an object and implements the internal [[Call]]
property, it returns "function". This seems like special treatment for functions, likely because there are cases where distinguishing between functions and other objects is necessary. As an aside, one might find it strange that typeof null
returns "object". null
signifies the intentional absence of an object value. In the same vein that typeof 0
returns "number" (signifying the absence of a numeric value in some contexts, perhaps), null
, signifying the absence of an object, returns "object".
Characteristics of Primitives
The specification mentions three terms directly related to primitives:
- Primitive Value
- Primitive Type
- Primitive Object.
A primitive value represents the lowest-level datum in the JavaScript language implementation. For number values, it's the 64-bit double-precision binary format IEEE 754-2008 value. This is why performing arithmetic with decimals can lead to the well-known issues of the IEEE 754 standard (e.g., 0.1 + 0.2). Primitive values belong to a specific primitive type. A primitive type represents all possible values of that kind. Primitive types cannot have properties because they are not objects. A primitive object is an instance created via the built-in constructor corresponding to that primitive type. This instance includes properties for handling primitive values of that specific type and holds the primitive value in an internal slot ([[PrimitiveValue]]
). It belongs to the object type, meaning it can have properties because it's an object. The primitive value held by a primitive object can be retrieved using the valueOf()
method.
For example, the number 10 is a Number Value and belongs to the number
type. NaN
and Infinity
also belong to the number
type (NaN and Infinity are number values defined in IEEE 754). If you create an object using the Number
constructor, its type is object
, but you can obtain the primitive value using valueOf()
.
When performing operations using operators, the object's valueOf()
method is executed, and the result is used for evaluation. Therefore, operations can be performed between objects and primitive values interchangeably.
var num10 = 10; // Number value
typeof num10 // 'number', Number value is of type number
typeof NaN // 'number'
typeof Infinity // 'number'
var num10Obj = new Number(10); // Number object
typeof num10Obj // 'object'
console.log(num10Obj) // In Chrome browser, you can inspect the value of the internal slot [[PrimitiveValue]] via the console.
num10Obj.valueOf() // 10
num10 === num10Obj // Evaluated as num10 === num10Obj.valueOf();
The valueOf
method can be overridden. This means you can extend its behavior to perform actions suited to the object's intent, rather than just returning the primitive value from the internal slot. Since JavaScript doesn't allow operator overloading, this feature can be used, albeit modestly, to make objects react appropriately to operators. This is possible because, internally, the object's valueOf
is executed first, and the operation is performed on the resulting value.
Leveraging the fact that an object's valueOf
is executed during operator computations allows developers to, albeit modestly, make objects respond to operators as intended, even without operator overloading in JavaScript. That is, its behavior can be changed and extended according to the object's intent, rather than simply returning the primitive value from the internal slot.
num10Obj.valueOf = function() { return 50; }; // The valueOf method can be overridden.
num10 + num10Obj // 60
Number.prototype.valueOf.call(num10Obj); // 10, The value 10 is still stored in the internal slot.
Primitive values are immutable. Primitive types are passed by value (Call by value), while objects are passed by reference (Call by reference). However, even for object types, operations utilize the primitive value obtained via the valueOf()
method, so the fundamental immutability of the value remains. Also, note that the result of an operation involving a primitive object is converted back to a primitive value.
var n10a = 10;
var n10b = n10a; // The value is copied.
n10b += 1;
n10a === n10b // false,
var no10a = new Number(10);
var no10b = no10a; // The reference is passed; a and b refer to the same object.
var no10c = new Number(10);
no10a === no10b // true, Reference comparison.
no10a === no10c // false, Reference comparison.
no10b += 1;
no10a === no10b // false, no10b now holds the number value 11, not a Number object.
Type Coercion to Primitive Objects
Primitive values cannot have properties. They aren't objects; they are just low-level pieces of data occupying a defined amount of space in memory.
var num = 10;
num.newProp = 5;
console.log(num.newProp); // undefined
The code itself executes normally without errors, but the property is not created. This behavior is due to the operation of the Set
abstract operation in the specification. Set
takes arguments like the object, property name, and value, and is used to assign a specific value to an object's property. The Set
abstract operation is as follows:
1. Assert: Type(O) is Object.
2. Assert: IsPropertyKey(P) is true.
3. Assert: Type(Throw) is Boolean.
4. Let success be ? O.[[Set]](P, V, O).
5. If success is false and Throw is true, throw a TypeError exception.
6. Return success.
The assertion on the first line states that the target's type must be Object
. This means a property is only created if the target is an object. If it's not an object, the operation is ignored without any error. However, in strict mode, a TypeError
exception is thrown, as shown below:
"Uncaught TypeError: Cannot create property 'newProp' on number '10'"
The reason errors occur in strict mode can be found in the The Strict Mode Of ECMAScript section of the specification.
"…nor to a non-existent property of an object whose [[Extensible]]
internal slot has the value false. In these cases a TypeError exception is thrown."
Accessing a non-existent property on an identifier whose target's [[Extensible]]
internal slot has the value false
results in a TypeError
exception. In other words, if the target is not Extensible, you cannot add properties to it, hence the exception. Fortunately, an API to check the value of the [[Extensible]]
internal slot is exposed in JavaScript, allowing verification in code: the Object.isExtensible()
method.
var obj = {};
console.log(Object.isExtensible(obj)) // true
var num = 10;
console.log(Object.isExtensible(num)) // false
console.log(Object.isExtensible(undefined)); // false
console.log(Object.isExtensible(null)); // false
console.log(Object.isExtensible(NaN)); // false
Perhaps the most defining characteristic of JavaScript primitives is the implicit type coercion to primitive objects. This allows primitive number values, not just objects created with the Number
constructor, to use properties from the Number
constructor's prototype, just like objects. While this is a necessary feature for coding convenience, it appears to be influenced by Java's autoboxing.
var num = 0.1234;
num.toFixed(2); // 0.12
We can confirm that this type coercion actually happens by adding a simple function to the Number
constructor's prototype.
Number.prototype.whoAmI = function() { console.log(typeof this);}
var num = 10;
typeof num; // "number"
num.whoAmI(); // "object"
typeof num // "number"
The type coercion to a primitive object is actually performed by the abstract operation [[ToObject]]
. Depending on the type of the primitive value passed as an argument, it uses a predetermined constructor based on the specification's conversion table to return a new object that holds the passed value in its internal slot. In other words, it returns a primitive object that evaluates identically to the input primitive value. While there are various reasons for type coercion to occur, the actual conversion happens exclusively through the [[ToObject]]
abstract operation.
While [[ToObject]]
is what effectively changes a primitive value into a primitive object, the abstract operation executed when our code accesses a property of a primitive value is [[GetV]]
. [[GetV]]
is performed when accessing a property of a primitive value. If the target is not an object, it creates a primitive object of the same type to allow borrowing the property. This operation, of course, also uses [[ToObject]]
.
1. Assert: IsPropertyKey(P) is true.
2. Let O be ? ToObject(V).
3. Return ? O.[[Get]](P, V).
P is the property to access, V is the primitive value, and O is the newly created primitive object. After verifying that the property key P is valid, it performs the conversion using [[ToObject]]
and then executes the abstract operation [[Get]]
on the created O. [[Get]]
is the operation performed when accessing an object's property. Since we created an object from the primitive value, accessing the object's property proceeds normally. It's a surprisingly straightforward process. Although we call it "type coercion to a primitive object," that's the surface meaning; internally, it's the creation, use, and subsequent disposal of a primitive object. In the JavaScript code example above, the object created when the whoAmI
method is called disappears once the method execution finishes. The original primitive value remains unchanged.
Summary
Thus, we've summarized primitives based on the specification. I plan to occasionally serialize content by dissecting and summarizing parts of the specification like this in the future. While nothing is set in stone, the next topic is likely to be strict mode. Strict mode might seem like another obvious topic, but this endeavor started with the desire to understand already familiar concepts in greater detail. This article was written based on the latest specification. Having thoroughly read the ES5 specification years ago, seeing how much has changed and continues to change in recent specs gives me a sense of nostalgia... accompanied by the thought that I should probably avoid reading the entire spec cover-to-cover again(?) ECMAScript will continue to change, improve, and evolve energetically.
with kakaopay
Recommend Post
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.