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:
- JavaScript creates a new empty object
{} - It sets that object's
[[Prototype]]toAnimal.prototype - It calls
Animal()withthispointing to the new object - The constructor assigns
this.name = "Rex" - 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.
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
Animalconstructor without proper arguments, which might cause errors or set invalid properties - It creates instance properties (
namewould beundefined) 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
A reliable checklist for constructor function inheritance:
- Create child constructor, call parent with
Parent.call(this, ...args) - Set
Child.prototype = Object.create(Parent.prototype) - Fix
Child.prototype.constructor = Child - 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.
| Concept | Key Point |
|---|---|
F.prototype | A regular property on every function, used by new |
| When it is read | Only at the moment new F() is called |
| Default value | { constructor: F } with constructor non-enumerable |
constructor property | Points back to the function, inherited by all instances |
| Setting up inheritance | Child.prototype = Object.create(Parent.prototype) |
| After inheritance setup | Always fix Child.prototype.constructor = Child |
| Adding methods | Extend the existing prototype, do not replace it |
Object.assign() | Safe way to add multiple methods without replacing prototype |
Primitive prototype | Ignored by new, falls back to Object.prototype |
null prototype | Creates objects with no prototype chain |
Key rules to remember:
F.prototypeis not the prototype ofFitself. It is the prototype that will be assigned to objects created bynew F()- The default
prototypeobject has aconstructorproperty pointing back toF. Preserve it. - When replacing
prototype(especially for inheritance), always restoreconstructor - Prefer extending the existing prototype over replacing it: use
F.prototype.method = ...orObject.assign(F.prototype, {...}) - Changing
F.prototypeafter objects are already created does not affect those existing objects - The
classsyntax (covered in a later module) handles all of this automatically, which is one of its biggest advantages