A class creates objects with a shared prototype
A class is a convenient way to describe how to create related objects. When you call a class with new, JavaScript makes an instance and runs the constructor with that instance as this. Properties assigned in the constructor usually belong to each instance. Methods declared in the class body usually live on the class’s prototype, where instances can share them.
The examples here use standard JavaScript and can be pasted into a modern browser’s developer console or a recent Node.js session. No libraries or page markup are required. Start with: class Counter { constructor(start = 0) { this.value = start; } increment() { this.value += 1; return this.value; } } const first = new Counter(); const second = new Counter(10);
The two counters have separate value properties: first.value is 0, while second.value is 10. They do not need separate copies of increment. Both can reach the same method through Counter.prototype. Try first.increment === second.increment; the result is true. Calling either method still updates the appropriate counter because a method call such as first.increment() supplies first as this.
How property lookup follows the chain
When JavaScript evaluates first.increment, it checks first for a property named increment. It does not find one there, so it checks first’s prototype, Counter.prototype. That object contains the method. If the property were not there either, lookup would continue through subsequent prototypes until it found the property or reached the end of the chain. A lookup that reaches the end returns undefined.
You can inspect this relationship with Object.getPrototypeOf(first) === Counter.prototype, which evaluates to true. You can also distinguish an instance’s own properties from inherited ones: Object.hasOwn(first, 'value') is true, while Object.hasOwn(first, 'increment') is false. These checks describe where a property was found; an inherited method is still available to call.
The chain is also why names can be shadowed. After first.increment = function () { return 'paused'; }, first has its own increment property. first.increment() now returns 'paused', but second.increment() still uses the method on Counter.prototype. Deleting first.increment removes the own property and makes the prototype method visible to first again. Shadowing changes lookup for that instance; it does not edit the shared method.
Sharing does not mean sharing all state
The Counter method is shared, but each counter’s value is stored on its own instance. This is a useful default: one method implementation can operate on many objects without making their changing values the same. Inside increment, this.value reads and writes a property of the receiver used for that call.
Take care with mutable data placed on a prototype. For example, if several objects find the same array through their prototype, pushing an item into that array affects what all of them see. For independent lists, initialize an array in the constructor instead: this.items = []. Then each new instance receives its own array. The same principle applies to other mutable objects that should represent per-instance state.
A method also depends on how it is called. If you save const step = first.increment and later call step(), that call no longer supplies first as the receiver. Class methods are not automatically bound to their instances. When passing a method as a callback, you can use first.increment.bind(first) or wrap the call in a function such as () => first.increment().
Extending a class adds another prototype link
Inheritance lets a class reuse behavior through a longer chain. Add this after the Counter definition: class LabeledCounter extends Counter { constructor(label, start = 0) { super(start); this.label = label; } describe() { return this.label + ': ' + this.value; } } const visits = new LabeledCounter('Visits', 2);
Now visits.describe() finds describe on LabeledCounter.prototype, while visits.increment() finds increment farther along, on Counter.prototype. The constructor calls super(start) to run the parent class constructor before assigning this.label. You can check the prototype links directly: Object.getPrototypeOf(visits) === LabeledCounter.prototype and Object.getPrototypeOf(LabeledCounter.prototype) === Counter.prototype are both true.
Extending a class can make a genuine “is a” relationship easy to express, but it is not necessary for every form of reuse. If two components merely need similar behavior without sharing an identity or lifecycle, a regular function passed the relevant data may be simpler than an inheritance chain.
Use the prototype model when debugging
When a property has an unexpected value, ask two questions: which object owns the property, and which object supplied this for the call? Object.hasOwn and Object.getPrototypeOf help answer the first. Looking at the call expression helps answer the second. This approach is often more useful than assuming everything written inside a class belongs directly to each instance.
Remember the practical distinction: constructor assignments normally create per-instance properties; class methods normally sit on a shared prototype; property lookup connects the two. Once you can trace that lookup, class behavior becomes easier to predict, whether you are writing a small utility object or debugging a larger interface.



