Skip to main content

Objects and Object Constructor

Resource

I. Object-literal syntax

There are multiple ways to define an object, but in most cases, it’s best to use object-literal syntax. Object-literal creates a single, unique object.

// object literal syntax
const myObject = {
property: 'Value!',
otherProperty: 77,
"my method": function() {
// do stuff!
}
};

We can use dot notation or bracket notation to access information out of an object.

// dot notation
myObject.property; // 'Value!'

// bracket notation
myObject["my method"]; // [Function: my method]

Objects as a design pattern

Using objects as a design pattern helps organize and manage data more efficiently, especially as the complexity of your codebase increases.

Example: A game of Tic-Tac-Toe

  • Without objects:
const playerOneName = "tim";
const playerTwoName = "jenn";
const playerOneMarker = "X";
const playerTwoMarker = "O";
  • With objects: using object literals.
const playerOne = {
name: "tim",
marker: "X"
};

const playerTwo = {
name: "jenn",
marker: "O"
};
// Benefits of using objects:

// Function to print any player's name without needing to know the specific variable name
function printName(player) {
console.log(player.name);
}

// Function to announce the winner of the game
function gameOver(winningPlayer) {
console.log(`Congratulations! ${winningPlayer.name} is the winner!`);
}

// Call the functions with the objects as parameters
printName(playerOne); // Output: tim
gameOver(playerTwo); // Output: Congratulations! jenn is the winner!

→ The first approach might seem straightforward, but the object-based approach offers significant benefits.

II. Object constructors

When you want multiple instances of a specific type of object, a better way to create them is using an object constructor.

An object constructor is a special function in JavaScript used to create and initialize an object. It defines a blueprint for creating multiple instances of objects with the same structure.

→ Each instance of this object created by the object constructor will share the same properties and methods defined in the constructor, allowing for the efficient creation and management of objects with similar characteristics.

// declaring an object constructor initializing object Player
function Player(name, marker) {
this.name = name;
this.marker = marker;
}

// object constructor created using a function expression
// very UNCOMMON because this won't be hoisted
const Player = function(name, marker){
this.name = name;
this.marker = marker;
}
  • You must use the keyword new to create an object instance with the constructor, otherwise you’ll get unexpected behavior/errors.
// calling the function with the keyword `new`
// a new instance player1 of Player is created
const player1 = new Player('steve', 'X');
console.log(player1.name); // 'steve'

The object constructor syntax allows the creation of multiple instances of the same type of object, while the object literal syntax creates a single object.

// object literal
// create a single object
const player = {
name: 'steve',
marker: 'X',
sayName: function() {
console.log(this.name);
}
};
// object constructor syntax
// allows creation of multiple instances of the same object
function Player(name, marker) {
this.name = name;
this.marker = marker;
this.sayName = function() {
console.log(this.name);
};
}

const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');

player1.sayName(); // logs 'steve'
player2.sayName(); // logs 'also steve'

this keyword

The this keyword in JavaScript is a special identifier that refers to the current execution context.

  1. Global context:
console.log(this === window); // true in a browser

this.globalVariable = "I'm global";
console.log(window.globalVariable); // "I'm global"
  1. Method call: When a function is called as a method of an object, this refers to the object that owns the method.
const obj = {
method: function() {
console.log(this);
}
};
obj.method(); // `this` refers to `obj`
  1. Constructor call: When a function is used as a constructor with the new keyword, this refers to the newly created object.
function Person(name) {
this.name = name;
this.introduce = function() {
console.log(`My name is ${this.name}`);
};
}

const alice = new Person('Alice');
alice.introduce(); // Output: My name is Alice
  1. Arrow functions: Arrow functions don't have their own this. They inherit this from the enclosing lexical scope.
const obj = {
name: 'Object',
regularFunction: function() {
console.log(`Regular function: ${this.name}`);

const arrowFunction = () => {
console.log(`Arrow function: ${this.name}`);
};

arrowFunction();
}
};

obj.regularFunction();
// Output:
// Regular function: Object
// Arrow function: Object

Note: Using arrow functions as constructors is not recommended and will not work as expected. Arrow functions do not have their own this context, which is crucial for constructors.

III. The prototype

Resource

A prototype is another object that the original object inherits from, meaning the original object has access to all its prototype's methods and properties.

a. All objects in JavaScript have a prototype.

  • You can check the object’s prototype by using the Object.getPrototypeOf() function on the object, e.g. Object.getPrototypeOf(person1).

b. The prototype itself is an object…

