Skip to main content

How to Use the F.prototype Property in JavaScript Constructor Functions

In the previous guide, you learned how prototypal inheritance works: every object has a hidden [[Prototype]] link, and JavaScript follows that link to find inherited properties. But how does that link get set in the first place when you create objects with constructor functions and the new keyword?

The answer lies in a special property that every function in JavaScript has: the prototype property. When you call a function with new, JavaScript uses that function's prototype property to set the [[Prototype]] of the newly created object. This mechanism is the bridge between constructor functions and the prototype chain, and understanding it is essential before diving into the class syntax (which is just syntactic sugar over this exact pattern).

This guide explains how F.prototype works, how default values behave, how to set up inheritance chains, and the critical mistakes that trip up even experienced developers.

The prototype Property of Constructor Functions

Every function in JavaScript automatically gets a property called prototype. This is a regular, visible property (not the hidden [[Prototype]] internal slot). For most functions, it sits there unused. But when a function is called with the new operator, the prototype property plays a critical role.

The Core Rule

When new F() is called, JavaScript sets the new object's [[Prototype]] to whatever F.prototype points to at that moment.

function Animal(name) {
this.name = name;
}

Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};

const dog = new Animal("Rex");

console.log(dog.name); // "Rex" (own property, set in constructor)
console.log(dog.speak()); // "Rex makes a sound" (inherited from Animal.prototype)

Here is what happened step by step when new Animal("Rex") executed:

  1. JavaScript creates a new empty object {}
  2. It sets that object's [[Prototype]] to Animal.prototype
  3. It calls Animal() with this pointing to the new object
  4. The constructor assigns this.name = "Rex"
  5. The new object is returned (since the constructor does not explicitly return an object)

After construction, the relationship looks like this:

dog                          Animal.prototype
┌─────────────────┐ ┌─────────────────────┐
│ name: "Rex" │ │ speak: function │
│ │ │ constructor: Animal │
│ [[Prototype]] ──────────► │ │
└─────────────────┘ │ [[Prototype]] ────────► Object.prototype
└─────────────────────┘

Verifying the Connection

You can verify that the new object's prototype is indeed F.prototype:

function User(name) {
this.name = name;
}

User.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};

const alice = new User("Alice");
const bob = new User("Bob");

// Both objects share the same prototype
console.log(Object.getPrototypeOf(alice) === User.prototype); // true
console.log(Object.getPrototypeOf(bob) === User.prototype); // true

// They share the same method (not copies)
console.log(alice.greet === bob.greet); // true

// The method works with the correct 'this'
console.log(alice.greet()); // "Hi, I'm Alice"
console.log(bob.greet()); // "Hi, I'm Bob"

F.prototype Is Only Used at new Time

A critical detail: F.prototype is read only once, at the moment of new call. Changing F.prototype afterward does not affect objects that were already created.

function Car(model) {
this.model = model;
}

Car.prototype.drive = function() {
return `${this.model} is driving`;
};

const car1 = new Car("Tesla");

// Change the prototype AFTER car1 was created
Car.prototype = {
fly() {
return `${this.model} is flying`;
}
};

const car2 = new Car("DeLorean");

// car1 still has the old prototype
console.log(car1.drive()); // "Tesla is driving"
console.log(car1.fly); // undefined (car1's prototype doesn't have fly)

// car2 has the new prototype
console.log(car2.fly()); // "DeLorean is flying"
console.log(car2.drive); // undefined (car2's prototype doesn't have drive)

// They have different prototypes
console.log(Object.getPrototypeOf(car1) === Object.getPrototypeOf(car2)); // false

F.prototype Must Be an Object (or null)

The prototype property only works as described if it is an object or null. If you set F.prototype to a primitive value (number, string, boolean), JavaScript ignores it and uses Object.prototype instead:

function Broken() {}
Broken.prototype = 42; // A primitive (will be ignored)

const obj = new Broken();

// JavaScript fell back to Object.prototype
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(obj) === Broken.prototype); // false

