Constructor borrowing emerged as a popular technique to address the reference value sharing issues inhernet in prototype-based inheritance. Also referred to as object masquerading or classical inheritance simulation, this pattern involves invoking a parent constructor from within a child constructor.
Since functions are essentially objects that execute code in a specific context, we can leverage call() or apply() to run a parent constructor using a newly created child object as the context. Consider this implementation:
function BaseClass() {
this.tags = ["js", "html", "css"];
}
function DerivedClass() {
BaseClass.call(this);
}
let instance1 = new DerivedClass();
instance1.tags.push("react");
console.log(instance1.tags); // ["js","html","css","react"]
let instance2 = new DerivedClass();
console.log(instance2.tags); // ["js","html","css"]
The key line demonstrates constructor borrowing. By using call(), the BaseClass logic executes within the context of each new DerivedClass instance, ensuring every object receives its own tags property rather than sharing a prototype reference.
Parameter Transmission
A significant advantage over pure prototype chaining is the ability to pass arguments to the parent constructor during inheritance:
function Article(title) {
this.title = title;
}
function BlogPost() {
Article.call(this, "JavaScript Guide");
this.published = true;
}
let post = new BlogPost();
console.log(post.title); // "JavaScript Guide"
console.log(post.published); // true
Here, Article accepts a title parameter. The BlogPost constructor calls Article with a specific value, effectively creating a title property on the resulting instance. Additional child properties should be defined after the parent call to prevent overwriting.
Limitations of Constructor Borrowing
This pattern suffers from the same drawback as the constructor pattern itself: methods must be defined inside the constructor, preventing function reuse. Moreover, child instances cannot access methods defined on the parent prototype, forcing everything into constructor logic. These restrictions make constructor borrowing impractical as a standalone solution.
Hybrid Inheritence Approach
Composite inheritance (sometimes called pseudo-classical inheritance) merges prototype chaining with constructor borrowing, capturing the strengths of both. The strategy uses prototype chaining for shared methods while employing constructor borowing for instance-specific properties.
function ParentItem(label) {
this.label = label;
this.categories = ["core", "basic"];
}
ParentItem.prototype.display = function() {
console.log(this.label);
};
function ChildItem(label, value) {
ParentItem.call(this, label);
this.value = value;
}
ChildItem.prototype = new ParentItem();
ChildItem.prototype.showValue = function() {
console.log(this.value);
};
let item1 = new ChildItem("Component A", 42);
item1.categories.push("advanced");
console.log(item1.categories); // ["core","basic","advanced"]
item1.display(); // "Component A"
item1.showValue(); // 42
let item2 = new ChildItem("Component B", 84);
console.log(item2.categories); // ["core","basic"]
item2.display(); // "Component B"
item2.showValue(); // 84
ParentItem defines shared properties and a prototype method. ChildItem borrows the constructor to get its own label and categories, then inherits prototype methods by reassigning its prototype to a ParentItem instance. This enables both method sharing and property isolation.
This hybrid approach successfully resolves the core issues of both prototype chaining and constructor borrowing. The pattern maintains full compatibility with JavaScript's native object identification mechanisms like instanceof and isPrototypeOf(), while enabling efficient code reuse through prototype methods and proper encapsulation through instance properties.