I stumbled upon this article I wrote in French precisely 15 years ago and I find it's still relevant. Despite its popularity, JavaScript is a language that deserves to be better known and acknowledged. So, I decided to translate it into English and republish it after a slight brush-up!
This article assumes you're familiar with JavaScript syntax. Note that I'm not discussing the class-based object orientation introduced in ECMAScript 6, but rather the native object orientation that has been present from the beginning in the language, based on the prototype concept.
So, let's take a deep breath and dive into the mysterious world of objects, prototypes, and closures!
Note:
The print()
function used in the examples is not part of the ECMAScript standard. In console mode, it displays text on the standard output. In a browser, it could be replaced by Document.write()
or alert()
. In Node.js you could use console.log()
.
Genuine Object Pieces Inside...
Objects
JavaScript is fundamentally built around the concept of objects. Everything in it is an object or a reference to an object. For example, arrays, types, or even functions are objects. Objects contain members, called properties, in the form of (name, value) pairs. Property values can be strings, numbers, booleans, or other objects (including arrays and functions).
Let's start with a simple example to represent my dog Rex:
// My dog Rex
var myDog = new Object();
myDog.name = "Rex";
myDog.gender = "Male";
print(myDog.name);
print(myDog.gender);
Executing the above code returns:
Rex
Male
We can also declare and manipulate an object like an associative array:
// Declaration as an array
var myDog = new Array();
myDog["name"] = "Rex";
myDog["gender"] = "Male";
print(myDog["name"]);
print(myDog["gender"]);
Rex
Male
myDog is still an object, and its properties can be accessed like array elements.
There's also a shorthand declaration for JavaScript objects:
// Shorthand declaration
var myDog = {name: "Rex", gender: "Male"};
print(myDog.name);
print(myDog["gender"]);
Rex
Male
This notation has been adopted by the JSON (JavaScript Object Notation) serialization format, which allows for the exchange of structured data over the network. It's often used as an alternative to the XML format by AJAX web applications for asynchronous client-server communications.
Now, let's give Rex the ability to speak:
myDog.bark = function() {
return "Woof!";
}
print(myDog.bark());
Woof!
In JavaScript, since functions are objects, it's possible to declare them as properties of other objects, thus creating methods. However, note that JavaScript doesn't differentiate between an object's properties, whether they're attributes or methods.
Constructors and Object Types
Now, imagine I also own a female dog named Mirza and want to represent her in JavaScript. We could replicate the previous declarations, but ideally, we'd reuse a common structure for both Rex and Mirza, i.e., create an object type Dog
.
JavaScript allows this through the use of a special function called a constructor, as follows:
// Dog object constructor function
function Dog(name, gender) {
this.name = name;
this.gender = gender;
this.bark = function() {
return "Woof!";
}
}
// Creating new Dog type objects
var myDog = new Dog("Rex", "Male");
var myFemaleDog = new Dog("Mirza", "Female");
print(myDog.name);
print(myFemaleDog.gender);
Rex
Female
Let's clarify two keywords used in the above example:
-
new
is the object creation operator. When applied to a function, that function is used as a constructor to create the new object. -
this
, within the constructor, refers to the object being created. So, this.name corresponds to a property of the object created by theDog
constructor, to which the value of thename
parameter passed to the function is assigned.
Although JavaScript handles primitive types (retrievable via the typeof
operator), it's more useful to consider that an object's type is the name of its constructor (retrievable via the constructor.name property). According to this perspective, the types managed by JavaScript are as follows:
- Object: generic, untyped object;
var myObject = {firstName: "Joe", lastName: "Black"};
print(myObject.constructor.name);
Object
- Boolean: boolean (either true or false);
var myBoolean = (1 == (2-1));
print(myBoolean.constructor.name);
Boolean
- Number: integer or decimal number;
var myNumber = 1;
print(myNumber.constructor.name);
Number
- String: string of characters;
var myString = "Hello";
print(myString.constructor.name);
String
- Array: array;
var myArray = [1, "brt"];
print(myArray.constructor.name);
Array
- Function: function containing a code segment;
var myFunction = function() {
return "hi there";
}
print(myFunction.constructor.name);
Function
- [ConstructorName]: type defined by the user via a constructor function.
function Dog(name, gender
) {
this.name = name;
this.gender = gender;
this.bark = function() {
return "Woof!";
}
}
var myDog = new Dog("Rex", "Male");
print(myDog.constructor.name);
Dog
By the way, let's recall that JavaScript is a dynamically typed language. This means that it doesn't know in advance the type of a variable (or rather, the object referenced by a variable). It relies on the value of the variable to determine its type and is able to convert the type of a variable based on context.
For instance:
var monNombre = 34;
var maChaine = "5";
print(monNombre + maChaine);
345
Encapsulation
Although JavaScript is an interpreted language, it allows the implementation of the encapsulation principle by distinguishing several levels of visibility:
- Public properties: By default, the components (attributes or methods) of an object are accessible to everyone.
function Object1() {
this.attribute = "Public attribute";
}
Object1.prototype.function = function() {
return "Public method";
}
var MyObject1 = new Object1();
print(MyObject1.attribute);
print(MyObject1.function());
Public attribute
Public method
- Private properties: Properties created in the constructor are only accessible to the object's private methods.
function Object2() {
var attribute = "Private attribute";
function function() {
return "Private method";
}
}
var MyObject2 = new Object2();
print(MyObject2.attribute);
try {
print(MyObject2.function());
} catch(e) {
print(e);
}
undefined
TypeError: MyObject2.function is not a function
- Privileged methods: Methods that can access the object's private properties while being accessible from outside.
function Object3() {
var attribute = "Private attribute";
this.function = function() {
return(attribute);
};
}
var MyObject3 = new Object3();
print(MyObject3.function());
Private attribute
Namespaces
As we have seen, the default execution context of the script is associated with the Global scope. Thus, all objects and all references declared outside of functions are global properties. As this can create conflicts, it is recommended to use objects to create namespaces and isolate variables in named packages.
This is achieved as follows:
// Package declaration
var org = {}; // Package root
org.test = {}; // Package branch
// Package members
org.test.Dog = function(name, gender) {
this.name = name;
this.gender = gender;
};
org.test.Dog.prototype.bark = function() {
return this.name + " barks!";
};
// Using the package
var myDog = new org.test.Dog("Rex", "Male");
print(myDog.bark());
Rex barks!
Prototypes
In our example, Dog
is not a class but indeed an object, certainly a special one, but an object nonetheless. In fact, JavaScript doesn't implement a class-based programming model (which leads some to say that it's not an object-oriented language), but rather a prototype-based programming model, similar to that of the Self language.
For every function, JavaScript associates a particular object, called a prototype, to serve as a reference for all objects created using the new
instruction. This object is accessible through the prototype
property of the constructor.
When a value is assigned to a property, JavaScript creates the property at the object level if it doesn't already exist. On the other hand, when the value of a property is requested, JavaScript performs the following operations:
- If the property exists at the object level, its value is returned; otherwise, JavaScript accesses the object's prototype and returns the prototype property's value if it exists;
- Otherwise, JavaScript accesses the prototype's prototype, and so on;
- If the property is not found by going up this prototype chain, JavaScript eventually arrives at the highest prototype, that of the generic Object, and then returns
undefined
.
Since an object's prototype is also an object, it can be dynamically modified. Modifying a prototype property (attribute or method) affects all instances due to the mechanism described above. This allows for dynamic and retroactive modification of all objects sharing the same prototype.
For example:
function Dog(name, gender) {
this.name = name;
this.gender = gender;
this.bark = function() {
return "Woof!";
};
}
myDog = new Dog("Rex", "Male");
Dog.prototype.sleep = function() {
return "Shh! " + this.name + " is sleeping...";
}
print(myDog.bark());
print(myDog.sleep());
myDog.sleep = function() {
return "Zzz...";
};
print(myDog.sleep());
Woof!
Shh! Rex is sleeping...
Zzz...
Let's break down step by step the actions performed by the JavaScript interpreter to fully understand the relationship between an object and its prototype:
-
Step 1 - The interpreter detects a function:
- An object (
F
here) representing theDog
function is created. - An object (
P
here) will serve as the prototype if the function is used as a constructor.
- An object (
-
Step 2 - Assign to the variable
myDog
:- A blank object (
O
here) is created. - The
Dog
function is used as a constructor, with the implicit parameterthis = O
. - The object
O
now has the properties: name, gender, and bark. - The variable
myDog
is a reference to the objectO
.
- A blank object (
-
Step 3 - Add a property to the constructor's prototype:
- The bark property is added to object
P
.
- The bark property is added to object
-
Step 4 - Call the
bark()
method of the variablemyDog
:-
myDog
points to the objectO
. - The object
O
has abark
property, so it is returned (and since it's a function, it's executed and its value returned). -
myDog.bark()
returns "Woof!"
-
-
Step 5 - Call the
sleep()
method of the variablemyDog
:-
myDog
points to the objectO
. - The object
O
does not have thesleep
property, so JavaScript goes to its prototype. - The prototype of object
O
is the prototype of its constructor, which is objectP
. - The object
P
has thesleep
property, so it is returned (and since it's a function, it's executed and its value returned). -
myDog.sleep()
returns "Shh! Rex is sleeping..."
-
-
Step 6 - Rewrite the
sleep()
property of the variablemyDog
:-
myDog
points to objectO
. - The
sleep
property of the objectO
is created.
-
-
Step 7 - Call the
sleep()
method of the variablemyDog
:-
myDog
points to the objectO
. - The object
O
now has thesleep
property, so JavaScript doesn't go to its prototype. -
myDog.sleep()
returns "Zzz..."
-
This dynamic modification mechanism through their prototype is also applicable to JavaScript's predefined objects:
String.prototype.reverse = function() {
var reversed = "";
for (i = this.length; --i >= 0;) {
reversed += this.charAt(i);
}
return reversed;
}
var myString = "Hello!";
print(myString.reverse());
!olleH
The reverse()
method is now available for all strings!
Let's translate the content into English:
Static Properties
Properties declared outside of the constructor (and therefore not present in the prototype) are not referenced by instantiated objects. This allows for the implementation of the equivalent of static methods and attributes in class-based programming (sometimes referred to as class methods and attributes):
var Dog = function(name, gender) {
this.name = name;
this.gender = gender;
this.bark = function() {
return "Woof!";
}
}
//Static method for detecting identical names
Dog.hasSameName = function(dog1, dog2) {
return (dog1.name == dog2.name);
}
var myDog = new Dog("Rex", "Male");
var myBitch = new Dog("Mirza", "Female");
print(Dog.hasSameName(myDog, myBitch));
myBitch.name = "Rex";
print(Dog.hasSameName(myDog, myBitch));
false
true
Inheritance
Prototype-based Inheritance
As we have already pointed out, the prototype-based programming model does not use the concepts of classes and class instances, but only that of objects. Therefore, inheritance doesn't occur between classes defined statically, but directly between object prototypes.
Inheritance between two objects is achieved by assigning the parent object to the prototype of the child. It is then possible to add new properties to the child or even replace those inherited from the parent.
Let's now imagine that I also want to represent my cat Felix, while reusing what we did for Rex and Mirza.
Let's create a parent object Animal and two child objects, Dog and Cat:
// Parent object
function Animal(name, gender) {
this.name = name;
this.gender = gender;
this.eat = function() {
return this.name + " eats";
}
this.sleep = function() {
return "Quiet! " + this.name + " is sleeping...";
}
}
// First child object
function Dog(name, gender) {
// Passing parameters to the parent object
Animal.call(this, name, gender);
} // which inherits from Animal
Dog.prototype = new Animal();
// but is of type Dog
Dog.prototype.constructor = Dog;
// Add a method to the child
Dog.prototype.bark = function() {
return this.name + " barks!";
}
// Second child object
function Cat(name, gender) {
// Passing parameters to the parent object
Animal.call(this, name, gender);
} // which inherits from Animal
Cat.prototype = new Animal();
// but is of type Cat
Cat.prototype.constructor = Cat;
// Add a method to the child
Cat.prototype.meow = function() {
return this.name + " meows!";
}
var myDog = new Dog("Buddy", "Male");
var myCat = new Cat("Whiskers", "Male");
// Add a method to the parent object, after creating child instances
Animal.prototype.eat = function() {
return this.name + " eats";
}
print(myDog.sleep());
print(myDog.bark());
print(myDog.eat());
print(myCat.sleep());
print(myCat.meow());
print(myCat.eat());
Quiet! Buddy is sleeping...
Buddy barks!
Buddy eats
Quiet! Whiskers is sleeping...
Whiskers meows!
Whiskers eats
Note the use of the call()
method of the JavaScript Function
object, which allows a function to be called as if it belonged to the object to which it is applied. We use this function to pass the name and gender parameters between the constructors of the children and the parent.
Multiple Inheritance
Although JavaScript does not support multiple inheritance, it is possible to partly simulate this behavior by using the call()
method mentioned above. However, since an object can only have one constructor (and therefore one prototype) at a time, it is impossible to maintain the dynamic link between parent and child objects.
Let's try to represent a Werewolf, which is both a Man
and a Wolf
, through prototype inheritance:
// First parent object
function Wolf() {
this.bite = function() {
return this.name + " has bitten you!";
};
}
// Second parent object
function Man() {
this.speak = function() {
return this.name + " speaks to you...";
};
}
// Child object
function Werewolf(name) {
this.name = name;
Wolf.call(this); // Call the constructor of the first parent
Man.call(this); // Call the constructor of the second parent
this.transform = function() {
return this.name + " has transformed!";
};
}
var myWerewolf = new Werewolf("Jack");
print(myWerewolf.speak());
print(myWerewolf.transform());
print(myWerewolf.bite());
Jack speaks to you...
Jack has transformed!
Jack has bitten you!
Dynamic Inheritance
When creating a new object using the new
operator, the created object is assigned an implicit reference to the constructor's prototype. Since this prototype is also an object, it seems feasible to dynamically modify an object's inheritance by modifying its prototype chain.
Let's now try to represent a Werewolf, which is either a Man or a Wolf but never both at the same time:
// First parent object
function Wolf() {
}
Wolf.prototype.speak = function() {
return this.name + " growls...";
};
Wolf.prototype.bite = function() {
return this.name + " has bitten you!";
};
// Second parent object
function Man() {
}
Man.prototype.speak = function() {
return this.name + " speaks to you...";
};
Man.prototype.bite = function() {
return this.name + " bit his tongue!";
};
// Child object
function Werewolf(name) {
this.name = name;
}
Werewolf.prototype = new Man();
var myWerewolf = new Werewolf("Jack");
print(myWerewolf.speak());
Werewolf.prototype = new Wolf();
print(myWerewolf.speak());
var myWerewolf2 = new Werewolf("Joe");
print(myWerewolf2.speak());
Regrettably, the previous code returns this:
Jack speaks to you...
Jack speaks to you...
Joe growls...
The myWerewolf
instance wasn't impacted by the modification of its constructor's prototype. This can be explained by the fact that its implicit prototype property still points to the same object, even if it's not the new prototype of its constructor. On the other hand, the new myWerewolf2
instance does indeed have an implicit reference to the new prototype.
To dynamically modify an object's inheritance, we would need to be able to directly modify the implicit reference to the constructor's prototype. The implementation of this property, although discussed in the ECMA-262 standard, is not mandatory. Thus, it's not available in all ECMAScript implementations.
It is generally implemented by the __proto__
property. The following code proves that the __proto__
property indeed points to the object's constructor's prototype:
// Parent object
function Wolf() {
}
Wolf.prototype.speak = function() {
return this.name + " growls...";
};
Wolf.prototype.bite = function() {
return this.name + " has bitten you!";
};
// Child object
function Werewolf(name) {
this.name = name;
}
Werewolf.prototype = new Wolf();
var myWerewolf = new Werewolf("Jack");
print((myWerewolf.__proto__ === Werewolf.prototype));
true
Thus, we can now dynamically reassign the Werewolf's parent:
// First parent object
function Wolf() {
}
Wolf.prototype.speak = function() {
return this.name + " growls...";
};
Wolf.prototype.bite = function() {
return this.name + " has bitten you!";
};
// Second parent object
function Man() {
}
Man.prototype.speak = function() {
return this.name + " speaks to you...";
};
Man.prototype.bite = function() {
return this.name + " bit his tongue!";
};
// Child object
function Werewolf(name) {
this.name = name;
}
Werewolf.prototype = new Man();
// Reassignment of the Werewolf's prototype
Werewolf.prototype.transform = function() {
switch (this.__proto__.constructor.name) {
case "Man":
Wolf.call(this);
this.__proto__.__proto__ = new Wolf();
return this.name + " has transformed into a Wolf!";
break;
case "Wolf":
Man.call(this);
this.__proto__.__proto__ = new Man();
return this.name + " has transformed into a Man!";
break;
}
}
var myWerewolf = new Werewolf("Jack");
print(myWerewolf.speak());
print(myWerewolf.bite());
print(myWerewolf.transform());
print(myWerewolf.speak());
print(myWerewolf.bite());
print(myWerewolf.transform());
print(myWerewolf.speak());
Jack speaks to you...
Jack bit his tongue!
Jack has transformed into a Wolf!
Jack growls...
Jack has bitten you!
Jack has transformed into a Man!
Jack speaks to you...
This dynamic inheritance example demonstrates the power of JavaScript.
Closures
Closures are a very powerful feature of JavaScript, yet they remain relatively unknown or often misunderstood, leading to the inadvertent production of faulty code. In simple terms, it's the ability of an inner function to access properties of the enclosing function, even after the enclosing function has finished its execution.
This is made possible through two mechanisms implemented by JavaScript: execution contexts and scopes. With every function call, JavaScript creates a new execution context and associates it with a scope, consisting of a series of objects that determine how the function's variables are initialized. This is applicable to functions nested within other functions, which includes recursive functions. These objects carry the different contexts that led to the function's execution. The lowest execution context is that of the JavaScript script itself and is carried by the Global
object.
Example №1
The concept of closures isn't easy to grasp in a theoretical sense, so let's illustrate it with a concrete example:
01: var number1 = 7;
02: var number2 = -2;
03:
04: function Outer(num) {
05: var factor = 3;
06: this.Inner = function (coef) {
07: return (num * factor * coef);
08: };
09: }
10:
11: var MyObject1 = new Outer(number1);
12: print(MyObject1.Inner(10));
13:
14: var MyObject2 = new Outer(number2);
15: print(MyObject2.Inner(100));
210
-600
Let's delve into the details of what JavaScript does when it parses the above code:
-
global context:
-
lines 1 and 2: at the most basic execution context, JavaScript adds properties
number1
andnumber2
with values 7 and -2 to theGlobal
object, which holds the script's execution context.
-
lines 1 and 2: at the most basic execution context, JavaScript adds properties
-
Step 1:
-
line 11: JavaScript creates a new object
MyObject1
by executing theOuter
function as a constructor: -
line 4: JavaScript establishes a new execution context for the
Outer
function and an object (called "Outer Call Scope" in the diagram) carrying thenum
parameter and function properties (includingfactor
). The scope of this execution context consists of the "Outer Call Scope" object linked withGlobal
. -
line 6: JavaScript creates a new execution context for the
Inner
function and an object (called "Inner Call Scope" in the diagram) carrying thecoef
parameter. The scope of this execution context consists of the "Inner Call Scope" object linked with "Outer Call Scope" and thenGlobal
. -
line 8: execution of the
Inner
function concludes, but its scope is retained in memory as it's referenced by another execution context (usage ofreturn
). -
line 9: execution of the
Outer
function ends, its scope isn't referenced anymore and will be removed in the next garbage collector cycle. JavaScript returns to the global execution context. -
line 12: the
Inner
method is called, JavaScript uses the retained context and scope to initialize variables and execute the function: - num equals 7;
- factor equals 3;
- coef equals 10;
- Inner() returns 210.
-
line 11: JavaScript creates a new object
-
Step 2:
- line 14: the previous process is performed again, leading to the creation of a new execution context and a new scope, different from those in Step 1.
- line 15, in this new scope:
- num equals -2;
- factor equals 3;
- coef equals 100;
- Inner() returns -600 this time.
Example №2
Here's a second example of a closure used to implement a function of functions:
function CreateDogAction(action) {
return function() {
return this.name + " is " + action;
}
}
function Dog(name) {
this.name = name;
this.eat = CreateDogAction("eating");
this.sleep = CreateDogAction("sleeping");
this.bite = CreateDogAction("biting");
}
var myDog = new Dog("Rex");
print(myDog.eat());
print(myDog.sleep());
print(myDog.bite());
Rex is eating
Rex is sleeping
Rex is biting
Example #3
A closure can contain multiple functions accessing a common property, as illustrated by this counter management example:
function initCounter(start) {
// Private variable
var counter = start;
// Create the counter manipulation functions
getCounter = function() {
return counter;
};
incCounter = function() {
counter++;
};
setCounter = function(value) {
counter = value;
};
resetCounter = function() {
counter = start;
};
}
initCounter(0);
setCounter(10);
incCounter();
print(getCounter());
resetCounter();
print(getCounter());
11
0
Conclusion
This article was written to shed light on some of JavaScript's features that often go unnoticed when the language is simply associated with dynamic web page features. However, delving deeper reveals that JavaScript is indeed a potent, ever-evolving language, underpinning the success of modern web applications.
The lack of knowledge regarding JavaScript's object features is likely tied to unfamiliarity with the prototype-based programming model. This is probably why ECMAScript version 6 also introduces the class-based programming model to the language.
Was this truly a wise move? I'll leave that for you to decide. In my view, it certainly hasn't encouraged the widespread use of prototypes, which are, after all, at the very heart and strength of JavaScript...
Top comments (1)
Vy good overview! Getting back into JS after being away a long time and everything you present here jives with what I remember from the early language specs.