If F.prototype is null, the new object will have no prototype at all:

function Bare() {}
Bare.prototype = null;

const obj = new Bare();
console.log(Object.getPrototypeOf(obj)); // null
console.log(obj.toString); // undefined (no Object.prototype methods)

Default F.prototype and the constructor Property

Every function gets a default prototype object automatically. This default object contains a single property: constructor, which points back to the function itself.

function User(name) {
this.name = name;
}

// JavaScript automatically creates this:
// User.prototype = { constructor: User }

console.log(User.prototype);
// { constructor: ƒ User }

console.log(User.prototype.constructor === User); // true

This creates a circular reference that is actually very useful:

User (function)

│ .prototype

┌─────────────────────┐
│ constructor: User ◄─┼─── points back to User
│ │
└─────────────────────┘

Using constructor to Identify the Creator

Because instances inherit from F.prototype, they also inherit the constructor property:

function User(name) {
this.name = name;
}

const alice = new User("Alice");

// alice inherits constructor from User.prototype
console.log(alice.constructor === User); // true

// It's not an own property (it's inherited)
console.log(alice.hasOwnProperty("constructor")); // false

Creating New Instances from Existing Ones

The constructor property is useful when you have an object and want to create another instance of the same type, without knowing the constructor function's name:

function Article(title, date) {
this.title = title;
this.date = date;
}

const article1 = new Article("JavaScript Basics", new Date(2024, 0, 15));

// Create another article using the same constructor
const article2 = new article1.constructor("Advanced Patterns", new Date(2024, 5, 20));

console.log(article2.title); // "Advanced Patterns"
console.log(article2 instanceof Article); // true

This pattern is particularly useful in generic code or libraries that receive objects and need to create new instances of the same type without hardcoding the constructor name.

The Default Prototype Is Not Special

JavaScript does not protect the default prototype object or the constructor property in any way. You can modify it, overwrite it, or delete the constructor property entirely. JavaScript will not fix it for you.

function User(name) {
this.name = name;
}

// The default prototype with constructor
console.log(User.prototype.constructor === User); // true

// Delete the constructor
delete User.prototype.constructor;

const alice = new User("Alice");
console.log(alice.constructor === User); // false
console.log(alice.constructor === Object); // true (falls back to Object.prototype.constructor)

When constructor is deleted from User.prototype, looking up alice.constructor walks further up the chain to Object.prototype.constructor, which is Object. The connection to User is lost.

caution

JavaScript does not guarantee that constructor is correct. It is your responsibility to maintain it when you modify prototypes. Many bugs come from accidentally losing this property.

Setting Up Inheritance with Constructor Functions

The real power of F.prototype appears when you chain constructor functions together to create an inheritance hierarchy.

Basic Pattern: Linking Prototypes

function Animal(name) {
this.name = name;
}

Animal.prototype.eat = function() {
return `${this.name} is eating`;
};

function Rabbit(name, color) {
Animal.call(this, name); // Call parent constructor
this.color = color;
}

// Set up inheritance: Rabbit.prototype should inherit from Animal.prototype
Rabbit.prototype = Object.create(Animal.prototype);

// Fix the constructor reference (it now points to Animal)
Rabbit.prototype.constructor = Rabbit;

// Add Rabbit-specific methods
Rabbit.prototype.jump = function() {
return `${this.name} jumps!`;
};

const bunny = new Rabbit("Thumper", "white");

console.log(bunny.name); // "Thumper" (set by Animal.call(this, name))
console.log(bunny.color); // "white" (set in Rabbit constructor)
console.log(bunny.eat()); // "Thumper is eating" (inherited from Animal.prototype)
console.log(bunny.jump()); // "Thumper jumps!" (from Rabbit.prototype)

console.log(bunny instanceof Rabbit); // true
console.log(bunny instanceof Animal); // true
console.log(bunny.constructor === Rabbit); // true (because we fixed it)

Let us break down the critical line:

Rabbit.prototype = Object.create(Animal.prototype);

