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); // true
console.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:

  1. It creates a new empty object in memory.
  2. This object’s internal [[Prototype]] is set to Person.prototype.
  3. The Person function is called with this bound to the new object.
  4. Inside the function, this.name = name assigns the name property.
  5. 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.prototypeObject.prototypenull

But 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:

PersonFunction.prototypeObject.prototypenull

These 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 Person function object has a .prototype property. This is just a regular object created by the engine, and its main role is to serve as the prototype for all instances created using new Person().
  • When p is created, its internal [[Prototype]] is set to Person.prototype, allowing it to inherit any methods defined there.
  • The function object Person, like all functions, inherits from Function.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.prototype is an object used to set the [[Prototype]] of new instances created by Person.
  • Person.[[Prototype]] (invisible to JavaScript code, but accessible via Object.getPrototypeOf(Person)) is the function object’s own inheritance link, which points to Function.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 constructor

On 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.