When you create a new Person object (like person1 or person2), JavaScript automatically sets up a link between the newly created objects and Person.prototype. This link is what we call the object's prototype.

  • Hence, you get a true value returned when you check the Objects prototype - Object.getPrototypeOf(player1) === Player.prototype.

c. …that the original object inherits from, and has access to all of its prototype’s methods and properties

  • This means that any methods or properties defined on Person.prototype are accessible to all objects (person1, person2) created from the Person constructor.

Example:

  • We define a Person constructor function.
  • We define the .sayHello function on the Player.prototype object.

→ It then became available for the player1 and the player2 objects to use. Similarly, you can attach other properties or functions you want to use on all Player objects by defining them on the objects’ prototype (Player.prototype).

// create an object with object constructor
function Person(name) {
this.name = name;
}

// Adding a method to the Person.prototype
Person.prototype.sayHello = function() {
console.log("Hello, I'm " + this.name);
};

// Creating objects
const person1 = new Person("Alice");
const person2 = new Person("Bob");

// Using the prototype method
person1.sayHello(); // Output: Hello, I'm Alice
person2.sayHello(); // Output: Hello, I'm Bob

// Checking the prototype
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person2) === Person.prototype); // true

To further illustrate:

console.log(person1.hasOwnProperty('sayHello')); // false 
console.log('sayHello' in person1); // true

→ This shows that sayHello isn't a direct property of person1, but person1 can still access it through its prototype.

1. Object.getPrototypeOf() vs. .__proto__ vs. [[Prototype]] vs. instanceof

Resource

a. Object.getPrototypeOf()

You can check the object’s immediate prototype by using the Object.getPrototypeOf() method on the object. This is the recommended way.

Object.getPrototypeOf(player1) === Player.prototype; // true
Object.getPrototypeOf(player2) === Player.prototype; // true

b. ._proto_

Unlike using Object.getPrototypeOf() to access an object’s prototype, the same thing can also be done using the .__proto__ property of the object. However, this is a non-standard way of doing so and is deprecated. Hence, it is NOT recommended to access an object’s prototype by using this property.

// Don't do this!
player1.__proto__ === Player.prototype; // returns true
player2.__proto__ === Player.prototype; // returns true

c.[[Prototype]]

In some places, you might come across [[Prototype]], which is another way of talking about the .__proto__ property of an object, like player1.[[Prototype]].

d. instanceof operator

You can use instanceof to check if an object is an instance of a particular class or constructor function. The operator checks the entire chain (unlike Object.getPrototypeOf() that only checks the one level up, immediate prototype).

You can implement instanceof using Object.getPrototypeOf():

function isInstanceOf(object, constructor) {
let proto = Object.getPrototypeOf(object);

while (proto !== null) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}

return false;
}

// Usage
const arr = [];
console.log(isInstanceOf(arr, Array)); // true
console.log(isInstanceOf(arr, Object)); // true
Example:
// array protoype chain
arr ---> Array.prototype ---> Object.prototype ---> null

// tesla prototype chain
tesla ---> Car.prototype ---> Object.prototype ---> null
const arr = [];
console.log(arr instanceof Array); // true
console.log(arr instanceof Object); // true (arrays inherit from Object)

// object constructor named Car
function Car(brand){
this.brand = brand;
}

const tesla = new Car('Tesla');
console.log(tesla instanceof Car); // true
console.log(tesla instanceof Object); // true

You can verify this chain using Object.getPrototypeOf():

console.log(Object.getPrototypeOf(arr) === Array.prototype);  // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true

console.log(Object.getPrototypeOf(tesla) === Car.prototype); // true
console.log(Object.getPrototypeOf(Car.prototype) === Object.prototype); // true

2. Prototypal Inheritance*

Resource

Prototypal Inheritance is when objects can access and use properties/methods from their prototype objects. When an object is created, it is automatically linked to its prototype object.

Every prototype object inherits from Object.prototype by default.

Example:

  1. player1 and player2 inherit from Player.prototype.
  2. Player.prototype itself inherits from Object.prototype, like every prototype object.
  3. This forms a chain: player1 -> Player.prototype -> Object.prototype
// Player.prototype.__proto__
Object.getPrototypeOf(Player.prototype) === Object.prototype; // true

// Output may slightly differ based on the browser
player1.valueOf(); // Output: Object { name: "steve", marker: "X", sayName: sayName() }

In this example, we are using the valueOf() function, we didn’t define it but it is defined on Object.prototype. player1 object inherits this function from Player.prototype, which inherits it from Object.prototype.

player1.hasOwnProperty('valueOf'); // false
Object.prototype.hasOwnProperty('valueOf'); // true

Same with .hasOwnProperty function, this function is also defined on the Object.prototype.

