The most effective way of declaring objects
How do we declare an object in JavaScript? If we need a namespace, we can simply use an object literal. But when we need an object type, we need to think twice about what approach to take, as it affects the maintainability of our object-oriented code.
Classical approach
We can create a constructor function and chain the members to its context:
"use strict"; /** * @class */ var Constructor = function(){ /** * @type {String} * @public */ this.bar = "bar"; /** * @public * @returns {String} */ this.foo = function() { return this.bar; }; }, /** @type Constructor */ instance = new Constructor(); console.log( instance.foo() ); // bar console.log( instance instanceof Constructor ); // true
We can also assign the members to the constructor prototype. The result will be the same as follows:
"use strict"; /** * @class */ var Constructor = function(){}, instance; /** * @type {String} * @public */ Constructor.prototype.bar = "bar"; /** * @public * @returns {String} */ Constructor.prototype.foo = function() { return this.bar; }; /** @type Constructor */ instance = new Constructor(); console.log( instance.foo() ); // bar console.log( instance instanceof Constructor ); // true
In the first case, we have the object structure within the constructor function body mixed with the construction logic. In the second case by repeating Constructor.prototype
, we violate the
Do Not Repeat Yourself (DRY) principle.
Approach with the private state
So how can we do it otherwise? We can return an object literal by the constructor function:
"use strict"; /** * @class */ var Constructor = function(){ /** * @type {String} * @private */ var baz = "baz"; return { /** * @type {String} * @public */ bar: "bar", /** * @public * @returns {String} */ foo: function() { return this.bar + " " + baz; } }; }, /** @type Constructor */ instance = new Constructor(); console.log( instance.foo() ); // bar baz console.log( instance.hasOwnProperty( "baz") ); // false console.log( Constructor.prototype.hasOwnProperty( "baz") ); // false console.log( instance instanceof Constructor ); // false
The advantage of this approach is that any variables declared in the scope of the constructor are in the same closure as the returned object, and therefore, available through the object. We can consider such variables as private members. The bad news is that we will lose the constructor prototype. When a constructor returns an object during instantiation, this object becomes the result of a whole new expression.
Inheritance with the prototype chain
What about inheritance? The classical approach would be to make the subtype prototype an instance of supertype:
"use strict"; /** * @class */ var SuperType = function(){ /** * @type {String} * @public */ this.foo = "foo"; }, /** * @class */ Constructor = function(){ /** * @type {String} * @public */ this.bar = "bar"; }, /** @type Constructor */ instance; Constructor.prototype = new SuperType(); Constructor.prototype.constructor = Constructor; instance = new Constructor(); console.log( instance.bar ); // bar console.log( instance.foo ); // foo console.log( instance instanceof Constructor ); // true console.log( instance instanceof SuperType ); // true
You may run into some code, where for instantiation Object.create
is used instead of the new operator. Here you have to know the difference between the two. Object.create
takes an object as an argument and creates a new one with the passed object as a prototype. In some ways, this reminds us of cloning. Examine this, you declare an object literal (proto) and create a new object (instance) with Object.create
based on the first one. Whatever changes you do now on the newly created object, they won't be reflected on the original (proto). But if you change a property of the original, you will find the property changed in its derivative (instance):
"use strict"; var proto = { bar: "bar", foo: "foo" }, instance = Object.create( proto ); proto.bar = "qux", instance.foo = "baz"; console.log( instance ); // { foo="baz", bar="qux"} console.log( proto ); // { bar="qux", foo="foo"}
Inheriting from prototype with Object.create
In contrast to the new operator, Object.create
does not invoke the constructor. So when we use it to populate a subtype prototype, we are losing all the logic located in a supertype
constructor. This way, the supertype
constructor is never called:
// ... SuperType.prototype.baz = "baz"; Constructor.prototype = Object.create( SuperType.prototype ); Constructor.prototype.constructor = Constructor; instance = new Constructor(); console.log( instance.bar ); // bar console.log( instance.baz ); // baz console.log( instance.hasOwnProperty( "foo" ) ); // false console.log( instance instanceof Constructor ); // true console.log( instance instanceof SuperType ); // true
Inheriting from prototype with Object.assign
When looking for an optimal structure, I would like to declare members via an object literal, but still have the link to the prototype. Many third-party projects leverage a custom function (extend) that merge the structure object literal into the constructor prototype. Actually, ES6 provides an Object.assign
native method. We can use it as follows:
"use strict"; /** * @class */ var SuperType = function(){ /** * @type {String} * @public */ this.foo = "foo"; }, /** * @class */ Constructor = function(){ /** * @type {String} * @public */ this.bar = "bar"; }, /** @type Constructor */ instance; Object.assign( Constructor.prototype = new SuperType(), { baz: "baz" }); instance = new Constructor(); console.log( instance.bar ); // bar console.log( instance.foo ); // foo console.log( instance.baz ); // baz console.log( instance instanceof Constructor ); // true console.log( instance instanceof SuperType ); // true
This looks almost as required, except there is one inconvenience. Object.assign
simply assigns the values of source objects to the target ones regardless of their type. So if you have a source property with an object (for instance, an Object
or Array
instance), the target object receives a reference instead of a value. So you have to reset manually any object properties during initialization.
Approach with ExtendClass
ExtendClass, proposed by Simon Boudrias, is a solution that seems flawless (https://github.com/SBoudrias/class-extend). His little library exposes the Base
constructor with the
extend static method. We use this method to extend this pseudo-class and any of its derivatives:
"use strict"; /** * @class */ var SuperType = Base.extend({ /** * @pulic * @returns {String} */ foo: function(){ return "foo public"; }, /** * @constructs SuperType */ constructor: function () {} }), /** * @class */ Constructor = SuperType.extend({ /** * @pulic * @returns {String} */ bar: function(){ return "bar public"; } }, { /** * @static * @returns {String} */ bar: function(){ return "bar static"; } }), /** @type Constructor */ instance = new Constructor(); console.log( instance.foo() ); // foo public console.log( instance.bar() ); // bar public console.log( Constructor.bar() ); // bar static console.log( instance instanceof Constructor ); // true console.log( instance instanceof SuperType ); // true
Classes in ES6
TC39 (the EcmaScript working group) is pretty aware of the problem, so the new language specification provides extra syntax to structure object types:
"use strict"; class AbstractClass { constructor() { this.foo = "foo"; } } class ConcreteClass extends AbstractClass { constructor() { super(); this.bar = "bar"; } baz() { return "baz"; } } let instance = new ConcreteClass(); console.log( instance.bar ); // bar console.log( instance.foo ); // foo console.log( instance.baz() ); // baz console.log( instance instanceof ConcreteClass ); // true console.log( instance instanceof AbstractClass ); // true
The syntax looks class-based, but in fact this a syntactic sugar over existing prototypes. You can check with the type of ConcreteClass
, and it will give you function because ConcreteClass
is a canonical constructor. So we don't need any trick to extend supertypes
, no trick to refer the supertype
constructor from subtype, and we have a clean readable structure. However, we cannot assign properties the same C-like way as we do now with methods. This is still in discussion for ES7 (https://esdiscuss.org/topic/es7-property-initializers). Besides this, we can declare a class's static methods straight in its body:
class Bar { static foo() { return "static method"; } baz() { return "prototype method"; } } let instance = new Bar(); console.log( instance.baz() ); // prototype method console.log( Bar.foo()) ); // static method
Actually, there are many in the JavaScript community who consider the new syntax as a deviation from the prototypical OOP approach. On the other hand, the ES6 classes are backwards compatible with most of the existing code. Subclasses are now supported by the language and no extra libraries are required for inheritance. And what I personally like the most is that this syntax allows us to make the code cleaner and more maintainable.