JavaScript object-oriented programming

This series of articles is a summary of the front-end boot camp exercises based on freecodecamp

Create a basic JavaScript object

Things we see every day in our lives: cars, shops, birds, etc. They are all objects: physical things that people can observe and interact with.

What are the properties of these objects? Cars have wheels. Stores sell things. Birds have wings.

These features, or properties, define what an object is made of. Note that objects that are similar can have the same attributes, but those attributes may have different values. For example: All cars have wheels, but not all cars have the same number of wheels.

Objects in JavaScript can be used to describe real-world objects and give them properties and behaviors just like their real-world counterparts. Here is an example of using these concepts to create a Duck object:

let duck = {
  name: "Aflac",
  numLegs: 2
};
Copy the code

The Duck object has two sets of key-value pairs: the name property, whose value is Aflac; The other is the numLegs property, which has a value of 2.

Use dot notation to access properties of objects

The last challenge created an object with various properties. Now you will see how to access the values of these properties. Here is an example:

let duck = {
  name: "Aflac",
  numLegs: 2
};
console.log(duck.name);
Copy the code

We can use “dot notation” to access the properties of an object. Duck is followed by a dot and the property name name to call Aflac.

Create methods on objects

Objects can have a special property called method.

Method properties are also functions. This adds a different behavior to the object. Here is an example duck with method attributes:

let duck = {
  name: "Aflac",
  numLegs: 2,
  sayName: function() {return "The name of this duck is " + duck.name + ".";}
};
duck.sayName();
Copy the code

The example adds the sayName method, which returns a sentence containing Duck’s name. Note: This method uses duck.name in the return statement to get the name property value

Use this keyword to improve code reuse

In the last challenge we saw how to set a method on a Duck object. Then in the return statement, we get the name property value by using “dot notation” duck.name:

sayName: function() {return "The name of this duck is " + duck.name + "."; }Copy the code

While this is an effective way to access object properties, there is a catch. If the variable name changes, any code that references the original name needs to be updated. In a short object definition, this is not a problem, but if the object has many references to its properties, the potential for errors is greater.

We can avoid this problem by using the this keyword:

let duck = {
  name: "Aflac",
  numLegs: 2,
  sayName: function() {return "The name of this duck is " + this.name + ".";}
};
Copy the code

This is a very complex topic, and the example above is just one way to use it. In the current context, this refers to the Duck object associated with this method. If you change the variable name of the object to Mallard, there is no need to find everything in the code that points to Duck using this. This makes the code more readable and reusable.

Defining a constructor

Constructors are functions that create objects. The function defines properties and behavior for this new object. Think of them as blueprints for new objects to be created.

Here is an example constructor:

function Bird() {
  this.name = "Albert";
  this.color = "blue";
  this.numLegs = 2;
}
Copy the code

This constructor defines a Bird object whose properties name, color, and numLegs are set to Albert, Blue, and 2, respectively. Constructors follow some common rules:

  • Constructors are capitalized to make it easier to distinguish constructors from other non-constructors.

  • The constructor uses the this keyword to set new properties for the object it will create. Inside the constructor, this points to the object it created.

  • Constructors create objects by defining properties and behaviors, rather than by setting return values like other functions.

Use constructors to create objects

In the last challenge, we created a Bird constructor:

function Bird() {
  this.name = "Albert";
  this.color  = "blue";
  this.numLegs = 2;
}

let blueBird = new Bird();
Copy the code

Note: This in the constructor always refers to the object being created.

Note: When creating objects through constructors, use the new operator. Only then will JavaScript know to create a new instance of the Bird constructor: blueBird. If you do not use the new operator to create a new object, the this in the constructor cannot point to the newly created object instance, resulting in unexpected errors. BlueBird now inherits all the attributes of the Bird constructor as follows:

blueBird.name;
blueBird.color;
blueBird.numLegs;
Copy the code

An instance created by a constructor is like any other object, and its properties can be accessed and modified:

blueBird.name = 'Elvira';
blueBird.name;
Copy the code

Extend the constructor to receive parameters

