Creating objects
Creating objects in JavaScript offers several methods:
- Object literals (
{}): Simplest and most popular approach. Define key-value pairs within curly braces. Object()constructor: Usenew Object()with dot notation to add properties.Object.create(): Create new objects using existing objects as prototypes, inheriting properties and methods.- Constructor functions: Define blueprints for objects using functions,
creating instances with
new. - ES2015 classes: Structured syntax similar to other languages, using
classandconstructorkeywords.
The new keyword
The new keyword in JavaScript is used to create an instance of a user-defined
object type or one of the built-in object types that has a constructor function.
When you use new, it does four things: it creates a new object, sets the
prototype, binds this to the new object, and returns the new object.
Copying objects
To copy an object or an array in JavaScript, you can use several methods. For
shallow copies, you can use the spread operator (...) or Object.assign().
For deep copies, you can use JSON.parse(JSON.stringify()) or libraries like
Lodash’s _.cloneDeep().
Example
// Shallow copy of an array
const originalArray = [1, 2, 3];
const shallowCopyArray = [...originalArray];
// Shallow copy of an object
const originalObject = { a: 1, b: 2 };
const shallowCopyObject = { ...originalObject };
// Deep copy using JSON methods
const deepCopyObject = JSON.parse(JSON.stringify(originalObject));Prototypes
Prototype Chain
Prototypical inheritance in JavaScript is a way for objects to inherit
properties and methods from other objects. Every JavaScript object has a special
hidden property called [[Prototype]] (commonly accessed via __proto__ or
using Object.getPrototypeOf()) that is a reference to another object, which is
called the object’s “prototype”. Instead of a class-based inheritance model,
JavaScript uses a prototype-based model, where objects can directly inherit from
other objects.
When a property is accessed on an object and if the property is not found on
that object, the JavaScript engine looks at the object’s __proto__, and the
__proto__’s __proto__ and so on, until it finds the property defined on one
of the __proto__s or until it reaches the end of the prototype chain.
When you try to access a property or method on an object, JavaScript will:
- Look for the property or method on the object itself.
- If it doesn’t find it, it will look at the object’s prototype.
- If it still doesn’t find it, it will look at the prototype’s prototype.
- This process continues until it either finds the property or method or
reaches the end of the chain, which is
null.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person("Alice");
alice.greet(); // "Hello, my name is Alice"In this example:
aliceis an instance ofPerson.alicedoes not have agreetmethod directly on it.- JavaScript looks at
alice’s prototype, which isPerson.prototype. Person.prototypehas agreetmethod, so JavaScript calls it.
JavaScript’s built-in objects also use the prototype chain. For example, arrays
inherit from Array.prototype, which in turn inherits from Object.prototype.
const arr = [1, 2, 3];
console.log(arr.toString()); // "1,2,3"In this example:
arris an instance ofArray.arrdoes not have atoStringmethod directly on it.- JavaScript looks at
arr’s prototype, which isArray.prototype. Array.prototypehas atoStringmethod, so JavaScript calls it.
You can add properties and methods to an object’s prototype, and all instances of that object will have access to those properties and methods.
Person.prototype.sayGoodbye = function () {
console.log(`Goodbye from ${this.name}`);
};
alice.sayGoodbye(); // "Goodbye from Alice"In this example, we added a sayGoodbye method to Person.prototype, and now
alice can use it.
Extending built-in objects
Extending a built-in/native JavaScript object means adding properties/functions to its prototype. While this may seem like a good idea at first, it is dangerous in practice. Imagine your code uses a few libraries that both extend the Array.prototype by adding the same contains method, the implementations will overwrite each other and your code will have unpredictable behavior if these two methods do not work the same way.
The only time you may want to extend a native object is when you want to create a polyfill, essentially providing your own implementation for a method that is part of the JavaScript specification but might not exist in the user’s browser due to it being an older browser.
Prototypal Inheritance
- Prototypes : Every object in Javascript has a prototype, which is another
object. When you create an object using an object literal or a constructor
function, the new object is linked to the prototype of its constructor
function or the
Object.prototypeif no prototype is specified. This is commonly referenced using__proto__or[[Prototype]]. You can also get the prototype by using inbuilt methodObject.getPrototypeOf()and you can set the prototype of an object viaObject.setPrototypeOf().
Example
// Define a constructor function
function Person(name, age) {
this.name = name;
this.age = age;
}
// Add a method to the prototype
Person.prototype.sayHello = function () {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
};
// Create a new object using the constructor function
let john = new Person("John", 30);
// The new object has access to the methods defined on the prototype
john.sayHello(); // "Hello, my name is John and I am 30 years old."
// The prototype of the new object is the prototype of the constructor function
console.log(john.__proto__ === Person.prototype); // true
// You can also get the prototype using Object.getPrototypeOf()
console.log(Object.getPrototypeOf(john) === Person.prototype); // true
// You can set the prototype of an object using Object.setPrototypeOf()
let newProto = {
sayGoodbye: function () {
console.log(`Goodbye, my name is ${this.name}`);
},
};
Object.setPrototypeOf(john, newProto);
// Now john has access to the methods defined on the new prototype
john.sayGoodbye(); // "Goodbye, my name is John"
// But no longer has access to the methods defined on the old prototype
console.log(john.sayHello); // undefined-
Prototype chain: When a property or method is accessed on an object, JavaScript first looks for it on the object itself. If it doesn’t find it there, it looks at the object’s prototype, and then the prototype’s prototype, and so on, until it either finds the property or reaches the end of the chain (i.e.,
null). -
Constructor functions: JavaScript provides constructor functions to create objects. When a function is used as a constructor with the new keyword, the new object’s prototype (
[[Prototype]]) is set to the constructor’s prototype property.
Example
// Define a constructor function
function Animal(name) {
this.name = name;
}
// Add a method to the prototype
Animal.prototype.sayName = function () {
console.log(`My name is ${this.name}`);
};
// Define a new constructor function
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
// Set the prototype of Dog to be a new instance of Animal
Dog.prototype = Object.create(Animal.prototype);
// Add a method to the Dog prototype
Dog.prototype.bark = function () {
console.log("Woof!");
};
// Create a new object using the Dog constructor function
let fido = new Dog("Fido", "Labrador");
// The new object has access to the methods defined on its own prototype and the Animal prototype
fido.bark(); // "Woof!"
fido.sayName(); // "My name is Fido"
// If we try to access a method that doesn't exist on the Dog prototype or the Animal prototype, JavaScript will return undefined
console.log(fido.fly); // undefinedObject.create(): This method creates a new object with the specified prototype object and properties. It’s a straightforward way to set up prototypical inheritance. If you create a object viaObject.create(null)it will not inherit any properties fromObject.prototype. This means the object will not have any built-in properties or methods liketoString(),hasOwnProperty(),
Example
// Define a prototype object
let proto = {
greet: function () {
console.log(`Hello, my name is ${this.name}`);
},
};
// Use `Object.create()` to create a new object with the specified prototype
let person = Object.create(proto);
person.name = "John";
// The new object has access to the methods defined on the prototype
person.greet(); // "Hello, my name is John"
// Check if the object has a property
console.log(person.hasOwnProperty("name")); // true
// Create an object that does not inherit from Object.prototype
let animal = Object.create(null);
animal.name = "Rocky";
// The new object does not have any built-in properties or methods
console.log(animal.toString); // undefined
console.log(animal.hasOwnProperty); // undefined
// But you can still add and access custom properties
animal.describe = function () {
console.log(`Name of the animal is ${this.name}`);
};
animal.describe(); // "Name of the animal is Rocky"Classical vs Prototypical Inheritance
Classical inheritance is a pattern used in many object-oriented programming languages like Java, C++, and Python. It involves creating a class hierarchy where classes inherit properties and methods from other classes.
- Class definition: You define a class with properties and methods.
- Instantiation: You create instances (objects) of the class.
- Inheritance: A class can inherit from another class, forming a parent-child relationship.
Example in Java
class Animal {
void eat() {
System.out.println("This animal eats food.");
}
}
class Dog extends Animal {
void bark() {
System.out.println("The dog barks.");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // Inherited method
dog.bark(); // Own method
}
}Prototypal inheritance is a feature of JavaScript where objects inherit directly from other objects. There are no classes; instead, objects serve as prototypes for other objects.
- Object creation: You create an object directly.
- Prototype chain: Objects can inherit properties and methods from other objects through the prototype chain.
- Flexibility: You can dynamically add or modify properties and methods.
Example in JavaScript
const animal = {
eat() {
console.log("This animal eats food.");
},
};
const dog = Object.create(animal);
dog.bark = function () {
console.log("The dog barks.");
};
dog.eat(); // Inherited method
dog.bark(); // Own method- Class-based vs. prototype-based: Classical inheritance uses classes, while prototypal inheritance uses objects.
- Inheritance model: Classical inheritance forms a class hierarchy, whereas prototypal inheritance forms a prototype chain.
- Flexibility: Prototypal inheritance is more flexible and dynamic, allowing for changes at runtime.
Prototype Pattern
The Prototype pattern is a creational design pattern that allows you to create new objects by copying an existing object, known as the prototype. This pattern is particularly useful when the cost of creating a new object is more expensive than cloning an existing one.
In JavaScript, the Prototype pattern can be implemented using the
Object.create method or by using the prototype property of a constructor
function.
Using `Object.create`
The Object.create method creates a new object with the specified prototype
object and properties.
const prototypeObject = {
greet() {
console.log("Hello, world!");
},
};
const newObject = Object.create(prototypeObject);
newObject.greet(); // Outputs: Hello, world!In this example, newObject is created with prototypeObject as its prototype.
This means that newObject inherits the greet method from prototypeObject.
Using constructor functions and the `prototype` property
Another way to implement the Prototype pattern in JavaScript is by using
constructor functions and the prototype property.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person("Alice");
const person2 = new Person("Bob");
person1.greet(); // Outputs: Hello, my name is Alice
person2.greet(); // Outputs: Hello, my name is BobIn this example, the Person constructor function is used to create new
Person objects. The greet method is added to the Person.prototype, so all
instances of Person inherit this method.
Advantages:
- Reduces the cost of creating new objects by cloning existing ones
- Simplifies the creation of complex objects
- Promotes code reuse and reduces redundancy
Disadvantages:
- Cloning objects can be less efficient than creating new ones in some cases
- Can lead to issues with deep cloning if the prototype object contains nested objects
Classes
ES2015 introduces a new way of creating classes, which provides a more intuitive and concise way to define and work with objects and inheritance compared to the ES5 function constructor syntax.
Comparison of ES5 function constructors vs ES2015 classes
| Feature | ES5 Function Constructor | ES2015 Class |
|---|---|---|
| Syntax | Uses function constructors and prototypes | Uses class keyword |
| Constructor | Function with properties assigned using this | constructor method inside the class |
| Method Definition | Defined on the prototype | Defined inside the class body |
| Static Methods | Added directly to the constructor function | Defined using the static keyword |
| Inheritance | Uses Object.create() and manually sets prototype chain | Uses extends keyword and super function |
| Readability | Less intuitive and more verbose | More concise and intuitive |
ES5 function constructors
In ES5, you define a class-like structure using a function constructor and prototypes.
Example
// ES5 function constructor
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function () {
console.log(
"Hello, my name is " + this.name + " and I am " + this.age + " years old."
);
};
// Creating an instance
var person1 = new Person("John", 30);
person1.greet(); // Hello, my name is John and I am 30 years old.ES2015 classes
ES2015 introduced the class syntax, which simplifies the definition of classes
and supports more features such as static methods and subclassing.
Example
// ES2015 Class
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(
`Hello, my name is ${this.name} and I am ${this.age} years old.`
);
}
}
// Creating an instance
const person1 = new Person("John", 30);
person1.greet(); // Hello, my name is John and I am 30 years old.Key Differences
-
Syntax and Readability:
- ES5: Uses function constructors and prototypes, which can be less intuitive and harder to read.
- ES2015: Uses the
classkeyword, making the code more concise and easier to understand.
-
Static Methods:
- ES5: Static methods are added directly to the constructor function.
- ES2015: Static methods are defined within the class using the
statickeyword.
Example
// ES5 function Person(name, age) { this.name = name; this.age = age; } Person.sayHi = function () { console.log("Hi!"); }; Person.sayHi(); // Hi! // ES2015 class Person { static sayHi() { console.log("Hi!"); } } Person.sayHi(); // Hi! -
Inheritance
- ES5: Inheritance is achieved using
Object.create()and manually setting the prototype chain. - ES2015: Inheritance is much simpler and more intuitive with the extends keyword.
Example
// ES5 Inheritance // ES5 function constructor function Person(name, age) { this.name = name; this.age = age; } Person.prototype.greet = function () { console.log( "Hello, my name is " + this.name + " and I am " + this.age + " years old." ); }; function Student(name, age, grade) { Person.call(this, name, age); this.grade = grade; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; Student.prototype.study = function () { console.log(this.name + " is studying."); }; var student1 = new Student("Alice", 20, "A"); student1.greet(); // Hello, my name is Alice and I am 20 years old. student1.study(); // Alice is studying. // ES2015 Inheritance // ES2015 Class class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); } } class Student extends Person { constructor(name, age, grade) { super(name, age); this.grade = grade; } study() { console.log(`${this.name} is studying.`); } } const student1 = new Student("Alice", 20, "A"); student1.greet(); // Hello, my name is Alice and I am 20 years old. student1.study(); // Alice is studying. - ES5: Inheritance is achieved using
-
supercalls:- ES5: Manually call the parent constructor function.
- ES2015: Use the
superkeyword to call the parent class’s constructor and methods.
Extending classes
class HikingBackpack extends Backpack {
constructor(
name,
volume,
color,
pocketNum,
strapLengthL,
strapLengthR,
lidOpen,
hydrationCapacity
) {
// Initialize the parent class properties
super(name, volume, color, pocketNum, strapLengthL, strapLengthR, lidOpen);
// New property specific to HikingBackpack
this.hydrationCapacity = hydrationCapacity; // Capacity in liters
}
// Method to check the hydration level and alert if it needs refilling
checkHydration() {
if (this.hydrationCapacity > 0) {
console.log(`You have ${this.hydrationCapacity} liters of water left.`);
} else {
console.log("Time to refill your water!");
}
}
// Extend or override methods from the parent class if necessary
// For example, adding extra functionality when the lid is toggled
toggleLid(lidStatus) {
super.toggleLid(lidStatus); // Call the parent method
if (lidStatus) {
console.log(
"Your hiking backpack lid is open. Remember to check to make sure the hydration pack is inserted."
);
} else {
console.log(
"Your hiking backpack lid is closed. Remember to check to make sure the hydration pack is inserted."
);
}
}
}Object getters and setters
In JavaScript, getters and setters are special methods that allow you to control how properties of an object are accessed and modified.
- Getters: Functions that are invoked whenever you try to access a property
using dot notation (e.g.,
obj.name). They provide a way to customize the value that is returned when the property is read. - Setters: Functions that are called when you try to assign a value to a
property using dot notation with the assignment operator (e.g.,
obj.name = "John"). They allow you to perform actions like data validation, formatting, or side effects before the actual value is stored in the object.
Example
const person = {
_firstName: "John",
_lastName: "Doe",
get fullName() {
return `${this._firstName} ${this._lastName}`;
},
set fullName(value) {
const parts = value.split(" ");
this._firstName = parts[0];
this._lastName = parts[1];
},
};
console.log(person.fullName); // Output: John Doe
person.fullName = "Jane Smith";
console.log(person.fullName); // Output: Jane SmithIn this example, the fullName property doesn’t have a direct value stored in
the object. The getter function calculates it by combining the _firstName and
_lastName properties. The setter splits the assigned value into first and last
names and updates the internal properties accordingly.
Benefits
Getters and setters provide several benefits:
- Encapsulation: They allow you to encapsulate the implementation details of a property, making it easier to change the internal representation without affecting external code that uses the property.
- Data validation: Setters can be used to validate the values being assigned to a property, ensuring data integrity and consistency.
- Computed Properties: Getters can be used to compute and return a derived value based on other properties or calculations.
- Side effects: Setters can be used to perform side effects when a property is changed. For example, you might update a related property or trigger an action when a specific value is assigned/modified, such as logging or debugging.
Mutable vs immutable objects in JavaScript
Immutability is a core principle in functional programming but it has lots to offer to object-oriented programs as well.
Mutable objects
Mutability refers to the ability of an object to have its properties or elements changed after it’s created. A mutable object is an object whose state can be modified after it is created. In JavaScript, objects and arrays are mutable by default. They store references to their data in memory. Changing a property or element modifies the original object. Here is an example of a mutable object:
Example
const mutableObject = {
name: "John",
age: 30,
};
// Modify the object
mutableObject.name = "Jane";
// The object has been modified
console.log(mutableObject); // Output: { name: 'Jane', age: 30 }Immutable objects
An immutable object is an object whose state cannot be modified after it is created. Here is an example of an immutable object:
Example
const immutableObject = Object.freeze({
name: "John",
age: 30,
});
// Attempt to modify the object
immutableObject.name = "Jane";
// The object remains unchanged
console.log(immutableObject); // Output: { name: 'John', age: 30 }Primitive data types like numbers, strings, booleans, null, and undefined
are inherently immutable. Once assigned a value, you cannot directly modify
them.
Example
let name = "Alice";
name.toUpperCase(); // This won't modify the original name variable
console.log(name); // Still prints "Alice"
// To change the value, you need to reassign a new string
name = name.toUpperCase();
console.log(name); // Now prints "ALICE"Some built-in immutable JavaScript objects are Math, Date but custom objects
are generally mutable.
const vs immutable objects
A common confusion / misunderstanding is that declaring a variable using const
makes the value immutable, which is not true at all.
const prevents reassignment of the variable itself, but does not make the
value it holds immutable. This means:
- For primitive values (numbers, strings, booleans),
constmakes the value immutable since primitives are immutable by nature. - For non-primitive values like objects and arrays,
constonly prevents reassigning a new object/array to the variable, but the properties/elements of the existing object/array can still be modified.
On the other hand, an immutable object is an object whose state (properties and
values) cannot be modified after it is created. This is achieved by using
methods like Object.freeze() which makes the object immutable by preventing
any changes to its properties.
Example
// Using const
const person = { name: "John" };
person = { name: "Jane" }; // Error: Assignment to constant variable
person.name = "Jane"; // Allowed, person.name is now 'Jane'
// Using Object.freeze() to create an immutable object
const frozenPerson = Object.freeze({ name: "John" });
frozenPerson.name = "Jane"; // Fails silently (no error, but no change)
frozenPerson = { name: "Jane" }; // Error: Assignment to constant variableIn the first example with const, reassigning a new object to person is not
allowed, but modifying the name property is permitted. In the second example,
Object.freeze() makes the frozenPerson object immutable, preventing any
changes to its properties.
It’s important to note that Object.freeze() creates a shallow immutable
object. If the object contains nested objects or arrays, those nested data
structures are still mutable unless frozen separately.
Therefore, while const provides immutability for primitive values, creating
truly immutable objects requires using Object.freeze() or other immutability
techniques like deep freezing or using immutable data structures from libraries
like Immer or
Immutable.js.
Various ways to implement immutability in plain JavaScript objects
Here are a few ways to add/simulate different forms of immutability in plain JavaScript objects.
Immutable object properties
const myObject = {};
Object.defineProperty(myObject, "number", {
value: 42,
writable: false,
configurable: false,
});
console.log(myObject.number); // 42
myObject.number = 43;
console.log(myObject.number); // 42Preventing extensions on objects
If you want to prevent an object from having new properties added to it, but
otherwise leave the rest of the object’s properties alone, call
Object.preventExtensions(...):
let myObject = {
a: 2,
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefinedIn non-strict mode, the creation of b fails silently. In strict mode, it
throws a TypeError.
Sealing an object
Object.seal() creates a “sealed” object, which means it takes an existing
object and essentially calls Object.preventExtensions() on it, but also marks
all its existing properties as configurable: false. Therefore, not only can
you not add any more properties, but you also cannot reconfigure or delete any
existing properties, though you can still modify their values.
// Create an object
const person = {
name: "John Doe",
age: 30,
};
// Seal the object
Object.seal(person);
// Try to add a new property (this will fail silently)
person.city = "New York"; // This has no effect
// Try to delete an existing property (this will fail silently)
delete person.age; // This has no effect
// Modify an existing property (this will work)
person.age = 35;
console.log(person); // Output: { name: 'John Doe', age: 35 }
// Try to re-configure an existing property descriptor (this will fail)
Object.defineProperty(person, "name", { writable: false }); // TypeError: Cannot redefine property: name
// Check if the object is sealed
console.log(Object.isSealed(person)); // Output: trueFreezing an object
Object.freeze() creates a frozen object, which means it takes an existing
object and essentially calls Object.seal() on it, but it also marks all “data
accessor” properties as writable:false, so that their values cannot be changed.
This approach is the highest level of immutability that you can attain for an object itself, as it prevents any changes to the object or to any of its direct properties (though, as mentioned above, the contents of any referenced other objects are unaffected).
let immutableObject = Object.freeze({});Freezing an object does not allow new properties to be added to an object and
prevents users from removing or altering the existing properties.
Object.freeze() preserves the enumerability, configurability, writability and
the prototype of the object. It returns the passed object and does not create
a frozen copy.
Object.freeze() makes the object immutable. However, it is not necessarily
constant. Object.freeze prevents modifications to the object itself and its
direct properties, nested objects within the frozen object can still be
modified.
let obj = {
user: {},
};
Object.freeze(obj);
obj.user.name = "John";
console.log(obj.user.name); //Output: 'John'What are the pros and cons of immutability?
Pros
- Easier change detection: Object equality can be determined in a performant and easy manner through referential equality. This is useful for comparing object differences in React and Redux.
- Less complicated: Programs with immutable objects are less complicated to think about, since you don’t need to worry about how an object may evolve over time.
- Easy sharing via references: One copy of an object is just as good as another, so you can cache objects or reuse the same object multiple times.
- Thread-safe: Immutable objects can be safely used between threads in a multi-threaded environment since there is no risk of them being modified in other concurrently running threads. In the most cases, JavaScript runs in a single-threaded environment
- Less memory needed: Using libraries like Immer and Immutable.js, objects are modified using structural sharing and less memory is needed for having multiple objects with similar structures.
- No need for defensive copying: Defensive copies are no longer necessary when immutable objects are returning from or passed to functions, since there is no possibility an immutable object will be modified by it.
Cons
- Complex to create yourself: Naive implementations of immutable data structures and its operations can result in extremely poor performance because new objects are created each time. It is recommended to use libraries for efficient immutable data structures and operations that leverage on structural sharing.
- Potential negative performance: Allocation (and deallocation) of many small objects rather than modifying existing ones can cause a performance impact. The complexity of either the allocator or the garbage collector usually depends on the number of objects on the heap.
- Complexity for cyclic data structures: Cyclic data structures such as graphs are difficult to implement.