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.
- Global context:
console.log(this === window); // true in a browser
this.globalVariable = "I'm global";
console.log(window.globalVariable); // "I'm global"
- 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`
- 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
- Arrow functions: Arrow functions don't have their own
this
. They inheritthis
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 theObject.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 thePerson
constructor.
Example:
- We define a
Person
constructor function. - We define the
.sayHello
function on thePlayer.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
// 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 fromObject.prototype
by default.
Example:
player1
andplayer2
inherit fromPlayer.prototype
.Player.prototype
itself inherits fromObject.prototype
, like everyprototype
object.- 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 toprototype
s 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 isnull
, 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.
3. Recommended method for prototypal inheritance
In this example:
- We define a
Person
from whom aPlayer
inherits properties and functions. - We use
Object.setPrototypeOf()
is used to establish this inheritance relationship betweenPlayer
andPerson
. - Then, we create object instances
player1
andplayer2
which inherits from bothPerson
andPlayer
.player1
andplayer2
both have access togetMarker()
,sayName()
methods.- Use methods like
toString()
fromObject.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!