The Bird constructor worked fine in the last challenge. Notice, however, that all instances of Birds created through the Bird constructor are automatically named Albert, are blue, and have two legs. What if you want your newly created birds to have different names and colors? Of course, it is also possible to manually modify the attributes of each bird instance, but it will increase a lot of unnecessary work:

let swan = new Bird();
swan.name = "Carlos";
swan.color = "white";
Copy the code

Suppose you write a program to track hundreds or even thousands of different birds in an aviary. You’ll spend a lot of time creating all the bird instances and changing their properties to different values. To reduce the effort of creating different Bird objects, you can set your Bird to a constructor that accepts arguments:

function Bird(name, color) {
  this.name = name;
  this.color = color;
  this.numLegs = 2;
}
Copy the code

We then pass the value as an argument to the Bird constructor to define each unique Bird instance: let cardinal = new Bird(“Bruce”, “red”); This assigns the name and color properties of Bird to Bruce and red, respectively. But the numLegs property is still set to 2. Cardinal has the following attributes:

cardinal.name
cardinal.color
cardinal.numLegs
Copy the code

This makes the constructor very flexible. One of the most useful uses of JavaScript constructors is that you can now define properties directly when you create each Bird instance. They group objects into groups based on common or similar properties and behaviors, and can automatically create individual instances

Use instanceof to validate the object’s constructor

Any new object created by a constructor is called instance of the constructor. JavaScript provides an easy way to verify this fact through the instanceof operator. Instanceof allows you to compare objects with constructors, returning true or false depending on whether the object was created by the constructor. Here’s an example:

let Bird = function(name, color) {
  this.name = name;
  this.color = color;
  this.numLegs = 2;
}

let crow = new Bird("Alexis", "black");

crow instanceof Bird;
Copy the code

The instanceof method returns true.

If an object is not created using a constructor, instanceof verifies that the object is not an instanceof the constructor:

let canary = {
  name: "Mildred",
  color: "Yellow",
  numLegs: 2
};

canary instanceof Bird;
Copy the code

The instanceof method returns false.

Know your own attributes

In the following example, the Bird constructor defines two attributes: name and numLegs:

function Bird(name) {
  this.name  = name;
  this.numLegs = 2;
}

let duck = new Bird("Donald");
let canary = new Bird("Tweety");
Copy the code

Name and numLegs are called their own properties because they are defined directly on the instance object. This means that duck and Canary have separate copies of these properties. In fact, all instances of Bird will have separate copies of these properties. The following code stores all of duck’s own properties in an array called ownProps:

let ownProps = [];

for (let property in duck) {
  if(duck.hasOwnProperty(property)) {
    ownProps.push(property);
  }
}

console.log(ownProps);
Copy the code

The console will display the value [“name”, “numLegs”].

Use stereotype attributes to reduce duplicate code

All Bird instances may have the same numLegs value, so there is essentially a repeating variable numLegs in each Bird instance.

This may not be a problem when there are only two instances, but imagine having millions of instances. This creates a lot of duplicate variables.

A better way to solve this problem is to use Bird’s Prototype. Prototype is an object that can be shared between all Bird instances. Here is an example of adding the numLegs property to Bird Prototype:

Bird.prototype.numLegs = 2;
Copy the code

All Bird instances now have the common numLegs property value.

console.log(duck.numLegs);
console.log(canary.numLegs);
Copy the code

Because all instances inherit properties from Prototype, you can think of Prototype as a “recipe” for creating objects. Note: Duck and Canary prototypes belong to Bird’s constructor, bird.prototype. Almost every object in JavaScript has a Prototype property that belongs to its constructor.

Iterate over all attributes

Now you know about the two properties: the self property and the Prototype property. Self properties are defined directly on the object. The Prototype property is defined on Prototype.

function Bird(name) {
  this.name = name;  //own property
}

Bird.prototype.numLegs = 2; // prototype property

let duck = new Bird("Donald");
Copy the code

This example will show you how to add duck’s own properties and prototype properties to the ownProps array and prototypeProps array, respectively:

let ownProps = [];
let prototypeProps = [];

