How does JavaScript .prototype work?

ghz 1years ago ⋅ 2004 views

Question

I'm not that into dynamic programming languages but I've written my fair share of JavaScript code. I never really got my head around this prototype-based programming, does any one know how this works?

var obj = new Object();
obj.prototype.test = function() { alert('Hello?'); };
var obj2 = new obj();
obj2.test();

I remember a lot discussion I had with people a while back (I'm not exactly sure what I'm doing) but as I understand it, there's no concept of a class. It's just an object, and instances of those objects are clones of the original, right?

But what is the exact purpose of this ".prototype" property in JavaScript? How does it relate to instantiating objects?

Update: correct way

var obj = new Object(); // not a functional object
obj.prototype.test = function() { alert('Hello?'); }; // this is wrong!

function MyObject() {} // a first class functional object
MyObject.prototype.test = function() { alert('OK'); } // OK

Also these slides really helped a lot.


Answer

Every JavaScript object [has an internal "slot"](https://www.ecma- international.org/ecma-262/10.0/index.html#sec-ordinary-object-internal- methods-and-internal-slots) called [[Prototype]] whose value is either null or an object. You can think of a slot as a property on an object, internal to the JavaScript engine, hidden from the code you write. The square brackets around [[Prototype]] are deliberate, and are an ECMAScript specification convention to denote internal slots.

The value pointed at by the [[Prototype]] of an object, is colloquially known as "the prototype of that object."

If you access a property via the dot (obj.propName) or bracket (obj['propName']) notation, and the object does not directly have such a property (ie. an own property , checkable via obj.hasOwnProperty('propName')), the runtime looks for a property with that name on the object referenced by the [[Prototype]] instead. If the [[Prototype]] also does not have such a property, its [[Prototype]] is checked in turn, and so on. In this way, the original object's prototype chain is walked until a match is found, or its end is reached. At the top of the prototype chain is the null value.

Modern JavaScript implementations allow read and/or write access to the [[Prototype]] in the following ways:

  1. The new operator (configures the prototype chain on the default object returned from a constructor function),
  2. The extends keyword (configures the prototype chain when using the class syntax),
  3. Object.create will set the supplied argument as the [[Prototype]] of the resulting object,
  4. Object.getPrototypeOf and Object.setPrototypeOf (get/set the [[Prototype]] after object creation), and
  5. The standardized accessor (ie. getter/setter) property named __proto__ (similar to 4.)

Object.getPrototypeOf and Object.setPrototypeOf are preferred over __proto__, in part because the behavior of o.__proto__ is unusual when an object has a prototype of null.

An object's [[Prototype]] is initially set during object creation.

If you create a new object via new Func(), the object's [[Prototype]] will, by default, be set to the object referenced by Func.prototype.

Note that, therefore, all classes, and all functions that can be used with thenew operator, have a property named .prototype in addition to their own [[Prototype]] internal slot. This dual use of the word "prototype" is the source of endless confusion amongst newcomers to the language.

Using new with constructor functions allows us to simulate classical inheritance in JavaScript; although JavaScript's inheritance system is - as we have seen - prototypical, and not class-based.

Prior to the introduction of class syntax to JavaScript, constructor functions were the only way to simulate classes. We can think of properties of the object referenced by the constructor function's .prototype property as shared members; ie. members which are the same for each instance. In class- based systems, methods are implemented the same way for each instance, so methods are conceptually added to the .prototype property; an object's fields, however, are instance-specific and are therefore added to the object itself during construction.

Without the class syntax, developers had to manually configure the prototype chain to achieve similar functionality to classical inheritance. This led to a preponderance of different ways to achieve this.

Here's one way:

function Child() {}
function Parent() {}
Parent.prototype.inheritedMethod = function () { return 'this is inherited' }

function inherit(child, parent) {
  child.prototype = Object.create(parent.prototype)
  child.prototype.constructor = child
  return child;
}

Child = inherit(Child, Parent)
const o = new Child
console.log(o.inheritedMethod()) // 'this is inherited'

...and here's another way:

function Child() {}
function Parent() {}
Parent.prototype.inheritedMethod = function () { return 'this is inherited' }

function inherit(child, parent) {
    function tmp() {}
    tmp.prototype = parent.prototype
    const proto = new tmp()
    proto.constructor = child
    child.prototype = proto
    return child
}

Child = inherit(Child, Parent)
const o = new Child
console.log(o.inheritedMethod()) // 'this is inherited'

The class syntax introduced in ES2015 simplifies things, by providing extends as the "one true way" to configure the prototype chain in order to simulate classical inheritance in JavaScript.

So, similar to the code above, if you use the class syntax to create a new object like so:

class Parent { inheritedMethod() { return 'this is inherited' } }
class Child extends Parent {}

const o = new Child
console.log(o.inheritedMethod()) // 'this is inherited'

...the resulting object's [[Prototype]] will be set to an instance of Parent, whose [[Prototype]], in turn, is Parent.prototype.

Finally, if you create a new object via Object.create(foo), the resulting object's [[Prototype]] will be set to foo.