The “JS101” section in dailyjs is always a valuable read. No matter how well I think I understand a topic, I always discover subtle details or perspectives that I hadn’t considered before, prompting me to reflect further. Reading their entry about prototypes was no different.
Prototypes were a source of confusion for me for quite a while. The concept of a prototype chain for method and property lookup, while elegant and powerful, took time for me to fully grasp. It’s crucial to remember that this chain (often called the inheritance chain) hinges on the [[Prototype]] property (sometimes accessible via the non-standard __proto__ property). The prototype property, found solely in Function objects, isn’t part of this chain but plays a role in its construction. Functions, therefore, possess both a prototype (referencing an object whose constructor points back to the function) and a [[Prototype]] that defines their “inheritance chain”. This prototype chain for functions includes two key Objects: Function.prototype and Object.prototype. (function(){}).__proto__.__proto__ === Object.prototype; I appreciate the article’s use of the term “internal prototype” to differentiate [[Prototype]] from prototype.
As mentioned, __proto__ is non-standard, but if we need to access the [[Prototype]] object, we can do it using ES5’s Object.getPrototypeOf
A critical point is that Object.prototype.__proto__ is null, marking the end of the prototype chain for the interpreter. Previously, this was the sole object without a prototype chain. However, with the introduction of Object.create(), we can now create objects where their [[Prototype]] set to null. I already mentioned an odd JS behavior related to this in this previous post
The daily.js article raises an interesting point regarding Object.create as the only way to create an object without a prototype chain (where [[Prototype]] is null). One might assume that setting constructorFunction.prototype to null would achieve the same result. However, this doesn’t lead to a null [[Prototype]] when creating an instance; instead, it points to Object.prototype. This appears to be one of those edge cases clarified somewhere in the ECMAScript specification.
To solidify our understanding of prototype and [[Prototype]], we can perform some checks (all of which should evaluate to true):
Given: function f1(){};
- f1.prototype.__proto__ === Object.prototype; (A function’s prototype is simply a regular object, so its __proto__ is Object.prototype)
- Function.prototype.__proto__ === Object.prototype;
- f1.__proto__ === Function.prototype; (Note that functions are instances of Function)
However, certain cases don’t follow this pattern and behave as they do because the standard dictates it. Here are some examples:
- Function is an unusual object. While it’s an instance of Object, its __proto__ is set to an empty function. Therefore: Function.__proto__ === Object.prototype; is false Furthermore, I came across this intriguing tidbit: Function.prototype is itself a function that always returns undefined and accepts any number of arguments. The reason for this might be consistency—every built-in constructor’s prototype follows this pattern. Number.prototype is a Number object, Array.prototype is an Array object, RegExp.prototype is a RegExp object, and so on.
- In practice, we shouldn’t concern ourselves too much with Object.__proto__ and Function.__proto__, as I struggle to think of any scenario where they’d be used in an inheritance chain.
This question in SO provides further insightful observations on these matters.
When discussing prototype chains, we should mention the instanceof operator: ob instanceof MyFunction; which checks if the specified constructor function exists within the prototype chain of the given object (by traversing the chain and comparing each object to MyFunction.prototype). We can accomplish the same using Object.isPrototypeOf, like so: MyFunction.prototype.isPrototypeOf(ob);
To round off this post, let’s include this exceptional diagram (found here) that perfectly illustrates these concepts:
Here’s a summary (also from from there) of what occurs when a constructor call creates an object (e.g., var f = new Foo();):
- The interpreter generates a new, empty object.
- The interpreter sets this new object’s underlying prototype to Foo.prototype.
- The interpreter invokes Foo in a way that makes this refer to the newly created object.
- When Foo finishes execution, if it doesn’t return a value (or returns a non-object), the result of the new expression is the object created in step 1. (If Foo returns an object, that object is used instead; this is an advanced concept most developers won’t encounter regularly.)