This creates a new object whose [[Prototype]] is Animal.prototype, and assigns it as Rabbit.prototype. Now when a Rabbit instance looks for a method, the chain is:

bunny → Rabbit.prototype → Animal.prototype → Object.prototype → null

Why Object.create() and Not Direct Assignment

You might be tempted to use simpler approaches to link prototypes. Here is why they do not work:

Wrong approach 1: Direct assignment

// WRONG: both point to the same object
Rabbit.prototype = Animal.prototype;

Rabbit.prototype.jump = function() {
return "Jumping!";
};

// This also added jump to Animal.prototype!
console.log(Animal.prototype.jump); // function (oops!)

With direct assignment, Rabbit.prototype and Animal.prototype are the same object. Any method you add to one appears on the other.

Wrong approach 2: Using new Animal()

// BAD: creates an unnecessary Animal instance
Rabbit.prototype = new Animal();

This technically works for the prototype chain, but it has problems:

  • It calls the Animal constructor without proper arguments, which might cause errors or set invalid properties
  • It creates instance properties (name would be undefined) on the prototype object, which is wasteful
  • Any side effects in Animal() run at the wrong time

Correct approach: Object.create()

// CORRECT: creates a clean object with the right prototype
Rabbit.prototype = Object.create(Animal.prototype);

This creates a new, empty object whose [[Prototype]] is Animal.prototype. No constructor is called, no instance properties are created, no side effects occur.

Complete Inheritance Example

function Shape(color) {
this.color = color;
}

Shape.prototype.describe = function() {
return `A ${this.color} ${this.type || "shape"}`;
};

Shape.prototype.area = function() {
return 0; // Default, should be overridden
};

// Rectangle inherits from Shape
function Rectangle(color, width, height) {
Shape.call(this, color); // Call parent constructor
this.width = width;
this.height = height;
this.type = "rectangle";
}

Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

Rectangle.prototype.area = function() {
return this.width * this.height;
};

// Circle inherits from Shape
function Circle(color, radius) {
Shape.call(this, color);
this.radius = radius;
this.type = "circle";
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.area = function() {
return Math.PI * this.radius ** 2;
};

// Usage
const rect = new Rectangle("blue", 10, 5);
const circle = new Circle("red", 7);

console.log(rect.describe()); // "A blue rectangle"
console.log(rect.area()); // 50

console.log(circle.describe()); // "A red circle"
console.log(circle.area()); // 153.93804002589985

// instanceof works through the chain
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
console.log(circle instanceof Circle); // true
console.log(circle instanceof Shape); // true

// They don't cross
console.log(rect instanceof Circle); // false
console.log(circle instanceof Rectangle); // false

The prototype chain for rect is:

rect → Rectangle.prototype → Shape.prototype → Object.prototype → null

Do Not Replace prototype, Extend It

When adding methods to a constructor's prototype, there are two approaches. One is safe, the other is dangerous.

The Safe Way: Add Properties to the Existing Prototype

function User(name) {
this.name = name;
}

// Add methods one by one to the existing prototype object
User.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};

User.prototype.farewell = function() {
return `Goodbye from ${this.name}`;
};

const alice = new User("Alice");

console.log(alice.greet()); // "Hello, I'm Alice"
console.log(alice.farewell()); // "Goodbye from Alice"
console.log(alice.constructor === User); // true (preserved!)

This approach preserves the default prototype object and its constructor property.

The Dangerous Way: Replace the Entire Prototype

function User(name) {
this.name = name;
}

// Replacing the entire prototype object
User.prototype = {
greet() {
return `Hello, I'm ${this.name}`;
},
farewell() {
return `Goodbye from ${this.name}`;
}
};

const alice = new User("Alice");

console.log(alice.greet()); // "Hello, I'm Alice"
console.log(alice.farewell()); // "Goodbye from Alice"
console.log(alice.constructor === User); // false (LOST!)
console.log(alice.constructor === Object); // true (wrong!)

When you assign a new plain object to User.prototype, the default object (which had constructor: User) is thrown away. The new object does not have a constructor property, so alice.constructor walks up to Object.prototype.constructor, which is Object.

