Search code examples
javascriptecmascript-6prototype

Is the following explanation of `prototypes` in Javascript valid?


I have been trying to learn about prototypes since I needed quite a bit of clarity on them. I have been using this MDN article and its related articles for reference.

After some reading, I got a bit of clarity on prototypes and tried putting them in my own words and would like to know if it is right and would love to know if and where I am wrong

START OF EXPLANATION

Every object is created using a constructor function of some sort. Say, we create an object as follows.

let obj = new Object();

Here, Object is the constructor function. The thing about functions is that, all of them (including non-constructor functions) have a property called prototype on them. This prototype property defines what will be the prototype of any object that is created using the new keyword and said constructor function. You can check the prototype property as follows:

console.log(Object.prototype);

The above piece of code will return an object with a bunch of methods that any object created using new Object() can use.

In the above example, if the wording is too confusing, you can replace all occurrences of Object with any other constructor function such as Array, Date or even custom constructor functions such as Person or something else that you may have defined.

END OF EXPLANATION

Is my understanding right? If not, can you point me where I went wrong?


Solution

  • Is my understanding right? If not, can you point me where I went wrong?

    In big picture terms, yes, your understanding is mostly correct, but the explanation is incomplete, and there are some specifics that are incorrect.

    Every object is created using a constructor function of some sort.

    That's not quite correct, JavaScript also has literal forms ({} [object], [] [array], and // [regular expression]) that create objects without using a constructor function. Those forms assign Object.prototype, Array.prototype, and RegExp.prototype (respectively) to the objects they create, even though the constructor itself isn't invoked.

    There are also other ways of creating objects that don't go through constructor functions at all. For instance, there's Object.create, which creates an object and assigns the prototype you supply to it:

    const p = {};
    const obj = Object.create(p);
    console.log(Object.getPrototypeOf(obj) === p); // true

    (There are also more obscure ways of creating objects through implicit conversion.) You can also change the prototype of an existing object by using Object.setPrototypeOf.

    The thing about functions is that, all of them (including non-constructor functions) have a property called prototype on them.

    Not quite, arrow functions and class methods do not have a prototype property and cannot be used as constructors:

    const arrow = () => {};
    class X {
        method() {
        }
        static staticMethod() {
        }
    }
    console.log("prototype" in arrow);              // false
    console.log("prototype" in X.prototype.method); // false
    console.log("prototype" in X.staticMethod);     // false

    This prototype property defines what will be the prototype of any object that is created using the new keyword and said constructor function.

    Correct. (Constructor functions can mess with what they return, but that's the usual, standard behavior.)

    At this point in an explanation I'd probably point out the distinction between the prototype property on functions and the prototype of an object. Beginners sometimes think setting a prototype property on an object will change its prototype; it doesn't, that name is only significant on functions, and it's not the function's prototype, it's just a property that (as you said) will be used to assign the prototype of an object created using new with that function. The prototype of an object is held in an internal field of the object called [[Prototype]]. That field isn't directly accessible, but you can access it via Object.getPrototypeOf and change it via Object.setPrototypeOf (you can also use the deprecated __proto__ accessor property, which is just a wrapper around those functions — but don't use __proto__, use the functions directly).

    But aside from all that, there's a big unanswered question in your explanation: What are prototypes for? What do they do? Why have them?

    The answer is that they provide JavaScript's inheritance mechanism. When you get the value of a property on an object and the object doesn't have a property of its own with the given key, the JavaScript engine looks at the object's prototype to see if it has the property (and the prototype of the prototype, and so on through the chain):

    const parent = {
        a: "a property on base",
    };
    const child = Object.create(parent);
    child.b = "a property on child";
    const grandChild = Object.create(child);
    grandChild.c = "a property on grandChild";
    
    console.log(grandChild.a); // "a property on base"
    console.log(grandChild.b); // "a property on child"
    console.log(grandChild.c); // "a property on grandChild"
    
    const hasOwn =
        Object.hasOwn || // Fairly new, ES2022
        Function.prototype.call.bind(Object.prototype.hasOwnProperty);
    
    console.log(`hasOwn(grandChild, "a")? ${hasOwn(grandChild, "a")}`); // false
    console.log(`hasOwn(grandChild, "b")? ${hasOwn(grandChild, "b")}`); // false
    console.log(`hasOwn(grandChild, "c")? ${hasOwn(grandChild, "c")}`); // true

    Those example property values are strings, but this is widely used where the property values are functions, providing a means of inheriting methods from parent objects.

    The property access process is asymmetrical, though; it only works as described above for getting a property's value. If you set a property's value on an object, it always sets it on the object itself, not on its prototype:

    const parent = {
        prop: "parent",
    };
    const child = Object.create(parent);
    
    const hasOwn =
        Object.hasOwn || // Fairly new, ES2022
        Function.prototype.call.bind(Object.prototype.hasOwnProperty);
    
    console.log(`[Before] child.prop: ${child.prop}`);
    // => "[Before] child.prop: parent"
    console.log(`[Before] hasOwn(child, "prop")? ${hasOwn(child, "prop")}`);
    // => "[Before] hasOwn(child, "prop")? false"
    
    child.prop = "child";
    
    console.log(`[After]  child.prop: ${child.prop}`);
    // => "child.prop: child"
    console.log(`[After]  hasOwn(child, "prop")? ${hasOwn(child, "prop")}`);
    // => "[After]  hasOwn(child, "prop")? true"

    (This difference between getting and setting the property value applies to data properties [the kind we mostly create]; accessor properties work differently because getting and setting the property result in function calls, and the accessor's setter function can do whatever the author wants it to do.)