Object.prototype.hasOwnProperty('hasOwnProperty'); // true

This is how JavaScript utilizes prototype - by having the objects contain a value - to point to prototypes and inherit from those prototypes, thus forming a chain. JavaScript figures out which properties exist (or do not exist) on the object and starts traversing the chain to find the property or function.

However, this chain does not go on forever. If you have already tried logging the value of Object.getPrototypeOf(Object.prototype), you will find that it is null, which indicates the end of the chain. At the end of this chain, if the specific property or function is not found, undefined is returned.

a. Defining Methods in Object Constructor

function Player(name, marker) {
this.name = name;
this.marker = marker;
this.sayName = function() { // Method defined inside the constructor
console.log(this.name);
};
}

const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');

The sayName function is defined for each instance of Player. This means every single instance of Player (like player1 and player2) will have its copy of the sayName method.

While this works, it could be more memory-efficient, especially if you want to create many instances of Player, because then each instance will have its own copy of sayName.

b. Defining Methods in Prototype

function Player(name, marker) {
this.name = name;
this.marker = marker;
}

// Adding the method to the prototype
Player.prototype.sayName = function() { // Method defined on the prototype
console.log(this.name);
};

const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');

The sayName function is shared by all instances of Player. When you create player1 and player2, they will both reference the same sayName method defined on Player.prototype, rather than having their own copies of it.

→ Defining property and function takes up a lot of memory (especially if you have a lot of common properties and functions), and a lot of created objects. That’s why, defining them on a centralized, property object that the objects have access to is much more memory-efficient.

In this example:

  • We define a Person from whom a Player inherits properties and functions.
  • We use Object.setPrototypeOf() is used to establish this inheritance relationship between Player and Person.
  • Then, we create object instances player1 and player2 which inherits from both Person and Player.
    • player1 and player2 both have access to getMarker(), sayName() methods.
    • Use methods like toString() from Object.prototype().
// prototype chain
player1, player2 ---> Player.prototype --> Person.prototype --> Object.prototype ---> null
// object constructor - defines Person object
function Person(name) {
this.name = name;
}

// object constructor - defines Player object
function Player(name, marker) {
this.name = name;
this.marker = marker;
}

// defines the sayName method on Person.prototype
Person.prototype.sayName = function() {
console.log(`Hello, I'm ${this.name}!`);
};

// defines the getMarker method on Player.prototype
Player.prototype.getMarker = function() {
console.log(`My marker is '${this.marker}'`);
};

Object.getPrototypeOf(Player.prototype); // returns Object.prototype

// Now make `Player` objects inherit from `Person`
// do this BEFORE creating new object instances
Object.setPrototypeOf(Player.prototype, Person.prototype);
Object.getPrototypeOf(Player.prototype); // returns Person.prototype

// create object instances
const player1 = new Player('steve', 'X');
const player2 = new Player('also steve', 'O');

// player1 also inherits sayName method from Person
player1.sayName(); // Hello, I'm steve!
player2.sayName(); // Hello, I'm also steve!

// player1 inherits getMarker method from Player
player1.getMarker(); // My marker is 'X'
player2.getMarker(); // My marker is 'O'

setPrototypeOf()

Though it seems easy to set up prototypal inheritance using Object.setPrototypeOf(), the prototype chain must be set up using this function before creating any objects. Using setPrototypeOf() after objects instances have already been created can result in performance issues.

Example 1: This will set Player.prototype to directly refer to Person.prototype (i.e. not a copy). Any changes you make to the Person.prototype will get reflected in Player.prototype.

// this does not work
// DO NOT, EVER reassign like this
Player.prototype = Person.prototype;

Example 2: If we had used Object.setPrototypeOf() in this example, then we could safely edit the Enemy.prototype.sayName function without changing the function for Player as well.

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

function Player(name, marker) {
this.name = name;
this.marker = marker;
}

Person.prototype.sayName = function() {
console.log(`Hello, I'm ${this.name}!`);
};

// Don't do this!
// Use Object.setPrototypeOf(Player.prototype, Person.prototype)
Player.prototype = Person.prototype;

// another object constructor Enemy
function Enemy(name) {
this.name = name;
this.marker = '^';
}

// Not again!
// Use Object.setPrototypeOf(Enemy.prototype, Person.prototype)
Enemy.prototype = Person.prototype;

Enemy.prototype.sayName = function() {
console.log('HAHAHAHAHAHA');
};

const carl = new Player('carl', 'X');
carl.sayName(); // Uh oh! this logs "HAHAHAHAHAHA" because we edited the sayName function!