When I started with JS, I really had poor understanding of things behind objects. Still far from being an expert in the area, some fog has disappeared. In strong OOP languages, we usually compose classes or use inheritance. in JS, especially before ES6, we have just objects which we can extend via mixins. Here JS can be quite confusing for traditional OOP developers. To understand JavaScript mixins, it is needed to understand objects and their prototypes.
Objects
An object in JS is some composite for a bunch of key-value pairs sometimes called properties including reference into “upper” objects called prototypes. We can imagine this like some bunch of added behavior and properties accessible. From OOP perspective, it is like super-class.
To create a new object with an undefined prototype we use Object.create function.
var objectWithUndefinedPrototype = Object.create(null);
To add new property name and value to this object we have multiple ways how-to do it, most simple is “dot” notion:
objectWithNotDefinedPrototype.name = 'John';
Often we create objects with basic “Object” prototype. For this we can use simplified construction:
var objectWithObjectPrototype = {};
// which is a syntax suggar for
var objectWithObjectPrototype = Object.create(Object);
More about prototypes
Prototype can be understood also as a pointer to another object. It allows property chain lookup. So we can utilize content defined in object prototype. Property lookup goes from used object up to top prototype until it’s found if there is any in the chain. The lookup is stopped when a property is found or there is no upper prototype (ie. it’s null) and then an undefined value is returned.
More about properties
As we can simply create property just by simple notion <object>.<property_name> = <property_value>. There is more detailed way by using Object.defineProperty function.
var dog = Object.create(null);
Object.defineProperty(dog, 'name', {
value: "Woofy",
writable: true,
enumerable: true,
configurable: true
});
Define properties with common defaults
We can create a function to simplify property definition.
var config = {
writable: true,
enumerable: true,
configurable: true
};var defineProperty = function(obj, name, value) {
config.value = value;
Object.defineProperty(obj, name, config);
}var person = Object.create(null);
defineProperty(dog, 'name', 'Woolfy');
defineProperty(dog, 'age', 7);
Create object hierarchy
Prototypes are basic stones for creating object hierarchies similar to inheritance in OOP languages. Like this, we can create object “circle” which can utilize properties from object “shape”. If we pass “shape” as parameter to Object.create parameter, circle will have shape as its prototype.
var shape = Object.create(null);
defineProperty(shape, ‘color’, ‘black’);var circle = Object.create(shape);
defineProperty(circle, 'radius', 1);// now we can check
console.log(circle.color); // black
console.log(circle.radius); // 1
Prototypes with __proto__
There is a special properties called __proto__. We can create the same with this property
var shape = {};
shape.__proto__ = null;
shape.color = 'black';var circle = {};
circle.radius = 1;
circle.__proto__ = shape;// now we can check
console.log(circle.color); // black
console.log(circle.radius) // 1
Note that shape is not the same as shape from the previous example as its prototype is “Object” here.
Construction below is the same and so {} is a syntax suggar for creating objects with Object prototype.
var myObj = {};
var myObj = Object.create(Object);
Getting prototype
By using Object.getPrototypeOf we can get prototype of the object.
var myObj = {};
Object.getPrototypeOf(myObj); // => Object
// or
myObj.__proto__; // Object
Constructor functions and “new” keyword
Keyword “new” creates new object so-called “instance of the function”. We can use “instanceof” keyword to check instance type.
function Dog() {};
var dog = new Dog();console.log(dog instanceof Dog); // ==> true
‘this’ reference keyword
Using “new” injects “this” reference to the new object that is being created and can be used in this constructor function. Note: Be careful when calling such a function (instead of using “new” keyword). In such a call “this” would refer to global scope instead of function scope.
function Dog() {
this.name = 'Woolfy';
}var woolfy = new Dog('Woolfy');
foo.name; // ==> 'woolfy';
Function prototype
Note that “prototype” is not necessarily equal to “__proto__”. There is a relation, though. When new object is created, __proto__ of the new object is the same as “prototype” property of function which created the object. There is simple example
function Dog(name) {
this.name = name;
}var woolfy= new Dog('Woolfy');
woolfy.__proto__ == Dog.prototype; // => true
Classes
Until ES6, we have several approaches for creating structures similar to class. Let’s start with “anti-pattern” approach where we refer to outer function.
function Dog(name) {
this.name = name;
this.getName = getDogName;
}
function getDogName() {
return this.name
}var dog = new Dog('goofy');
console.log(dog.getName());
Let’s do it better without exposing outer function
function Dog(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}var dog = new Dog('goofy');
console.log(dog.getName());
Another approach by using prototype
function Dog(name) {
this.name = name;
}
Dog.prototype.getName = function() {
return this.name;
}var dog = new Dog('goofy');
console.log(dog.getName());
There are other ways how to achieve the same, for example by using object literals. Anyway, ES6 ha brought “class” keyword so we can define prototypes of objects in cleaner OOP fashion.
class Dog {
constructor (name) {
this.name = name;
}
getName() {
return this.name;
}
}var d = new Dog('woofy');
console.log(d.getName());
Mixins
Mixins allow us to compose/expand multiple objects. Mixin is done by Object.assign function. A first parameter is an object prototype that is going to be expanded and a second parameter is an object that should be added.
function Dog(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
const Colored = {
color: 'red',
getColor: function() {
return this.color;
}
};Object.assign(Dog.prototype, Colored);
var d = new Dog('woory');
console.log(d.getColor());
Functional mixins
We can rewrite this into a functional form.
function Dog(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}const Colored = (target) =>
Object.assign(target, {
color: ‘blue’,
getColor() {
return this.color;
}
});Colored(Dog.prototype);
var d = new Dog(‘woory’);
console.log(d.getColor());
There are some more constructions possible as JS is very flexible but for basic understanding this should be enough. When compared with some traditional OOP languages, JS is more powerful allowing more advanced constructions and also requires a slight mental shift. On the other hand, syntax tends to be more error-prone. Although ES6 brings more transparent constructions, historical burden stays.