Brendan Eich came up with the first draft of the JS language specification in little more than a week. He tried mixing the best parts of the imperative and functional programming, having as resulting a very flexible language. Combine this flexibility with an incredible wide adoption, and you’ll understand all the debate around both basic features and high level frameworks.
To put things into perspective, we’ll go through the four core concepts of OOP, and figure out how these apply to the most popular language on earth.
Objects & Classes
Up until 2015, JS had a simple constructor mechanism to create objects:
You’ll notice that a constructor is just a plain function. Everything attached to
this will be passed to the resulting object instance, and calling
Pirate() with the
new keyword enables the function’s special constructor powers. Constructor functions were the established technique to build objects, until ES2015 classes were introduced:
class keyword is mainly syntactical sugar, since Pirate is still a function at its core. However, you’ll find out that the JS Classes specification comes with some new features and rules. For instance, you can invoke
class Pirate only using the
For an in depth look at ES2015 Classes, spare a few minutes to check Axel Rauschmayer’s detailed article on the subject.
The real syntactic sugar is brought by object literals which simplify the object creation process, but they have their drawbacks when it comes to code reusability:
Whichever technique you prefer, you’ll find out that neither the constructor functions nor the classes guarantee a final structure on the resulting object. Concepts such as C#’s partial classes or Ruby’s open classes offer a lot more flexibility than a constraining class model such as Java’s. However, JS allows programmers to alter object structures by adding state and behavior directly to the instance, even at run time. This leads to scenarios in which objects sharing the same class may have little structure in common, making the JS objects conceptually alike to dictionaries.
First, let’s take a look at the properties of two different
jack instances, one created through a constructor function and one created through a constructor class:
Even though the two used techniques should have alike results, the instance obtained through invoking a constructor function contains the
say property while the class instance doesn’t. This happens because methods defined in
class Pirate are attached to the
Pirate.prototype object by default. In contrast,
function Pirate attaches its methods to the resulting instance itself.
hasOwnProperty helps differentiating between instance and prototype properties. It is easy to figure out that having the
say method attached to each of the
Pirate objects is not a good behavior reusability decision. To fix this, let’s update the constructor function to make sure that any
Pirate method is instead attached to
Pirate.prototype. This change is straight forward, since all functions have a
prototype property linking to an object. This object is passed to all resulting instances when the function is called as a constructor:
This small change has actually a great impact since all objects will have access to the
say method through prototypal inheritance from now on. Furthermore, any new method added to the prototype, even at run time, will be automatically visible in all the
Let’s analyze a more detailed prototype based inheritance example:
The previous code snippet shows a simple scenario in which
Actor’s behavior by sharing its prototype object. This results in a
jack instance which has access to methods from both constructors. Under the hood, this access is resolved through the prototype chain. Each instance has direct access to the prototype object associated to the constructor used to create that instance. You can check this through the instance’s
__proto__ accessor property:
This prototype chain backs the lookup process used to access instance properties. Every time a property is accessed, the prototype chain is traversed bottom to top in search for the specific property. Applied to our current example, since the
jack instance itself doesn’t have the
greeting method, the engine will look next in
Pirate’s prototype method - with no result. Next,
Actor’s prototype will be inspected and its
greeting method will be used.
It should be obvious that trying to access properties deep in the prototype chain or non existing properties - when the entire chain will be traversed with no result - could cause performance issues in an ill thought architecture. Furthermore, while working with function prototypes is encouraged, the
__proto__ property is much more sensitive business.
Another special instance property is the
constructor property which inherited from top of the chain’s
Object.prototype. When creating the object, the constructor also links itself to the instance. In our example, since inheritance may break this link, we made sure that it is still valid through:
Pirate.prototype.constructor = Pirate;. Even though this is not mandatory, it could save you a lot of headache with object copying and
With the advent ES2015, our example could be refactored as follows:
Granted, this approach looks less hacky, mainly because it hides all the prototype complexity. However, keep in mind that hiding complexity is all that the ES2015 class brings to the table. Under to hood, things are still the same, and knowing all the prototypal inheritance details we went through is a must in order to make sense of the subject.
class notion last year would confuse programmers that are new to JS even more, since they may have a specific set of expectations and ideas from other object oriented languages.