for (let property in duck) {
  if(duck.hasOwnProperty(property)) {
    ownProps.push(property);
  } else {
    prototypeProps.push(property);
  }
}

console.log(ownProps);
console.log(prototypeProps);
Copy the code

Console. log(ownProps) will display [“name”] in the console, and console.log(prototypeProps) will display [“numLegs”].

Learn about constructor properties

The instance object duck created in the previous challenge has a special constructor property:

let duck = new Bird();
let beagle = new Dog();

console.log(duck.constructor === Bird); 
console.log(beagle.constructor === Dog);
Copy the code

Both console.log calls will show true in the console.

Note that the constructor property is a reference to the constructor that created the instance. A benefit of the constructor property is that you can examine the property to find out what object it is. Here’s an example to see how it works:

function joinBirdFraternity(candidate) { if (candidate.constructor === Bird) { return true; } else { return false; }}Copy the code

Note: Since the constructor property can be overridden, it is best to use the Instanceof method to check the type of the object.

Change the stereotype to the new object

So far, you can add attributes to Prototype separately:

Bird.prototype.numLegs = 2;
Copy the code

You need to add more than one attribute, which can be a drag.

Bird.prototype.eat = function() {
  console.log("nom nom nom");
}

Bird.prototype.describe = function() {
  console.log("My name is " + this.name);
}
Copy the code

A more efficient approach is to set the object’s Prototype to a new object that already contains attributes. This way, all attributes can be added at once:

Bird.prototype = { numLegs: 2, eat: function() { console.log("nom nom nom"); }, describe: function() { console.log("My name is " + this.name); }};Copy the code

When changing the stereotype, remember to set the constructor properties

Manually setting up a prototype for a new object has an important side effect. It clears the constructor attribute! This property can be used to check which constructor created the instance, but since this property has been overridden, it now gives the wrong result:

duck.constructor === Bird;
duck.constructor === Object;
duck instanceof Bird;
Copy the code

These expressions return false, true, and true in that order.

To solve this problem, anyone who manually resets a prototype object for a new object should remember to define a constructor property in the prototype object:

Bird.prototype = { constructor: Bird, numLegs: 2, eat: function() { console.log("nom nom nom"); }, describe: function() { console.log("My name is " + this.name); }};Copy the code

Where did the prototype of the learned object come from

Just as people inherit genes from their parents, an object can inherit its prototype directly from the constructor that created it. Consider the following example: the Bird constructor creates a duck object:

function Bird(name) {
  this.name = name;
}

let duck = new Bird("Donald");
Copy the code

Duck inherits its prototype from the Bird constructor. You can use the isPrototypeOf method to verify the relationship between them:

Bird.prototype.isPrototypeOf(duck);
Copy the code

This will return true

Understanding the prototype chain

All objects in JavaScript (with a few exceptions) have their own prototype. Also, the object’s prototype is itself an object.

function Bird(name) {
  this.name = name;
}

typeof Bird.prototype;
Copy the code

Because Prototype is an object, the prototype object has its own prototype! The prototype of Bird. Prototype is Object.

Object.prototype.isPrototypeOf(Bird.prototype);
Copy the code

What does that do? You may remember the hasOwnProperty method we learned from the previous challenge:

let duck = new Bird("Donald");
duck.hasOwnProperty("name"); 
Copy the code

HasOwnProperty is a method defined on Object.prototype. Although it is not defined on bird. prototype and duck, it can still be invoked on both objects. This is an example of the Prototype chain. In this prototype chain, Bird is duck’s supertype and Duck is duck’s subtype. Object is the supertype common to both Bird and Duck instances. Object is the supertype of all objects in JavaScript, which is the top level of the prototype chain. Therefore, all objects have access to the hasOwnProperty method.

Use inheritance to avoid duplication

There’s a rule called: Don’t Repeat Yourself. Often used in the abbreviated DRY form, meaning “don’t repeat yourself.” The problem with writing duplicate code is that any change requires going to multiple places to fix all the duplicate code. This usually means we need to do more work, resulting in a higher error rate.

