Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Dynamic Behavior and Key Limitations of JavaScript Prototypes

Tech 2

Because property resolution along the prototype chain happens dynamically at runtime, any change made to a prototype object will immediately be visible to all existing instances, even for instances that were created before the modification. For example:

// `friend` is already an existing Person instance created earlier
Person.prototype.sayHi = function() {
  console.log("hi");
};
friend.sayHi(); // Outputs "hi", works correctly

This behavior is possible because the connection between an instance and its prototype is loose and dynamic. When accessing friend.sayHi(), the JavaScript engine first checks the instance object itself for the sayHi property. If it is not found, the search continues to the linked prototype object. Since the instance only stores a pointer to the prototype, not a cached copy of the prototype's contents, the new method is found and executed without issues.

Adding new members to an existing prototype works seamlessly, but this is very different from completely replacing the constructor's entire prototype object. The internal [[Prototype]] pointer on an instance is set once when the constructor is called to create the instance, and this pointer does not update automatically when the constructor's prototype property is reassigned to a new object. Rewriting the entire prototype breaks the connection betweeen the constructor and the original prototype, but all existing instances still reference the original prototype they received when created. Instances only track a reference to their prototype, not to the constructor that created them, as shown in the example below:

function Person() {}
let friend = new Person();

Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName() {
    console.log(this.name);
  }
};

friend.sayName(); // Throws an error

Here, the friend instance is created before Person.prototype is replaced with a new object. The call to friend.sayName() fails because friend still points to the original default prototype that existed when it was created, which does not define the sayName method.

Prototypes for Native JavaScript Objects

The prototype pattern is not only used for custom constructor types — it is also the foundational pattern for all native JavaScript reference types. Every native reference type constructor (including Object, Array, String, and others) defines all of its default instance methods directly on its prototype. For example, the sort() method available on all array instances is defined on Array.prototype, and the substring() method for strings is defined on String.prototype:

console.log(typeof String.prototype.substring); // "function"

Native prototypes can be used to get references to default methods, or extended with new methods just like custom object prototypes. Adding a new method to a native prototype makes it automatically available to all instances of that type. For example, the code below adds a startsWith() method to the String primitive wrapper type:

String.prototype.startsWith = function(text) {
  return this.indexOf(text) === 0;
};

let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true

If the input text matches the start of the calling string, this method returns true. Since the method is added to String.prototype, it can be used by any string in the runtime. When accessing the method on a primitive string value, JavaScript automatically creates a String wrapper object for the primitive under the hood, allowing the method to be found and called correctly.

Common Problems with the Prototype Pattern

The prototype pattern does have notable drawbacks. First, it reduces flexibility for passing initialization parameters to constructors, which means all instances get the same default prototype property values. This is inconvenient, but it is not the biggest issue.

The main problem with the pattern comes from the shared nature of prototype properties. All properties on the prototype are shared across all instances, wich works well for methods and primitive values. For primitive values, you can easily shadow a prototype property by adding a same-named property directly to the instance. The real issue arises when the prototype holds properties that are reference values:

function Person() {}
Person.prototype = {
  constructor: Person,
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  friends: ["Shelby", "Court"],
  sayName() {
    console.log(this.name);
  }
};

let firstPerson = new Person();
let secondPerson = new Person();

firstPerson.friends.push("Van");

console.log(firstPerson.friends); // ["Shelby", "Court", "Van"]
console.log(secondPerson.friends); // ["Shelby", "Court", "Van"]
console.log(firstPerson.friends === secondPerson.friends); // true

In this example, Person.prototype has a friends property that stores an array of strings. After creating two Person instances, we push a new name to firstPerson.friends. Because the array lives on the prototype, not on the first instance, the change is automatically reflected in secondPerson.friends, which references the exact same array. While shared state can be intentional in some cases, most applications expect each instance to have its own copy of mutable reference properties. This is the primary reason that the standalone prototype pattern is rarely used in production development.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.