JavaScript Objects Explained: Properties, Ordinary vs. Exotic, and Realms
Image from pixabay.com
This article delves into the details of an object in JavaScript or more technically ECMAScript.
Let’s begin with a fundamental definition: a JavaScript object is essentially a collection of key-value pairs, where keys are typically strings or Symbols. They are fundamental to JavaScript’s data structure model, enabling the creation of custom data structures and more complex organizations of data.
Data Property vs Accessor Property
The keys of an object are also referred to as property name, and each property falls into one of two categories: a data property or an accessor property:
Data Property
A data property holds a JavaScript language value (e.g., undefined
, null
, string
, symbol
, object
, number
, bigint
, boolean
) along with a set of internal Boolean attributes that define its configuration.
const person = {
name: "Bram Caro",
age: 45,
};
In the code example above, we have defined data properties. We have created an object with property keys name
and age
. These property keys directly hold the value.
You can retrieve the attributes of a data property using the Object.getOwnPropertyDescriptor()
method. The example below demonstrates this concept:
console.log(Object.getOwnPropertyDescriptor(person, "name"));
The code above outputs a set of data property attributes:
{
value: 'Bram Caro',
writable: true,
enumerable: true,
configurable: true
}
value
: This attribute holds the value assigned to the property.writable
: This is aBoolean
attribute. If set tofalse
, it makes the value attribute read-only. In other words, ifwritable
is set tofalse
, one would not be able to change the property’s value for the key name.enumerable
: This is aBoolean
attribute that, if set to false, will make the property unavailable for iteration (e.g., in a for…in loop).configurable
: This is anotherBoolean
attribute that, if set tofalse
, will not allow any changes to property attributes except changing thevalue
attribute and settingwritable
tofalse
.
Object.defineProperty(person, "name", {
writable: false,
});
person.name = "Some other name";
console.log(person.name); //Bram Caro
In this example, the name
property of the person
object is set to writable: false
. On the next line, we attempt to change the value of name
. This operation will not throw an error but will silently fail
. Consequently, console.log(person.name)
will still print the original value.
Accessor Property
An accessor property associates a key with one or two accessor functions (a getter, a setter, or both) and a set of internal Boolean attributes. These accessor functions are executed to control how the property’s value is stored and retrieved.
Accessor properties are excellent candidates for performing calculations on properties before retrieving them, executing validations, or triggering side effects.
In the example below, we will demonstrate how to implement accessor property:
const person = {
personName: "Bram Caro",
// This is a data property holding the actual name
// 'name' is an accessor property
get name() {
return this.personName; // The getter function
},
set name(value) {
this.personName = value; // The setter function
},
};
console.log(person.name); // Accesses the getter
person.name = "Amole"; // Accesses the setter
console.log(person.name); // Accesses the getter again
This example defines an accessor property named name on the person object.
- The
get name()
method is a getter. When you read person.name, this function executes, and its return value (this.personName
) becomes the value ofperson.name
. - The
set name(value)
method is a setter. When you assign a value toperson.name
(e.g.,person.name = "Amole"
), this function executes. The assigned value is passed as thevalue argument
to the setter, allowing you to control how it’s stored (here, by updating the internalpersonName
data property). - The output demonstrates this: Initially,
person.name
retrieves “Bram Caro” via the getter. Afterperson.name = "Amole"
is called (triggering the setter), subsequent reads ofperson.name
retrieve the updated “Amole” via the getter.
{
get: [Function: get name],
set: [Function: set name],
enumerable: true,
configurable: true
}
This output is from Object.getOwnPropertyDescriptor(person, "name")
when applied to an accessor property. Unlike a data property descriptor, it does not have a value
or writable
attribute. Instead, it has:
get
: A reference to the getter function.set
: A reference to the setter function.enumerable
andconfigurable
: These Boolean attributes function the same way as they do for data properties, controlling iteration and mutability of the property’s attributes respectively.
Ordinary vs Exotic Objects
JavaScript objects are broadly categorized into two fundamental types:
Ordinary Object
An Ordinary Object is a standard JavaScript object without any unique or ‘magical’ built-in behaviors. They possess all the essential internal slots and methods as defined by the language specification. Examples include objects created via the Object()
constructor or using object literal syntax (e.g., {}).
Exotic Objects
An Exotic Object is any object that deviates from the standard behaviors of ordinary objects, possessing unique internal semantics or ‘special’ functionalities. Simply put, it’s an object that is not an ordinary object. A prime example is the JavaScript Array. While arrays are indeed objects, they exhibit special behaviors, such as their length property automatically updating when elements are added or removed. Conversely, if you manually set an array’s length to zero, the array will be truncated. These are the kinds of built-in ‘magical’ functionalities that differentiate Exotic Objects like Arrays from ordinary objects.
These exotic behaviors exist for performance reasons and to enable distinct functionalities for objects. For example, by making Array
an exotic object, JavaScript engines can implement ‘Fast Arrays.’ Using 32-bit indices, direct indexing of array elements allows engines like V8 to directly access memory offsets instead of first performing a hash key lookup.
Another example of an exotic object is a function object. ECMAScript allows function objects to be called, to construct new objects, and to set their prototype property for inheritance. Allowing an object to be called and to have instructions execute when called provides the behavior for function execution.
An example of function object:
function CallableObject() {
console.log("I am a callable object");
}
CallableObject();
This code defines CallableObject
, which is a function. When CallableObject()
is invoked, the code inside its body (console.log("I am a callable object")
) is executed, demonstrating its “callable” exotic behavior.
console.log(Object.getPrototypeOf(CallableObject));
// Output: [Function CallableObject] Object
console.log(CallableObject instanceof Object); // true
These lines confirm that CallableObject is indeed an object.
Object.getPrototypeOf(CallableObject)
returns[Function: CallableObject]
, indicating that its prototype isFunction.prototype
(and by extension, it inherits fromObject.prototype
).CallableObject instanceof Object
evaluates to true, directly confirming that a function is an instance ofObject
in JavaScript.
Another example, using Function
constructor:
const fobj = new Function('console.log("I am a callable object two");');
fobj(); // Output: I am a callable object two
Here, fobj
is created using the Function
constructor. This demonstrates another way to create a function object programmatically. When fobj()
is called, the string provided to the constructor is executed as JavaScript code. This highlights the ability of function objects to encapsulate and execute code.
While functions can be created using the Function
constructor with the new
keyword, doing so is generally discouraged in a production environment. This is primarily due to security risks (as it evaluates strings as code, similar to eval()
) and potential performance overhead. The recommended way to define a function is using the function
keyword (or arrow function syntax), which provides better readability and is easier to maintain.
Object Indices
The ECMAScript specification formally distinguishes between two types of indices: Integer Index and Array Index.
Integer Index
An Integer Index applies to ordinary objects. In JavaScript, ordinary objects can utilize square bracket notation to access or define properties whose keys are numeric integral values.
const obj = {};
obj[1] = "value for index one";
console.log(obj); // Output: { '1': 'value for index one' }
console.log(obj[1]); // Output: value for index one.
An Integer Index is defined as an integral number (a non-fractional number) ranging from +0 up to 253 −1. Crucially, because JavaScript object keys can only be strings or Symbols, any numeric integral index used as a key on an ordinary object is implicitly converted to its string representation. For example, the numeric index 1 in the previous example is converted to the string ‘1’, which is why the output console.log(obj) shows the key wrapped in single quotes: { '1': 'value for index one' }
.
Array Index
An Array Index is a subset of Integer Indices, ranging from +0 up to 232 −2. This specific range is reserved for true array indexing. Consequently, an array (being an exotic object with specialized behavior) is effectively limited to this upper bound for its length and directly accessible elements.
Intrinsic, Fundamental Objects
Beyond the classification of Ordinary and Exotic Objects, the ECMAScript specification further categorizes objects, particularly among its built-in objects. While these categories sometimes overlap, they highlight specific architectural roles.
Intrinsic Objects
In JavaScript (as defined by the ECMAScript specification), Intrinsic Objects are fundamental built-in objects that are automatically initialized by the JavaScript engine (e.g., V8, SpiderMonkey) before any user code begins execution. They form the core runtime environment of the language. Key examples include Object, Array, Function, and Promise.
Crucially, Intrinsic Objects are realm-specific identities: each Realm possesses its own unique set of these foundational objects. These objects are created and fully initialized during the Realm’s instantiation.
What is a Realm?
A Realm is a distinct sandbox environment, or execution context, designed for evaluating ECMAScript (JavaScript) code. In a browser environment, an <iframe> is a prime example of a Realm. JavaScript code executing within an <iframe> operates in its own Realm, possessing a distinct set of Intrinsic Objects (built-in objects like Object, Array, Function, etc.).
A realm example of VM in Node.JS:
const vm = require("vm");
const realm1 = vm.createContext();
const realm2 = vm.createContext();
vm.runInContext("var y='I am in Realm One';", realm1);
vm.runInContext("var y='I am in Realm Two';", realm2);
console.log(realm1.y); // I am in Realm One
console.log(realm2.y); // I am in Realm Two
console.log(y); // Uncaught ReferenceError: y is not defined
-
const vm = require("vm");
: This line imports Node.js’s built-in vm (Virtual Machine) module. The vm module provides APIs for compiling and running JavaScript code within V8 (Node.js’s JavaScript engine) contexts. These contexts are essentially what the ECMAScript specification calls “Realms.” -
const realm1 = vm.createContext();
: Here, we are creating a new, isolated V8 context (or Realm). Think of realm1 as a completely blank, new JavaScript global environment. It has its own global object, its own set of built-in objects (like Object, Array, Function), and its own scope chain, all separate from the main Node.js process. Similarly, on the next line we create another Realm calledrealm2
which is completely isolated execution context fromrealm1
. -
vm.runInContext("var y='I am in Realm One';", realm1);
: This is where the magic happens. We’re telling the vm module to execute the JavaScript code string"var y='I am in Realm One';"
within the context of realm1. Because realm1 is an isolated environment, thevar y
declaration creates a globalvariable y
only within realm1’s global scope. It does not affect the y variable in realm2 or the main Node.js environment. Next line does the exact same thing, but for realm2. A globalvariable y
is created only within realm2’s global scope, and it’s assigned a different value.
In essence, a Realm provides a self-contained execution environment complete with its own set of built-in objects, a unique global scope, and an independent set of global properties. It serves as the fundamental context within which ECMAScript code is evaluated, and where all JavaScript objects are created. Host environments are responsible for establishing and managing these Realms.
Fundamental Objects
Fundamental Objects are those built-in objects that are absolutely essential for the runtime semantics of the ECMAScript language. This category includes, but is not limited to, the Object object, Function object, Boolean object, Symbol object, and various Error objects. A host environment cannot function without the presence and proper operation of these Fundamental Objects.
The primary Fundamental Objects commonly identified are:
- Object (the constructor/prototype for generic objects)
- Function (the constructor/prototype for functions)
- Boolean (the wrapper for boolean primitives)
- Symbol (the wrapper for symbol primitives)
- Error (and its sub-types like TypeError, RangeError, etc.)
For instance, consider a scenario where the Object Fundamental Object is not present in an execution context; in such a case, the very concept of an object would cease to exist.
Object.prototype
serves as the base prototype for virtually all other objects in JavaScript, meaning they inherit properties and methods from it. Consequently, without the Object constructor and its Object.prototype
, fundamental language features like object instantiation, property inheritance, and even basic object operations would be impossible.
Summary
In this comprehensive exploration, we’ve dissected the multifaceted nature of JavaScript objects as defined by the ECMAScript specification. From the foundational distinction between data and accessor properties to the nuanced categories of ordinary and exotic objects, and further into the roles of intrinsic and fundamental built-ins and isolated Realms, understanding these concepts is crucial. This deeper knowledge empowers you to write more efficient, predictable, and robust JavaScript code by truly comprehending what happens beneath the surface. As you continue your journey in JavaScript, remember that objects are at the very heart of the language, and mastering their intricacies is key to becoming a proficient developer.