Look at the following example where Bird and Dog share the describe method:

Bird.prototype = { constructor: Bird, describe: function() { console.log("My name is " + this.name); }}; Dog.prototype = { constructor: Dog, describe: function() { console.log("My name is " + this.name); }};Copy the code

We can see that the Describe method is repeatedly defined in two places. Following the DRY principle mentioned above, we can rewrite this code by creating an Animal SuperType (or superclass) :

function Animal() { }; Animal.prototype = { constructor: Animal, describe: function() { console.log("My name is " + this.name); }};Copy the code

The describe method is defined in the Animal constructor. The Bird and Dog constructor methods can be removed:

Inherit behavior from a superclass

In the last challenge, we created an Animal supertype that defines the behavior shared by all animals:

function Animal() { }

Animal.prototype.eat = function() {
  console.log("nom nom nom");
};
Copy the code

In this section and the next challenge we’ll learn how to reuse Animal’s methods in Bird and Dog without having to redefine them. Here we use the inheritance property of constructors. In this challenge we learn the first step: create an instance of the supertype (or superclass). You have learned one way to create an instance of Animal using the new operator:

let animal = new Animal();
Copy the code

This syntax has some drawbacks when used for inheritance that are too complex for our current challenge. Instead, we’ll learn another way to replace the new operation without these drawbacks:

let animal = Object.create(Animal.prototype);
Copy the code

Object.create(obj) creates a new Object and specifies obj as the prototype of the new Object. Recall that Prototype is like a “recipe” for creating objects. If we set animal’s prototype to the same prototype as animal’s constructor, we will have the same formula for animal as any other animal instance.

animal.eat();
animal instanceof Animal;
Copy the code

The instanceof method returns true

Set the archetype of the child as an instance of the parent

In the previous challenge, we learned the first step in inheriting behavior from the superclass (or superclass) Animal: create a new instance of Animal.

This section challenges us to learn the second step: setting prototype for a subtype (or subclass). So Bird is an example of Animal.

Bird.prototype = Object.create(Animal.prototype);
Copy the code

Remember that Prototype is like a “recipe” for creating objects. In a sense, the Bird object recipe contains all the key “ingredients” of Animal.

let duck = new Bird("Donald");
duck.eat();
Copy the code

Duck inherits all Animal properties, including the eat method.

Resets an inherited constructor property

When an object inherits its Prototype from another object, it also inherits its parent class’s constructor property.

Look at the examples below

function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
let duck = new Bird();
duck.constructor
Copy the code

But Duck and all other instances of Bird should show that they were created by Bird, not Animal. To do this, you can manually set Bird’s constructor property to the Bird object:

Bird.prototype.constructor = Bird;
duck.constructor
Copy the code

Add methods after inheritance

A constructor that inherits its Prototype object from a superclass constructor can have its own methods in addition to the inherited ones.

Bird is a constructor that inherits Animal prototype:

function Animal() { }
Animal.prototype.eat = function() {
  console.log("nom nom nom");
};
function Bird() { }
Bird.prototype = Object.create(Animal.prototype);
Bird.prototype.constructor = Bird;
Copy the code

In addition to the behavior inherited from the Animal constructor, you need to add behavior unique to the Bird object. Here, we add a fly() function to the Bird object. The function is added to Bird’s Prototype in the same way as the other constructors:

Bird.prototype.fly = function() { console.log("I'm flying!" ); };Copy the code

The Bird example now has the eat() and fly() methods:

let duck = new Bird();
duck.eat();
duck.fly();
Copy the code

Duck.eat () will display the string nom nom nom, duck.fly() will display the string I’m flying! .

Override inherited methods

In the previous challenge, we learned that an object can inherit its properties and behavior (or methods) by referring to another object’s Prototype:

ChildObject.prototype = Object.create(ParentObject.prototype);
Copy the code

ChildObject then links its method to its Prototype:

ChildObject.prototype.methodName = function() {... };Copy the code

We can also override inherited methods. Add methods to Childobject. prototype in the same way – by using the same method name as the one you want to override. Bird overrides the eat() method inherited from Animal:

