JavaScript distinguishes itself from most usual “object-oriented programming” (OOP) languages, which are “class based”: JavaScript is “prototype based”. This originality is confusing for people accustomed to other OOP languages, and sometimes perceived as a weakness vis-à-vis better “controlled” languages such as Java, of which it “inherited” the name based on a misunderstanding.
This chapter is organized as follows:
We can hardly talk about objects without talking about functions, and even arrays. Therefore, Chapters 5–7 are very intertwined. But “object” is probably the most basic notion, and we start with it, even if several “forward references” must be used in the text to point out that some tools will be defined later on.
The prototypal approach may seem puzzling, but it provides flexibility and ease in programming (maybe too much?). We do hope that this chapter will convince you that it is better to stand with the very nature of JavaScript, and use the prototypal approach instead of the classical one, as often as possible, and especially for the kind of “data-oriented” applications targeted by this book.
An object is something that we can grasp (a glass on a table), distinguish at a glance (a cloud in the sky) or which we can describe by some characteristics (an appointment with a friend). In any language, an object is either a particular object (this glass on this table), or the generic concept that we attach to a set of similar particular objects (“a” glass).
These two viewpoints may be as old as any human language: is the generic concept different from the set of the named entities it represents? (see Platoʼs “Cave allegory”, the “problem of universals”). The computer languages face the same issue:
A metaphor may help identify the issue: let us dare to quote the field of law: exclusive use of Jurisprudence (any case may be used a posteriori) versus exclusive use of the Civil Code (any case reduces to an article of the Code).
We propose to rephrase the distinction between:
Generic concept, an a priori representation of similar objects, made of:
Named entities, represented by an explicit notation:
Let us first study how JavaScript answers the named entity notion.
A JavaScript object is merely the container of a collection of named values. Here is the syntax of the notation representing such a collection.
The operator typeof
answers "object"
to every notation complying with the following syntax (object notation):
{} is the empty object
{ "key_1" : "value_1", "key_2" : "value_2", … }
This notation is simple, but may present some traps: here are several warnings to help you to avoid writings that will mislead the interpreter.
Any Unicode character can be used, and any word, including “reserved word”. However, we recommend to respect these best practices:
Examples of valid property names, complying best practice:
{ "firstName": "Jean", "name2": "O", "_salaire_€": 20e3, …
The type of the property is the type of its value (idem: variables):
string
: (idem variables) the engine store, the string and the address (reference) becomes the value of the property.
"first": "Jean", // address of the string "Jean"
number or boolean:
the primitive values must be written without quotes, unless they are strings and a conversion may be necessary:
"age": 22, // type number
"age": "22", // type string: to convert if required
object
(including array): any value whose typeof is "object"
is valid:
circonscription": { "dept": "Gironde", "numero": 2 }
affiliation": [ "party_1", "party_2" ]
These literals are stored and their references become the values.
function
: any function expression, then called a “method”:
fullName: function(){return this.first +" "+ this.last;}
The methods are not accepted in JSON format, only as strings:
fullName: "function(){return this.first +' '+ this.last;}"
An object literal on the right-hand side of a variable definition statement is stored, and the address is assigned to the (left-hand) variable. Example:
const candidatN = {
"first": "Jean", "last": "Dupont",
"age": 22,
"circonscription": {
"dept": "Gironde", "numero": 2
},
"fullName": function(){return this.first+" "+this.last;}
};
The object is created when the engine meets the curly braces {.}: the built-in function Object is implicitly invoked. The keyword this will be explained later.
The object literal is a mere character string that we can archive in a plain text file. The further reading of the file will provide a string that could be directly used as the literal for an object in the target code, written in JavaScript or possibly some other programming language: the chapter on AJAX details this importation mechanism.
This idea of a plain text file for the object notation has been specified by Douglas Crockford in 2001, precisely “Java Script Object Notation” (JSON). Since then, it has been massively used as an exchange format over the Internet. To facilitate the interface with those “JSON files”, an object JSON has been added to JavaScript, with two static methods:
– JSON.parse()
interprets the string as a literal and returns the object.– JSON.stringify()
converts an object into a string, keeping only the “enumerable properties”, and making some editing (see below).
const candidatN = {
"first": "Jean", "last": "Dupont", "age": undefined,
"circ": {
dept: "Gironde", num: 2
},
"fullN" : function(){return this.first +" "+ this.last;}
};
JSON.stringify(candidatN);
//prints:
{"first":"Jean","last":"Dupont","circ":{"dept":"Gironde","num":2}}
NOTE.– White spaces or line feeds are removed, quotes are added (dept, num
), and the method is ignored, as well as the property whose value is undefined
.
There are two notations to denote one particular property of an object:
let dN = candidatN.circo.dept;
dN = candidatN["circo"]["dept"];
The dot notation is simpler, but cannot be used in every case, in particular if the name of the property is given through a variable. For example:
function f(prop){
candidatN[prop]; // ok, if prop exists in candidatN
candidatN.prop; // undefined : "prop" isn't a property
}
f("first"); // gives prop, in f, the value "first"
For instance, with the nested object “circo”, we can create the object once, outside of any “candidat” object, then the reference to that “circo” object can be mutualized between several “candidat-like” objects. To mutualize avoids creating multiple instances of the “Gironde” string, and makes it possible to “inherit” further additions, modifications brought to the mutualized reference:
const circo_3302 = { // (Gironde INSEE number is 33)
"dept": "Gironde", "num": 2
};
const candidatN = {
"first": "Jean", "last": "Dupont", "circo": circo_3302
};
circo_3302.population = 25600;
candidatN.circo.population // 25600 new property is "concatenated"
Table 4.1. Shorthand syntax for object literal notation (ES6)
Regular notation | ES6 shorthand |
|
|
A declared and defined variable can be used directly as a property. Methods are named directly. |
Using JSON.stringify(obj) restores the regular JSON notation. For example:
let a = "A", b = 1;
JSON.stringify({a, b}); // {"a":"A","b":1}
JSON.stringify({a:a, b:b}); // {"a":"A","b":1} : identical
Table 4.2 lists the “static methods” of the built-in object Object
, which must be invoked directly from Object: Object.method()
.
Table 4.2. The most usual static methods of Object
Method | Description | Return |
Object.assign |
Copies the enumerable own properties of (o1,o2 … | Object |
Object.create |
Creates an object whose prototype is (proto) | Object |
Object.defineProperties Object.defineProperty |
Creates or modifies the properties of (o) Idem. only one property |
Object |
Object.freeze Object.isFrozen |
Prevent against modifications of (o) Test status |
Object Boolean |
Object.getPrototypeOf |
Returns the [[prototype]] of (o) | Object |
Object.setPrototypeOf |
[to avoid : instead use Object.create() ] |
|
Object.keys Object.values Object.entries |
lists names of enumerable own properties of (o), idem for values, idem for [key, value] |
Array |
Table 4.3 lists the methods of the object Object.prototype, which are delegated to all objects. They are invoked from any object: obj.method().
Table 4.3. The methods of Object.prototype, delegated to any object
Method | Description | Return |
obj.hasOwnProperty |
Tests if property (p) is “own” | Boolean |
obj.isPrototypeOf |
Tests if prototype chain contains (o1) | Boolean |
obj.propertylsEnumerable |
Tests if property (p) is “enumerable” | Boolean |
obj.toString obj.toLocaleString |
Provides a stringified version of the object (idem with locale rules) | String |
And, it is worth remembering here the two (static) methods of the object JSON.
Here is the syntax of the two most useful methods:
Object.assign( target, source [, source2, …] ) makes it possible to “augment” the object “target” with the properties of another object “source” (or several). Returns the augmented “target”.
Object.create( proto [, descriptor1, descriptor2, …] ) creates an object from the object “proto” used as the prototype of the new object. The (optional) descriptors make it possible to add individual properties with specific control attributes.
The two methods Object.defineProperty( obj, prop, descriptor ), and Object.defineProperties( obj, objectWithDescibedProps )
also utilize the “descriptors” properties, as described below.
In the methods described in the following, Descriptors have been added to JavaScript by ES5, to better control the rights (e.g. read/write) of every property. All the descriptors possess:
configurable
: the descriptor itself can be modified and the property can be deleted “delete” (default: false);enumerable
: the property is enumerable (default: false
).
Then two cases (exclusives): the “data descriptors” with:
value
: the value of the property (default: undefined
);writable
: the property can be modified (default: false
);and the “accessor descriptors” instead have:
get
: function returning the value of the property (def.: undefined
);set
: function whose argument becomes the value (def.: undefined
).Using a “data descriptor”:
{ "property name" : { "value": value,
"enumerable": boolean,
"configurable": boolean,
"writable": boolean }
}
By default, all three booleans are initialized to false
.
WARNING.– For an object created by a literal, only the couple (key, value) is considered, and by default, all three booleans are initialized to true
.
The Object.freeze( obj ) method prevents any modification of the object, which constant does not do.
The methods Object.getPrototypeOf(obj),
obj.isPrototypeOf(obj2),obj.hasOwnProperty(p),
obj.propertyIsEnumerable(p),and
Object.getOwnPropertyNames(obj) are obvious.
Object.getOwnPropertyNames
and similar methods can be added which make it possible to list otherwise invisible properties (not of use in this work).
There exists a property enumeration instruction in JavaScript: for..i n
const candidatN = {"first": "Jean", "last": "Dupont", … …};
for (let prop in candidatN) {console.log(prop);}
for (let prop in candidatN) {console.log(candidatN[prop]);}
// these instruction lines successively display:
"first", "last", "age", "circonscrition"
"Jean", "Dupont", 22, [Object()]
NOTE 1.– for..in looks like a loop, but it is an enumerator: it lists the enumerable properties of the object, either own properties or delegated by the prototype. To break down between own/delegated properties, use obj.hasOwnProperty(prop).
NOTE 2.–for..in is not recursive, for a nested object, you must code recursively.
Object.keys(obj)/.values/.entries
returns an array of the enumerable properties of obj in the same order than for..in but limited to each property: keys returns the names, values the values and entries
the couples. Let us compare:
for (let p in candidatN) {
if(candidatN.hasOwnProperty(p)){console.log(candidatN[p]);}
}
// equivalent to:
Object.values(candidatN).forEach(function(v){console.log( v );};
The JSON methods accept a second, optional, argument: a function with two arguments (key, value).
For example, with ‘JSON.stringifyʼ we cannot directly “stringify” a method, but we can get its code as plain text::
function stringifyTheMethods(k, v) {
return (typeof v === "function")? v.toString(): v;
});
JSON.stringify(candidatN, stringifyTheMethods);
// yields:
{"first":"Jean","last":"Dupont","circo":{"dept":"Gironde","num":2},"fullName ":"function() {return this.first +" "+ this.last;}"}
NOTE.– The quotes used in the code are escaped.
For example, with ‘JSON.parseʼ, to avoid warning messages (“JSON badly formed”), we can add escapements for tabulations, line breaks, etc., with regular expressions::
function escapeSpecialChars(k, v) {
return v.replace(/"/g, "\"") // quotes
.replace(/ /g, "\n") // line feed
.replace(/ /g, "\r") // carriage return
.replace(//g, "\b") // back space
.replace(/ /g, "\t") // horizontal tab
.replace(/f/g, "\f"); // new page
}
JSON.parse( jsonText, escapeSpecialChars );
JavaScript takes into account the notion of “named entity”:
Question:
Many OOP languages use a software feature, named a “class”, to represent the generic concept, plus a mechanism to build “instances” of that class, which are the named entities. In JavaScript, there is no such thing as a class, neither an instance: So, what do we do?
NOTE.– The ambition of Netscape was to have a scripting language similar to Java. Brendan Eich, in order to complete his contract during the very short allotted time, chose the “prototypal approach”, which is to say: the mere addition, in every object, of a link to another object, named its “prototype”. This simple choice has since fueled many misunderstandings, especially because of the operator: “new”. Despite the introduction of the keyword “class” in ES6, nothing has changed: classes and instances still do not exist in JavaScript.
Does the “prototypal approach” answer the three earlier mentioned criteria for generic concepts ? Does it provide:
Instead of being member of a “class”, every JavaScript object owns a link to a particular object: its “prototype”. Though it is a property of the object, this link is not directly accessible (not part of the norm). Let us name this link: [[prototype]].
Figure 4.1 demonstrates the fundamental power and limits of this relation.
So far, we know how to build objects from literals. Now, the question is:
How can we build an object with a given object as its prototype?
Since ES6, there is a simple answer: thanks to the method Object.create,
we can pick an object and use it as the prototype for the new object:
const proto = {/* any object, e.g. a literal */};
const newob = Object.create(proto); // newob.
[[prototype]]= proto
The strict partial order and the associated tree structure determine the “prototype chain” of every object. To keep it simple, let us say that any JavaScript object is built:
Object.prototype:
|
Object. create (proto)
, and its [[prototype]] is proto
:
|
The overall tree structure is made up of nodes that are prototypes, except the terminal leaves, and its root is Object.prototype
(we can ignore null
). All nodes, which are neither root, nor leaves, can be labeled by the prototype that is characteristic of the related “equivalence class”.
We can say that these equivalence classes are the “generic concepts” we were looking for in the previous section, and, at the same time, any such generic concept is also a “named entity”, for the prototype is an object (remember the similarity with “jurisprudence”).
Given an object (p
), it is by itself a named entity, and can play the role of a generic concept for other objects (x
): x
¬ p
The method Object.create(p)
allows us to create one x whose prototype is p
, and Object.getPrototypeOf(x)
allows us to check if the prototype is p
:
const p = { oprint(){return 'I am p the prototype';} };
const x = Object.create(p);
if(Object.getPrototypeOf(x) === p){console.log(x.oprint());}
// " I am p the prototype " (the test is positive)
Let us create a new object using x as prototype:
const y = Object.create(x);
if(Object.getPrototypeOf(y) === x){console.log(y.oprint());}
// " I am p the prototype " (the test is also positive)
The objects x
and y
successively inherit the method oprint
from p
. The relation is transitive and the chain of prototypes is:
y x ¬ p ¬ Object.prototype
And inheritance is transitive as well: the object that plays the role of prototype automatically delegates its methods and properties to the members of the equivalence class, which it labels.
This “inheritance by delegation” works as follows:
When a property is “called” on an object x
, for instance through the dot notation x.oprint
, then the property is searched among x
ʼs own properties. If not found, the same search is performed for each member of the prototype chain down to Object.prototype
. The first occurrence found becomes the value, else undefined
is the value.
With the object notation {…}
we can create objects whose prototype is Object.prototype
.
With the Object.create(p)
method, we can create objects whose prototype is the object p
, which therefore becomes a prototype unbeknownst to itself.
The prototypes chain is also an inheritance chain: the properties of an object playing the role of a prototype are delegated to all the objects upwards in the chain.
The inheritance by delegation is dynamic: any modification of a method of a certain prototype object automatically modifies the method used by any object that has the prototype in its chain.
NOTE.– By contrast, a method is “static” if that method is owned by the object itself.
JavaScript is very permissive; it is sometimes unwise to use this flexibility: some recommendations are as follows:
RECOMMENDATION 1.– Objects are containers, the contents can be modified, but do not modify the box: use const x = Object.create(p);
or const x = {..};
to declare and create an object.
RECOMMENDATION 2.– Its prototypes chain is the marker of an object; do not modify that chain once created and do not use Object.setPrototypeOf(x).
In the following, three object construction approaches are described: literal with the operator {..}
, prototypal with Object.create
and classical with the operator new
.
To better understand object construction, we need some additional background.
The built-in object Function
is a function, and as such (see chapter 5) it owns a special property named prototype
, which is not to be confused with the [[prototype]] property. Hence Function
owns a Function.prototype
and a [[prototype]].
The built-in object Object
is a function, therefore it owns an Object.prototype
, which points to the root to any prototypes chain, and whose [[prototype]] is null
.
The literal approach for creating an object is: const x = {..};
it:
x
: implicitly with function Object
;x
[[prototype]] = Object .prototype
;= "Object".
Once created, the object may receive new properties, and others can be modified:
const x = {last: "Dupont", first:"J"};
x.first = "Jean";
x.full = function(){return this.first+" "+this.last}
NOTE.– These properties and methods are named “static” and we must use the object itself to modify them. By contrast, properties or methods delegated from the prototype can be modified independently.
Any object, created with the literal approach, can be used to create a new object whose [[prototype]] is that first object:
const protox = {last = "Dupont", first = "Jean";
full(){return this.first+" "+this.last}};
const x = Object.create(protox);
console.log(x.full()); // Jean Dupont
if(Object.getPrototypeOf(x) === protox) // true
console.log(x.constructor.name); // Object
The method 'Object.create
' acts as follows:
x
;x
. [[prototype]] = protox;
x.constructor
inherits from protox.constructor
.function Candidat(){} // provides Candidat.prototype
Candidat.prototype.full = function(){return this.first+ … };
const x = Object.assign( // line 3
Object.create(Candidat.prototype),
{last = "Dupont", first = "Jean"} );
console.log(x.full()); //-> Jean Dupont
if(Object.getPrctctypeOf(x) === Candidat.prototype) //true
console.log(x.constructor.name); //-> Candidat
The combination Object.assign/Object.create
(lines 3–5) underlines the distinction between the “generic concept” (the argument of 'create
') and the “named entity” (arguments 2+ of 'assign
'). We call it the assign/create pattern (see “Design patterns” in Chapter 7 for more details).
The method Object.assign
adds new properties, or modifies existing ones according to its arguments 2 and next. To remove an existing property, we need to apply the operator 'delete
' on that property. The property constructor
has been valuated to Candidat
by Object.create
.
Further adding of methods to Candidat.prototype
will automatically trigger those methods for every object created through the assign/create combo.
In Part 3, this pattern is put into practice with several “data-oriented” applications.
new
do?It invokes a function, which is named a “constructor”. Good practice is to capitalize its name with an uppercase letter:
function Candidat(last){this.last = last;} // line 1
const x = new Candidat("Dupont"); // line 2
Line 1: Two objects are created: Candidat
and Candidat.prototype
.
WARNING.– The property Candidat
.[[prototype]] is Function.prototype
, for Candidat
is a function, which inherits function’s methods: e.g. call
.
Line 2, new Candidat
, acts as follows:
x
, which is assigned to the pronoun this
, inside the function. Otherwise, this = window
(or the global object);this
(unless an explicit return
is coded: to avoid!);x.
[[prototype]] = Candidat.prototype
, which makes x
inherit by delegation;x.constructor = Candidat
.The role of the constructor is very limited: the object Candidat.prototype
is the one that really triggers the inheritance by delegation. The overall classical operation1 is complex, even if it is transparent for the programmer.
Let’s explore the arguments of Douglas Crockford2, who advocated the introduction of Object.create
He was simulating that method3 using the operator new
:
Object.create = function (proto) {
function F() {} // creates a prototype property
F.prototype = proto; // makes proto that prototype
return new F(); // creates an object with proto as prototype
};
A class hierarchy is based upon the “Is-a” relation, for example, “a candidate is-a person” (plus some specific properties). Let us compare the two approaches.
function Person (last, first) { // line 1
this.last = last || "?";
this.first = first || "";
}
function Candidate (last, first, dN) { // line 2
Person.call(this, last, first); // uses code of Person with this
this.dN = dN || "somewhere";
}
Candidate.prototype = new Person(); // line 3
Candidate.prototype.constructor = Candidate; // line 4
Person.prototype.fullName = function(){ // line 5
return this.first+" "+this.last;
};
Candidate.prototype.fullName = function(){ // line 6
return Person.prototype.fullName.call(this)+", at "+this.dN;
};
const c1 = new Person("D","Jean", "Creuse");
const c2 = new Candidate("D","Jean", "Creuse");
Let us comment on the logics of that code:
Person
and Candidate
are creating prototypes, and make some initializations;Candidate
“recycles” the code of Person
with itself (this
), i.e. the object that will be created when invoking new Candidate()
. This is the job of Person.call
(see Chapter 6).c2 ¬ Candidate.prototype ¬ Person.prototype ¬ Object.prototype
constructor
, which line 3 initialized to Person
;fullName
to Person.prototype;
Person.fullName
in Candidate.prototype
.The inheritance is controlled in lines 3-4. Finally, for c1
, c2
, let’s print: 'c.constructor.name'
, JSON.stringify(c)
and c.fullName()
:
name JSON fullName()
Perscn {"last":"D","first":"Jean"} Jean D
Candidate {"last":"D","first":"Jean","dN":"Creuse"} Jean D, en Creuse
function Person(){} // line 1
function Candidate(){} // line 2
Person.prototype.fullName = function(){ // line 3
return this.first+" "+this.last;
};
Candidate.prototype = Object.assign( // line 4
Object.create(Person.prototype),
{ fullName(){
return Person.prototype.fullName.call(this)+", at "+this.dN;
}});
Candidate.prototype.constructor = Candidate; // line 5
const p1 = Object.assign( // line 6
Object.create(Person.prototype),
{ last:"D", first:"Jean", dN:"Creuse"});
const p2 = Object.assign( // line 7
Object.create(Candidate.prototype),
{ last:"D", first:"Jean", dN:"Creuse"});
Let us comment and compare:
Person
and Candidate
merely create prototypes;fullName
to Person.prototype
(= line 5 classical);Candidate.prototype
to inherit from Person.prototype
, and updates method fullName
(= line 6 classical);Candidate.prototype.constructor
(= line 4 classical).Figures 4.5 and 4.6 demonstrate the similarity of the two approaches.
In Figures 4.5 and 4.6, the numbered dotted lines represent the prototype chains of the following objects:
c2/p2 ¬ Candidate.prototype ¬ Person.prototype ¬ Object.prototype ¬ null.
In the prototypal approach, 'pattern'
replaces 'new'
, and there is no initialization for Person
and Candidate
.
constructor
refers to the function f
that provides its f.prototype
to the object (if a literal f = Object). The constructor.name
property is shared by all the objects of an equivalence class.Methods defined in the constructor are “static” and methods defined in its prototype are “delegated”. For example:
function Candidate(last, first){ // method in the constructor
this.last = last; this.first = first;
this.full = function(){return this.first+" "+this.last};
}
const c1 = new Candidate("Dupont"), c2 = new Candidate("Durand");
(cl.full === c2.full); // false: not shared
The full
method is different for each new
instruction. You may get hundreds!
function Candidate(last, first){
this.last = last; this.first = first;
} // method in the prototype
Candidate.prototype.full = function(){return this.first+"…};
(cl.full === c2.full); // true: shared
A single method 'full'
is delegated: not found among the own properties, the method is taken from the prototype. One unique version for hundreds!
There are two ways to provide properties to an object:
Object.create
');Object.assign
').An object inherits from only one prototype (delegation). The concatenation makes it possible to multiply static inheritances under the condition that the referred objects (from which copies are made) remain unchanged. This can be the case when we concatenate methods or specific values, which are meant to be constant. One solution can be to “freeze” those referred objects (Object.freeze
), hence preserving them from later modifications.
There is no “class” nor “instance” in JavaScript, however we have learnt that a generic concept is provided by the "equivalence class” induced by the relation “has prototype”, and we can name it (constructor.name
). Any member of that class can be named an “instance”, if you are pleased with it.
52.14.151.45