JavaScript behaves quite differently from traditional object-oriented languages like Java or C++. To truly understand how objects, inheritance, and memory layout work in JavaScript, we need to move past the syntax and explore the runtime model, especially how prototype chains and constructor functions work under the hood.
JavaScript values fall into two main categories: primitives and objects.
Primitives represent immutable data types like numbers, strings, and booleans. These values are stored directly on the stack, and any operation on them returns a new value rather than modifying the original. When we work with something like "hello".length, JavaScript temporarily wraps the string primitive in a String object to access its property, then discards the wrapper.
Objects, on the other hand, are reference types stored on the heap. Every non-primitive — arrays, functions, maps, user-defined objects is a heap-allocated object with properties that can change over time. We can dynamically add or remove keys and mutate values freely. Internally, JavaScript engines implement objects as hash tables with string or symbol keys, and the associated values can be anything even functions or other objects. This structure is highly dynamic, in contrast to languages like Java or C++, where an object’s memory layout is fixed at compile time based on its class definition.
For example, in Java we define a class upfront, and every instance has the same layout in memory. In JavaScript, we can start with an empty object and add new properties at any time. We can even delete existing ones or change their types on the fly. That flexibility is powerful, but it also means the engine has to do more work to manage performance. Internally, JavaScript engines like V8 optimize this by creating hidden classes and switching strategies based on access patterns, but conceptually, it all behaves like a flexible key-value map.
This flexibility is powered by the prototype system. JavaScript uses prototype-based inheritance instead of class-based. Each object has an internal [[Prototype]] key that points to another object. This prototype link is what JavaScript follows when we try to access a property that doesn't exist on the object itself. If we access rabbit.eats, and rabbit doesn't have that property, JavaScript looks up rabbit.[[Prototype]] say, an animal object and continues searching there. This chain continues until it either finds the property or reaches null.
Here’s the example:
const animal = { eats: true };
const rabbit = Object.create(animal);
rabbit.hops = true;console.log(rabbit.hops); // true
console.log(rabbit.eats); // true
When we access rabbit.hops, JavaScript finds the property directly on the object. But when we access rabbit.eats, the engine walks up the prototype chain to find it on animal. If we accessed something like rabbit.toString, the lookup would continue further up to Object.prototype.
The full chain in this case looks like:

Each step is an object linked to the next via [[Prototype]]. We can inspect these links using Object.getPrototypeOf(obj) or the legacy __proto__ property.
console.log(Object.getPrototypeOf(rabbit) === animal); // true
console.log(rabbit.__proto__ === animal); // trueconsole.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(animal.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
console.log(Object.prototype.__proto__); // null
Functions:
Functions in JavaScript are more than just callable routines. They are also objects. This means a function can be invoked, but it can also hold properties just like any other object.
function greet() {
console.log("hello");
}
greet.language = "English";Here, greet is a function, but since it's also an object, we can attach a property like language to it. Internally, functions are allocated on the heap and include a set of hidden internal slots that allow them to be invoked. One of those is the function’s [[Call]] internal method, which makes it executable.
In addition to being callable, functions can also act as constructors when used with the new keyword. This duality being both a function and a constructor is central to how JavaScript supports object creation and inheritance without classes in the traditional sense.
function Person(name) {
this.name = name;
}
const p = new Person("Tom");When new Person("Tom") is invoked, the JavaScript engine performs a series of internal steps:
- It creates a new empty object in memory.
- This object’s internal
[[Prototype]]is set toPerson.prototype. - The
Personfunction is called withthisbound to the new object. - Inside the function,
this.name = nameassigns thenameproperty. - The newly initialized object is returned unless the constructor explicitly returns a different object.
So after the call, p is an object with a name property, and it inherits from Person.prototype. The resulting prototype chain looks like this:
p → Person.prototype → Object.prototype → nullBut the function itself, Person, is also an object created by the engine when the function was declared. Since it's an object, it has its own internal [[Prototype]] that points to Function.prototype, and from there to Object.prototype. That’s a second prototype chain:
Person → Function.prototype → Object.prototype → nullThese two chains are parallel: one governs the object instance created by new, and the other governs the constructor function itself.


To understand the relationship more concretely:
- The
Personfunction object has a.prototypeproperty. This is just a regular object created by the engine, and its main role is to serve as the prototype for all instances created usingnew Person(). - When
pis created, its internal[[Prototype]]is set toPerson.prototype, allowing it to inherit any methods defined there. - The function object
Person, like all functions, inherits fromFunction.prototype. That’s why it has access to.call(),.bind(),.apply(), and so on.
Person.prototype.sayHello = function() {
console.log(`Hi, I'm ${this.name}`);
};p.sayHello(); // Hi, I'm Tom
This works because the engine doesn’t find sayHello on p itself, so it walks up the prototype chain and finds it on Person.prototype.
To clarify the difference:
Person.prototypeis an object used to set the[[Prototype]]of new instances created byPerson.Person.[[Prototype]](invisible to JavaScript code, but accessible viaObject.getPrototypeOf(Person)) is the function object’s own inheritance link, which points toFunction.prototype.
This dual-structure is what allows functions to act as both logic containers and object blueprints.
It’s also why arrow functions behave differently. Arrow functions don’t have a prototype property at all. i.e, they cannot be used with new, because there’s no prototype object to link to the instance. They are designed to be lightweight and lexically bound to the surrounding this.

For example:
const Arrow = () => {};
console.log(Arrow.prototype); // undefined
new Arrow(); // TypeError: Arrow is not a constructorOn the other hand, regular functions always come with a .prototype object by default (unless we override it), and that is what powers object construction via new.
Classes:
As for classes, they’re just syntactic sugar over constructor functions. When we write:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} speaks.`);
}
}Under the hood, JavaScript creates a function named Animal, sets up its prototype property, and adds the speak method to Animal.prototype. It behaves identically to writing:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} speaks.`);
};The only difference is syntax and some stricter rules. For example, classes must be called with new, and their methods are non-enumerable by default.
To summarize, when the JavaScript engine boots up, for instance, when V8 initializes, it sets up the base objects in memory. It creates Object, Function, Array, and so on, along with their corresponding .prototype objects. These objects are created in the heap and form the roots of the entire object graph.