function Animal() { }
Animal.prototype.eat = function() {
  return "nom nom nom";
};
function Bird() { }

Bird.prototype = Object.create(Animal.prototype);

Bird.prototype.eat = function() {
  return "peck peck peck";
};
Copy the code

If you have an example: let duck = new Bird(); Then you call duck.eat(), and here’s how JavaScript looks for methods on Duck’s prototype chain:

  1. duck= >eat()Is the definition here? It isn’t.
  2. Bird= >eat()Is the definition here? = > yes. Execute it and stop searching up.
  3. Animal=> This is also defined hereeat()Method, but JavaScript stops searching before reaching this layer of the prototype chain.
  4. Object= >JavaScriptThe search stopped before reaching this level of the prototype chain.

Use mixins to add common behavior between unrelated objects

As you can see, behaviors can be shared through inheritance. However, there are cases where inheritance is not the best solution. Inheritance does not apply to unrelated objects, such as Bird and Airplane. Although they can both fly, Bird is not an Airplane and vice versa.

For unrelated objects, a better approach is to use mixins. Mixins allow other objects to use collections of functions.

let flyMixin = function(obj) {
  obj.fly = function() {
    console.log("Flying, wooosh!");
  }
};
Copy the code

FlyMixin can take any object and provide it with the Fly method.

let bird = {
  name: "Donald",
  numLegs: 2
};

let plane = {
  model: "777",
  numPassengers: 524
};

flyMixin(bird);
flyMixin(plane)
Copy the code

The flyMixin here takes the Bird and plane objects and assigns the fly method to each of them. Bird and plane can now fly:

bird.fly();
plane.fly();
Copy the code

The console will display the string Flying, wooosh! Twice, each.fly() call is displayed.

Notice how mixins allow the same fly method to be reused by unrelated objects bird and plane.

Use closures to protect properties within an object from external modification

In the last challenge, Bird had a public property name. The definition of a public property is that it can be accessed and changed outside the scope of Bird’s definition

bird.name = "Duffy";
Copy the code

Thus, bird’s name property can be easily changed to any value anywhere in the code. Think of things like passwords and bank accounts if any part of the code base could easily change them. That would cause a lot of problems.

The easiest way to privatize attributes is to create variables in constructors. You can limit the scope of this variable to the constructor instead of making it available globally. In this way, properties can only be accessed and changed by methods in the constructor.

function Bird() {
  let hatchedEgg = 10;

  this.getHatchedEggCount = function() { 
    return hatchedEgg;
  };
}
let ducky = new Bird();
ducky.getHatchedEggCount();
Copy the code

Here getHatchedEggCount is a privileged method because it has access to the private hatchedEgg property. This is because hatchedEgg is declared in the same context as getHatchedEggCount. In JavaScript, a function always has access to the context in which it was created. This is called closure.

Understand the Immediate Function Expression (IIFE)

A common pattern in JavaScript is for functions to be executed immediately after declaration:

(function () { console.log("Chirp, chirp!" ); }) ();Copy the code

This is an anonymous function expression that immediately executes and prints Chirp, Chirp! . Note that functions do not have names and are not stored in variables. The two parentheses () at the end of a function expression cause it to be executed or called immediately. The pattern is called the Immediately Invoked Function Expression or IIFE.

Create a module using IIFE

An immediate function expression (IIFE) is usually used to group related functions into a single object or module. For example, the previous challenge defined a mixin:

function flyMixin(obj) { obj.fly = function() { console.log("Flying, wooosh!" ); }; }Copy the code

We can divide these mixins into the following modules:

let motionModule = (function () {
  return {
    flyMixin: function(obj) {
      obj.fly = function() {
        console.log("Flying, wooosh!");
      };
    }
  }
})();
Copy the code

Note: an immediate function expression (IIFE) returns a motionModule object. The returned object contains all mixin behavior as an object property. The advantage of the Module pattern is that all motion-related behavior can be packaged into a single object that can then be used by other parts of the code. Here’s an example of how to use it:

motionModule.flyMixin(duck);
duck.fly();
Copy the code