Objects and Prototypes in JavaScript
by Nicklas EnvallObject-oriented programming is a well-known programming paradigm. Object-oriented programming can be class-based or prototype-based. In prototype-based languages, objects exist, but classes do not. We will see in this article that JavaScript supports prototype-based object orientation.
The density of information starts slows and ramps up, so beginners can get something out of the article as well. This article consists of the following sections:
- What is an Object?
- Prototypes in JavaScript
- JavaScript has no classes
1. What is an Object?
An object in JavaScript is a collection of properties. An object property, in its simple form, has a name (key) and a value. We use property names to point to values. All property names must be unique strings within the object, while a property value can be of any data type.
There are two common ways for creating objects, which are the literal syntax form and constructed form.
var obj1 = {}; // literal syntax var obj2 = new Object(); // constructor syntax
We can create a property by giving it a value. Here we set our property with dot notation, but we could also use bracket notation which we do in the latter created object:
// dot notation var person = new Object(); // Note that you could use {} instead of new Object(); person.name = "James"; person.age = 33; // bracket notation var person = new Object(); person["name"] = "James"; person["age"] = 33;
To access a value from an object, we can use the []
or .
operator. We can also replace values in our object if the property name exists in the object. Here is where we utilize the key.
person.name; // "James" - dot notation person["name"] // "James" - bracket notation person.name = "Jessica"; // "James" is replaced with "Jessica"
We can also delete a property by using the delete
operator, even though it's rarely done.
person.name; // "Jessica" delete person.name; person.name; // undefined
You might ask yourself, "well should I use bracket or dot notation?". Most often, we use the .
notation because it's more readable. But depending on the situation, we might have to access the values differently. An example, when we would use bracket notation would be if you want to use a variable to access a value like var propertyName = "name"
, followed by person[propertyName]
. Such an approach is not possible with dot notation.
What I just showed you is the basics, but we rarely actually setup an object by adding one property at a time. We almost always use literal syntax, which allows us to write our properties straight away:
var artist = { // object name: "Michael Bolton", // key "name" and value "Michael Bolton" age: 66, // key "age" and value 66 (number) occupation: "singer" // key "occupation" and value "singer" }
Now we have an artist
object, which has three properties. If we look at name: "Michael Bolton"
we can see that we have added a property with a key called "name" which points to the value "Michael Bolton".
1.1 Property Attributes & Property Descriptor
We have covered that a property has a name and a value. However, an object property has more attributes, like writable
, enumerable
, and configurable
, which all default to true (boolean). Before ECMAScript 5, we could not change these attributes, but now with ECMAScript 2015, we may define our properties with a property descriptor.
Object.getOwnPropertyDescriptor returns a property descriptor, which allows us to see the description of a specific property:
var person = { age: 29 } var descriptor = Object.getOwnPropertyDescriptor(person, "age"); console.log(descriptor); // { // value: 29, // writable: true, // enumerable: true, // configurable: true // }
If we want to add or change a property and also have control of these attributes, we can use Object.defineProperty:
var person = {}; Object.defineProperty(person, "age", { value: 29, writable: true, enumerable: true, configurable: true });
1.1.1 Writable
If writable is true, then we may change the value. If it's false, we may not.
var person = {}; Object.defineProperty(person, "age", { value: 29, writable: false, enumerable: true, configurable: true, }); person.age; // 29 person.age = 30; person.age; // 29 - still 29
1.1.2 Enumerable
var person = { name: "carl", age: 20 }; for (key in person) console.log(key);
The following code would print name
and age
. Now if we put a property's attribute enumerable
to false, it would make it non-enumerable. In the example below, it will only print age
.
var person = { name: "carl", age: 20 }; // We redefine the name property's enumerability Object.defineProperty(person, "name", { value: "carl", writable: true, enumerable: false, configurable: true, }); for (key in person) console.log(key);
1.1.3 Configurable
If you do not want to be able to change the descriptor definition, you may set configurable
to false.
var person = { name: "carl" }; Object.defineProperty(person, "name", { value: "carl", writable: true, enumerable: true, configurable: false, }); // We may now not redefine the property Object.defineProperty(person, "name", { value: "carl", writable: true, enumerable: true, configurable: true, });
This results in TypeError: can't redefine non-configurable property "name"
. See that writable is true, so we can still change the value by:
person.name = "phil"; person.name // "phil"
1.2 Accessor Properties - Getter and Setters
We can also define and retrieve values with getters and setters which we refer to as, accessor properties. This means that there are two types of properties, data properties (which we covered above) and accessor properties.
var person = { name: "james", // data property title: "sir", // data property // accessor property get something () {}, set something (value) {} }
A property with a getter method makes it possible to read the property. A setter method makes it possible to write to the property. We can create our own getter by writing a function like shown below. No arguments get passed to the get method, the method itself gets invoked, when we try to get the value from the accessor property.
var person = { get name() { return "james" } } person.name; // "james"
If we would set the value instead, then the setter method would be called, and the value we assign gets passed to the method.
var person = { get name() { return this.something; }, set name(value) { this.something = value; } } person.name = "james"; person.name; // "james"
We use this.something
because else it would go on recursively forever. However, this is just to illustrate the basics. Perhaps you would be more interested in doing something like:
var person = { name: "james", title: "sir", get fullName () { return this.title + " " + this.name; } } person.fullName; // "sir james"
2. Prototypes in JavaScript
A prototype in JavaScript is a property on an object that points to another object. Before we look more into what a prototype is, I would like to show you why we would want to use prototypes.
Right, so imagine the following code:
function Person (name) { this.name = name; this.sayHello = function () { console.log(`Hello my name is ${this.name}`) } } let bill = new Person("Bill"); let carl = new Person("Carl"); console.log(bill); // { name: "Bill", sayHello: sayHello() } console.log(carl); // { name: "Carl", sayHello: sayHello() } bill.sayHello === carl.sayHello; // false
It's pretty straight forward, we have created two similar objects. But there's a big problem with this approach. The problem with this approach is that each time we create a new object, we also create a new function for the object's sayHello
property. This takes up memory. Imagine, for example, creating hundreds of users objects like this, we would be inundated with functions that do the same thing.
A solution to this is to have another object store the function so we can use that object as a reference:
const person = { sayHello () { console.log(`Hello my name is ${this.name}`) } } let bill = Object.create(person); bill.name = "Bill"; let carl = Object.create(person); carl.name = "Carl"; bill.sayHello === carl.sayHello; // true
What we have done here is that by using Object.create we've created two new objects that is linked with the person
object.
This might surprise you, but the bill
object looks like this, { name: "Bill" }
but we can still call sayHello
. This is because the bill
object actually has a property called prototype
. As we've covered, a prototype is a property on an object that points to another object. The pointed object works as a fallback source of properties. Meaning, when we call bill.sayHello
it is not found, so then bill.prototype
gets looked at and there it's found.
This means that we can put functions directly on the prototype if we'd like to do a constructor call on a function to create a new object:
function Person (name) { this.name = name; } Person.prototype.sayHello = function () { console.log(`Hello my name is ${this.name}`); } let bill = new Person("Bill"); let carl = new Person("Carl"); bill.sayHello === carl.sayHello; // true
Now bill
and carl
will be two different objects, but they still point to the same prototype. The prototype has the sayHello
function. Futhermore, since bill
and carl
are two different objects we can, for example, add a function we only want the bill
object to have.
bill.dance = function () { console.log("I'm dancing"); } console.log(bill); // { name: "Bill", dance: dance() } console.log(carl); // { name: "Carl" }
2.1 Delegation & Prototype chain
Up until now, we have covered that prototypes work as a fallback, which is more specifically known as delegation. With delegation, we delegate the responsibility to another object. An object first checks itself “do I have this on me?” if not then it checks its "parent", and this would be repeated until a property is found or not.
var obj1 = { name: "james" } var obj2 = Object.create(obj1); obj2.name = "bill"; var obj3 = Object.create(obj2); obj3.name = "carl";
We have now created a prototype chain that looks like, obj1 <- obj2 <- obj3
. Now let us delete
one property at a time to illustrate delegation.
obj3.name; // carl obj2.name; // bill obj1.name; // james delete obj3.name; obj3.name; // bill obj2.name; // bill obj1.name; // james delete obj2.name; obj3.name; // james obj2.name; // james obj1.name; // james
3. JavaScript has no classes
Now we shift our focus to classes. ES6 gave us the class
keyword, which allows us to use the following syntax:
class Person { constructor(name) { this.name = name; } sayHello () { console.log(`Hello my name is ${this.name}`); } } class Employee extends Person { constructor(name, position) { super(name); this.position = position; } work () { console.log("I'm working now"); } } let bill = new Employee("Bill", "Senior Developer"); let carl = new Employee("Carl", "Junior Developer");
After seeing the code you might think, "why did you waste my time with prototypes?". Well, that's because it's all an illusion because there are no classes in JavaScript. This is because as written in the beginning, JavaScript uses prototype-based programming. But it looks like a class, right? Yes, it does, but it's only syntactic sugar. Before ES6 we would have to do something like this:
function Person (name) { this.name = name; } Person.prototype.sayHello = function () { console.log(`Hello my name is ${this.name}`); } function Employee (name, position) { Person.call(this, name); this.position = position; } Employee.prototype = Object.create(Person.prototype); Employee.prototype.work = function () { console.log("I'm working now"); } let bill = new Employee("Bill", "Senior Developer"); let carl = new Employee("Carl", "Junior Developer");
All of this is still very class-oriented. Another approach could be to use OLOO (objects linked to other objects) which is more focused on knowing how delegation works.
const Person = { init (name) { this.name = name; }, sayHello () { console.log(`Hello my name is ${this.name}`); } } let Employee = Object.create(Person); Employee.setup = function (name, position) { this.init(name); this.position = position; } Employee.work = function () { console.log("I'm working now"); } let bill = Object.create(Employee); bill.setup("Bill", "Senior Developer"); let carl = Object.create(Employee); carl.setup("Carl", "Junior Developer");