Using Object.assign() for Multiple Methods

If you want to add many methods at once without replacing the prototype, use Object.assign():

function User(name) {
this.name = name;
}

Object.assign(User.prototype, {
greet() {
return `Hello, I'm ${this.name}`;
},
farewell() {
return `Goodbye from ${this.name}`;
},
toString() {
return `User: ${this.name}`;
}
});

const alice = new User("Alice");

console.log(alice.greet()); // "Hello, I'm Alice"
console.log(alice.constructor === User); // true (preserved!)

Object.assign() copies properties into the existing User.prototype object rather than replacing it. The constructor property survives.

Common Mistake: Overwriting prototype and Losing constructor

This is the single most common mistake when working with constructor function prototypes. Let us examine the problem in detail and see every way to fix it.

The Problem

function Product(name, price) {
this.name = name;
this.price = price;
}

// Replace the prototype
Product.prototype = {
getInfo() {
return `${this.name}: $${this.price}`;
},
applyDiscount(percent) {
this.price *= (1 - percent / 100);
}
};

const laptop = new Product("Laptop", 999);

// Methods work fine
console.log(laptop.getInfo()); // "Laptop: $999"

// But constructor is broken
console.log(laptop.constructor === Product); // false
console.log(laptop.constructor === Object); // true

// Creating a new instance from an existing one fails
const tablet = new laptop.constructor("Tablet", 499);
// This calls new Object("Tablet", 499), not new Product(...)
console.log(tablet instanceof Product); // false (wrong type!)
console.log(tablet.getInfo); // undefined (no methods!)

The damage spreads: any code relying on constructor (pattern libraries, serialization code, factory methods) breaks silently.

Fix 1: Manually Restore constructor

function Product(name, price) {
this.name = name;
this.price = price;
}

Product.prototype = {
constructor: Product, // Manually add it back
getInfo() {
return `${this.name}: $${this.price}`;
},
applyDiscount(percent) {
this.price *= (1 - percent / 100);
}
};

const laptop = new Product("Laptop", 999);
console.log(laptop.constructor === Product); // true (fixed!)

This works, but the constructor property is now enumerable, unlike the original default which was non-enumerable:

// Original default constructor is non-enumerable
function Test() {}
console.log(Object.getOwnPropertyDescriptor(Test.prototype, "constructor"));
// { value: ƒ Test, writable: true, enumerable: false, configurable: true }

// Our manual one is enumerable
console.log(Object.keys(Product.prototype));
// ["constructor", "getInfo", "applyDiscount"] (constructor shows up!)

Fix 2: Restore constructor with Correct Flags

To match the original behavior exactly, use Object.defineProperty():

function Product(name, price) {
this.name = name;
this.price = price;
}

Product.prototype = {
getInfo() {
return `${this.name}: $${this.price}`;
},
applyDiscount(percent) {
this.price *= (1 - percent / 100);
}
};

// Restore constructor with correct flags (non-enumerable)
Object.defineProperty(Product.prototype, "constructor", {
value: Product,
writable: true,
enumerable: false, // Matches the original default
configurable: true
});

const laptop = new Product("Laptop", 999);
console.log(laptop.constructor === Product); // true
console.log(Object.keys(Product.prototype)); // ["getInfo", "applyDiscount"] (no constructor)

Fix 3: Do Not Replace, Extend Instead (Best Practice)

The cleanest solution is to avoid the problem entirely:

function Product(name, price) {
this.name = name;
this.price = price;
}

// Extend the existing prototype
Product.prototype.getInfo = function() {
return `${this.name}: $${this.price}`;
};

Product.prototype.applyDiscount = function(percent) {
this.price *= (1 - percent / 100);
};

// Or use Object.assign for multiple methods at once
// Object.assign(Product.prototype, { getInfo() {...}, applyDiscount() {...} });

const laptop = new Product("Laptop", 999);
console.log(laptop.constructor === Product); // true (never lost it)

The Inheritance Case: Always Fix constructor

When setting up inheritance, you always replace prototype, so you must always fix constructor:

function Animal(name) {
this.name = name;
}

Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};

function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}

// This replaces Dog.prototype, losing constructor
Dog.prototype = Object.create(Animal.prototype);

// ALWAYS fix constructor after setting up inheritance
Dog.prototype.constructor = Dog;

// Now add Dog-specific methods
Dog.prototype.bark = function() {
return `${this.name} barks: Woof!`;
};

const rex = new Dog("Rex", "German Shepherd");

console.log(rex.constructor === Dog); // true
console.log(rex.speak()); // "Rex makes a sound"
console.log(rex.bark()); // "Rex barks: Woof!"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
tip

A reliable checklist for constructor function inheritance:

  1. Create child constructor, call parent with Parent.call(this, ...args)
  2. Set Child.prototype = Object.create(Parent.prototype)
  3. Fix Child.prototype.constructor = Child
  4. Add child-specific methods to Child.prototype

Always follow this exact order. If you add methods before step 2, they will be lost when you replace the prototype.

Verification Helper

When debugging prototype issues, this helper function can save time:

function verifyPrototypeSetup(Constructor, ParentConstructor) {
const instance = new Constructor();

const checks = {
"constructor is correct": instance.constructor === Constructor,
"instanceof works": instance instanceof Constructor,
"parent instanceof works": ParentConstructor
? instance instanceof ParentConstructor
: "N/A (no parent)",
"prototype chain is linked": ParentConstructor
? Object.getPrototypeOf(Constructor.prototype) === ParentConstructor.prototype
: Object.getPrototypeOf(Constructor.prototype) === Object.prototype,
"constructor is non-enumerable":
!Object.keys(Constructor.prototype).includes("constructor")
};

for (const [check, result] of Object.entries(checks)) {
const status = result === true ? "PASS" : result === "N/A (no parent)" ? "SKIP" : "FAIL";
console.log(` ${status}: ${check}`);
}
}

// Test a correctly set up hierarchy
function Vehicle(type) { this.type = type; }
Vehicle.prototype.describe = function() { return this.type; };

function Truck(brand) {
Vehicle.call(this, "truck");
this.brand = brand;
}
Truck.prototype = Object.create(Vehicle.prototype);
Truck.prototype.constructor = Truck;

verifyPrototypeSetup(Truck, Vehicle);
// PASS: constructor is correct
// PASS: instanceof works
// PASS: parent instanceof works
// PASS: prototype chain is linked
// FAIL: constructor is non-enumerable ← because we used simple assignment

The last check fails because Truck.prototype.constructor = Truck creates an enumerable property. Use Object.defineProperty() for a perfect setup.

Summary

The F.prototype property is the mechanism that connects constructor functions to the prototype chain. It is read once at new call time and determines the [[Prototype]] of every instance created by that constructor.

ConceptKey Point
F.prototypeA regular property on every function, used by new
When it is readOnly at the moment new F() is called
Default value{ constructor: F } with constructor non-enumerable
constructor propertyPoints back to the function, inherited by all instances
Setting up inheritanceChild.prototype = Object.create(Parent.prototype)
After inheritance setupAlways fix Child.prototype.constructor = Child
Adding methodsExtend the existing prototype, do not replace it
Object.assign()Safe way to add multiple methods without replacing prototype
Primitive prototypeIgnored by new, falls back to Object.prototype
null prototypeCreates objects with no prototype chain

Key rules to remember:

  • F.prototype is not the prototype of F itself. It is the prototype that will be assigned to objects created by new F()
  • The default prototype object has a constructor property pointing back to F. Preserve it.
  • When replacing prototype (especially for inheritance), always restore constructor
  • Prefer extending the existing prototype over replacing it: use F.prototype.method = ... or Object.assign(F.prototype, {...})
  • Changing F.prototype after objects are already created does not affect those existing objects
  • The class syntax (covered in a later module) handles all of this automatically, which is one of its biggest advantages