JavaScript Object Basics
Instance Objects and the new Operator
Constructors
Constructors have two key characteristics:
- The
thiskeyword is used inside the function to refer to the object instance being created. - The
newoperator must be used to instantiate the object.
var Vehicle = function () {
this.price = 1000;
};
// Vehicle is a constructor. By convention, constructor names start with a capital letter.The new Operator
var Vehicle = function (p) {
this.price = p;
};
var v = new Vehicle(600);
typeof v // "object"
v.price // 1000
// Vehicle is a constructor. By convention, constructor names start with a capital letter.How the new Operator Works
When the new operator is used, the following steps occur:
- A new empty object is created as the instance to be returned.
- The new object’s prototype is set to the constructor’s
prototypeproperty. - The empty object is assigned to the
thiskeyword inside the constructor. - The constructor’s code is executed.
function _new(/* constructor */ constructor, /* constructor arguments */ params) {
// Convert arguments to an array
var args = [].slice.call(arguments);
// Extract the constructor
var constructor = args.shift();
// Create an empty object inheriting the constructor’s prototype
var context = Object.create(constructor.prototype);
// Execute the constructor
var result = constructor.apply(context, args);
// Return the result if it’s an object; otherwise, return the context
return (typeof result === 'object' && result != null) ? result : context;
}
// Example
var actor = _new(Person, 'John', 28);Object.create() for Creating Instance Objects
var person1 = {
name: 'John',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};
var person2 = Object.create(person1);
person2.name // John
person2.greeting() // Hi! I'm John.The this Keyword
The this keyword refers to the object that owns the current property or method.
var A = {
name: 'John',
describe: function () {
return 'Name: ' + this.name;
}
};
var name = 'Jane';
var f = A.describe;
f() // "Name: Jane"Contexts for Using this
Global Context
this === window // true
function f() {
console.log(this === window);
}
f() // trueConstructor
var Obj = function (p) {
this.p = p;
};Object Methods
var obj = {
foo: function () {
console.log(this);
}
};
obj.foo() // objBinding this (Using call, apply, and bind)
Function.prototype.call()
The call method specifies the this value (the scope in which the function executes) and invokes the function.
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // trueFunction.prototype.apply()
The apply method is similar to call, but it accepts an array of arguments.
func.apply(thisValue, [arg1, arg2, ...]) // Usage format
function f(x, y) {
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15Function.prototype.bind()
The bind method binds this to a specific object and returns a new function.
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var func = counter.inc.bind(counter);
func();
counter.count // 1Combined Usage Example
[1, 2, 3].slice(0, 1) // [1]
// Equivalent to
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);Prototypes and Inheritance
The Prototype Object (prototype)
In JavaScript, the prototype object allows all instances of a constructor to share its properties and methods, saving memory and highlighting relationships between instances. For regular functions, the prototype property is largely unused, but for constructors, it becomes the prototype of the created instances.
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
var cat1 = new Animal('BigFur');
var cat2 = new Animal('LittleFur');
cat1.color // 'white'
cat2.color // 'white'Prototype Chain
Every JavaScript object has a prototype, which itself is an object with its own prototype, forming a prototype chain. All objects ultimately trace back to Object.prototype, which inherits methods like valueOf and toString. The chain ends at null.
The constructor Property
The prototype object has a constructor property that points to the associated constructor. Modifying the prototype typically requires updating the constructor property.
function P() {}
var p = new P();
p.constructor === P // true
p.constructor === P.prototype.constructor // true
p.hasOwnProperty('constructor') // false
// Better practice
P.prototype.method1 = function (...) { ... };The instanceof Operator
var v = new Vehicle();
v instanceof Vehicle // true
// Equivalent to
Vehicle.prototype.isPrototypeOf(v)
var d = new Date();
d instanceof Date // true
d instanceof Object // true
var obj = { foo: 123 };
obj instanceof Object // trueConstructor Inheritance
To implement inheritance:
- Call the parent constructor in the child constructor.
- Set the child’s prototype to inherit the parent’s prototype.
function Sub(value) {
Super.call(this);
this.prop = value;
}
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
// Alternative
Sub.prototype = new Super();Example:
// Step 1: Child inherits parent instance
function Rectangle() {
Shape.call(this); // Call parent constructor
}
// Alternative
function Rectangle() {
this.base = Shape;
this.base();
}
// Step 2: Child inherits parent prototype
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;// Child class B’s print method calls parent class A’s print method, then adds its own logic
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
};Multiple Inheritance
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// Inherit M1
S.prototype = Object.create(M1.prototype);
// Add M2 to the prototype chain
Object.assign(S.prototype, M2.prototype);
// Specify constructor
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'Modules
var module1 = new Object({
_count: 0,
m1: function () {
//...
},
m2: function () {
//...
}
});Encapsulating Private Variables: Constructor Approach
function StringBuilder() {
this._buffer = [];
}
StringBuilder.prototype = {
constructor: StringBuilder,
add: function (str) {
this._buffer.push(str);
},
toString: function () {
return this._buffer.join('');
}
};Encapsulating Private Variables: IIFE Approach
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1: m1,
m2: m2
};
})();Module Augmentation
var module1 = (function (mod) {
mod.m3 = function () {
//...
};
//...
return mod;
})(window.module1 || {});Immediately Invoked Function Expression (IIFE)
// finalCarousel exposes init and destroy globally; internal methods (go, handleEvents, initialize, dieCarouselDie) are private
(function($, window, document) {
function go(num) {
}
function handleEvents() {
}
function initialize() {
}
function dieCarouselDie() {
}
// Attach to global scope
window.finalCarousel = {
init: initialize,
destroy: dieCarouselDie
};
})(jQuery, window, document);Object Methods
Object.getPrototypeOf(): Returns the prototype of the specified object.
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
// Empty object’s prototype is Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype’s prototype is null
Object.getPrototypeOf(Object.prototype) === null // true
// Function’s prototype is Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // trueObject.setPrototypeOf(): Sets the prototype of a specified object and returns it.
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b // true
a.x // 1Object.create(): Creates an instance object with a specified prototype.
// Prototype object
var A = {
print: function () {
console.log('hello');
}
};
// Instance object
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // trueObject.prototype.isPrototypeOf(): Checks if an object is in the prototype chain of another.
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // falseObject.prototype.__proto__: Gets or sets an object’s prototype (read/write).
var obj = {};
var p = {};
obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // trueComparison of Prototype Retrieval Methods
var obj = new Object();
obj.__proto__ === Object.prototype // true
obj.__proto__ === obj.constructor.prototype // trueRecommended Methods for Getting an Object’s Prototype (in order of preference):
obj.__proto__(not recommended due to deprecation)obj.constructor.prototypeObject.getPrototypeOf(obj)(standard and preferred)
Object.getOwnPropertyNames(): Returns an array of an object’s own property names, excluding inherited ones.
Object.getOwnPropertyNames(Date)
// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"]Object.prototype.hasOwnProperty(): Returns a boolean indicating whether a property is defined on the object itself or inherited.
Date.hasOwnProperty('length') // true
Date.hasOwnProperty('toString') // falseObject Copying
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}Shallow Copy vs. Deep Copy
- Concepts: Both shallow and deep copies apply to reference types. A shallow copy copies the reference to an object, so the new and old objects share the same memory. A deep copy creates a new, identical object that does not share memory with the original, so modifications to the new object don’t affect the original.
- Difference: Shallow copy only copies the first-level properties; deep copy recursively copies all levels.
Shallow Copy
Using Spread Operator
let obj = {a: 1, b: 2, c: {d: 3, e: [1, 2]}};
let obj1 = {...obj};Using Object.assign
let obj1 = {
a: {
b: 1
},
c: 2
};
let obj2 = Object.assign({}, obj1);
obj2.a.b = 3;
obj2.c = 3;Deep Copy
Recursive Deep Copy
function deepCopy(obj1) {
var obj2 = Array.isArray(obj1) ? [] : {};
if (obj1 && typeof obj1 === "object") {
for (var i in obj1) {
if (obj1.hasOwnProperty(i)) {
// Recursively copy reference types
if (obj1[i] && typeof obj1[i] === "object") {
obj2[i] = deepCopy(obj1[i]);
} else {
// Simple copy for primitive types
obj2[i] = obj1[i];
}
}
}
}
return obj2;
}
var obj1 = {
a: 1,
b: 2,
c: {
d: 3
}
};
var obj2 = deepCopy(obj1);
obj2.a = 3;
obj2.c.d = 4;
alert(obj1.a); // 1
alert(obj2.a); // 3
alert(obj1.c.d); // 3
alert(obj2.c.d); // 4Using JSON.stringify and JSON.parse
let obj = {
name: 'John',
age: 20,
arr: [1, 2],
};
let obj1 = JSON.parse(JSON.stringify(obj));Strict Mode
'use strict';Purpose of Strict Mode
- Prohibits unreasonable or imprecise syntax, reducing quirks in JavaScript.
- Increases error reporting for unsafe code, improving security.
- Enhances compiler efficiency, improving performance.
- Prepares code for future JavaScript versions.
- Place
'use strict';at the start of a script to enable strict mode for the entire file. - Place it at the start of a function to enable strict mode for that function.
Prohibited Behaviors in Strict Mode
- Writing to read-only properties
- Writing to getter-only properties
- Extending non-extensible objects
- Using
evalorargumentsas identifiers - Duplicate function parameter names
- Octal literals with leading zero
thispointing to the global object- Implicit global variables
- Using
fn.calleeorfn.caller - Using
arguments.calleeorarguments.caller - Deleting variables
- Using
withstatements - Creating
evalscope argumentsnot tracking parameter changes- Using reserved words as identifiers
- Transitioning to newer JavaScript versions
Prototypes and Prototype Chains
Prototype and Prototype Chain Basics
JavaScript is a prototype-based language where each object has a prototype from which it inherits properties and methods. The prototype chain enables inheritance by allowing objects to access properties and methods from their prototype, continuing up the chain until the property is found or null is reached.
// Prototype example
function Person(name) {
this.name = name;
}
// Add method to prototype
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person('Alice');
person1.sayHello(); // Output: Hello, my name is Alice
// Prototype chain lookup
console.log(person1.hasOwnProperty('name')); // true, name is an own property
console.log(person1.hasOwnProperty('sayHello')); // false, sayHello is on the prototypeConstructors and Instances
Constructors are functions used to create specific object types. Any function can act as a constructor when called with the new operator.
// Constructor example
function Car(make, model, year) {
// Instance properties
this.make = make;
this.model = model;
this.year = year;
// Instance method (not recommended, as it creates a new copy for each instance)
this.getAge = function() {
return new Date().getFullYear() - this.year;
};
}
// Define method on prototype (recommended, shared across instances)
Car.prototype.getMakeAndModel = function() {
return `${this.make} ${this.model}`;
};
const myCar = new Car('Toyota', 'Camry', 2015);
console.log(myCar.getMakeAndModel()); // Toyota Camry
console.log(myCar.getAge()); // Age calculation
// Check constructor
console.log(myCar.constructor === Car); // truePrototype Object (prototype) and __proto__
Every function has a prototype property pointing to an object containing properties and methods shared by all instances. When an instance is created with new, its __proto__ property (preferably accessed via Object.getPrototypeOf()) points to the constructor’s prototype.
// prototype vs. __proto__
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a noise.`);
};
const dog = new Animal('Dog');
// Prototype relationship
console.log(Animal.prototype === Object.getPrototypeOf(dog)); // true
console.log(dog.__proto__ === Animal.prototype); // true (avoid direct __proto__ usage)
// Prototype chain
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // truePrototype Chain Lookup Mechanism
When accessing a property, JavaScript checks the object itself first. If not found, it traverses the prototype chain until the property is found or null is reached.
// Prototype chain lookup example
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
function Employee(name, jobTitle) {
Person.call(this, name); // Call parent constructor
this.jobTitle = jobTitle;
}
// Set up prototype chain inheritance
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.sayJob = function() {
console.log(`I work as a ${this.jobTitle}`);
};
const john = new Employee('John', 'Developer');
// Prototype chain lookup
john.sayHello(); // Lookup: john -> Employee.prototype -> Person.prototype -> sayHello
john.sayJob(); // Lookup: john -> Employee.prototype -> sayJob
console.log(john.name); // Lookup: john -> name
console.log(john.jobTitle); // Lookup: john -> jobTitlePrototype Inheritance
Prototype Chain Inheritance
Prototype chain inheritance sets the child’s prototype to an instance of the parent class.
// Prototype chain inheritance example
function Parent() {
this.parentProperty = 'Parent Property';
}
Parent.prototype.parentMethod = function() {
console.log('Parent Method');
};
function Child() {
this.childProperty = 'Child Property';
}
// Set up prototype chain inheritance
Child.prototype = new Parent(); // Key step
const child = new Child();
console.log(child.parentProperty); // Inherits parent instance property
child.parentMethod(); // Inherits parent prototype method
console.log(child.childProperty); // Child’s own property
// Issue 1: All child instances share parent instance properties (problematic for reference types)
function ParentWithArray() {
this.sharedArray = [];
}
ParentWithArray.prototype.addToArray = function(item) {
this.sharedArray.push(item);
};
function ChildWithArray() {
this.childProperty = 'Child Property';
}
ChildWithArray.prototype = new ParentWithArray();
const child1 = new ChildWithArray();
const child2 = new ChildWithArray();
child1.addToArray('Item from child1');
console.log(child2.sharedArray); // ['Item from child1'] - Shared array
// Issue 2: Cannot pass arguments to parent constructorConstructor Inheritance (Classical Inheritance)
Constructor inheritance calls the parent constructor in the child constructor using call or apply.
// Constructor inheritance example
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// Inherit instance properties
Parent.call(this, name); // Key step
this.age = age;
}
const child1 = new Child('Child1', 5);
const child2 = new Child('Child2', 6);
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green'] - Arrays not shared
// Issue: Cannot inherit prototype methods
console.log(child1.sayName); // undefinedCombination Inheritance (Most Common)
Combination inheritance merges prototype chain and constructor inheritance, making it the most widely used approach.
// Combination inheritance example
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
// Inherit instance properties (constructor inheritance)
Parent.call(this, name);
this.age = age;
}
// Inherit prototype methods (prototype chain inheritance)
Child.prototype = new Parent(); // Key step
Child.prototype.constructor = Child; // Fix constructor reference
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('Child1', 5);
const child2 = new Child('Child2', 6);
child1.colors.push('black');
console.log(child1.colors); // ['red', 'blue', 'green', 'black']
console.log(child2.colors); // ['red', 'blue', 'green'] - Arrays not shared
child1.sayName(); // Child1
child1.sayAge(); // 5
// Issue: Calls parent constructor twice (once in Child.prototype = new Parent(), once in Parent.call(this, name))ES6 class Syntax Sugar
ES6 introduced the class keyword, providing a cleaner syntax for object-oriented programming. It is syntactic sugar over prototype-based inheritance.
// ES6 class example
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
// Static method
static info() {
console.log('This is a Person class');
}
}
const person1 = new Person('Alice', 25);
person1.sayHello();
Person.info();
// Inheritance
class Employee extends Person {
constructor(name, age, jobTitle) {
super(name, age); // Call parent constructor
this.jobTitle = jobTitle;
}
sayJob() {
console.log(`I work as a ${this.jobTitle}`);
}
// Method override
sayHello() {
super.sayHello(); // Call parent method
console.log(`My job title is ${this.jobTitle}`);
}
}
const employee1 = new Employee('Bob', 30, 'Developer');
employee1.sayHello();
employee1.sayJob();
// Class characteristics
console.log(employee1 instanceof Employee); // true
console.log(employee1 instanceof Person); // trueEncapsulation and Polymorphism
Encapsulation (Private Properties/Methods, Closure-Based)
Encapsulation hides an object’s internal details, exposing only necessary interfaces. JavaScript achieves this through closures or ES6 class private fields (using #).
// Encapsulation with closures
function Counter() {
// Private variable
let count = 0;
// Private method
function changeBy(val) {
count += val;
}
// Public methods
this.increment = function() {
changeBy(1);
};
this.decrement = function() {
changeBy(-1);
};
this.value = function() {
return count;
};
}
const counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.value()); // 2
counter.decrement();
console.log(counter.value()); // 1
// Cannot access private variables or methods
console.log(counter.count); // undefined
console.log(typeof counter.changeBy); // 'undefined'
// ES6 class private fields
class BankAccount {
#balance = 0; // Private field
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
withdraw(amount) {
if (amount > this.#balance) {
throw new Error('Insufficient funds');
}
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount(100);
account.deposit(50);
console.log(account.getBalance()); // 150
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing classInheritance (ES6 class, super Keyword)
Inheritance allows an object to acquire properties and methods from another. ES6 class uses extends and super for cleaner inheritance.
// ES6 class inheritance example
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
super.speak(); // Call parent method
console.log(`${this.name} barks.`);
}
getBreed() {
return this.breed;
}
}
const dog = new Dog('Rex', 'German Shepherd');
dog.speak();
console.log(dog.getBreed());
// Inheritance checks
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // truePolymorphism (Method Overriding, Interface Simulation)
Polymorphism allows different objects to respond differently to the same operation. JavaScript achieves this through method overriding and interface simulation.
// Method overriding (a form of polymorphism)
class Shape {
area() {
console.log('Calculating area of a generic shape');
return 0;
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
console.log('Calculating area of a circle');
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
console.log('Calculating area of a rectangle');
return this.width * this.height;
}
}
const shapes = [new Shape(), new Circle(5), new Rectangle(4, 6)];
shapes.forEach(shape => {
console.log(`Area: ${shape.area()}`); // Same method call, different behavior
});
// Interface simulation (JavaScript lacks built-in interfaces)
class Printable {
print() {
throw new Error('Method "print()" must be implemented');
}
}
class Document extends Printable {
constructor(content) {
super();
this.content = content;
}
print() {
console.log(`Printing document: ${this.content}`);
}
}
class Photo extends Printable {
constructor(description) {
super();
this.description = description;
}
print() {
console.log(`Printing photo: ${this.description}`);
}
}
function printItems(items) {
items.forEach(item => {
if (item instanceof Printable) {
item.print();
} else {
console.error('Item does not implement Printable interface');
}
});
}
const itemsToPrint = [new Document('Important contract'), new Photo('Vacation snapshot')];
printItems(itemsToPrint);Design Patterns (Singleton, Factory, Observer)
Design patterns are reusable solutions to common software design problems. Below are implementations of common patterns in JavaScript.
// Singleton Pattern (ensures a single instance of a class)
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
this.value = Math.random();
}
return Singleton.instance;
}
getValue() {
return this.value;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
console.log(instance1.getValue() === instance2.getValue()); // true
// Factory Pattern (creates objects without specifying exact classes)
class Car {
constructor(options) {
this.type = 'car';
this.model = options.model;
this.year = options.year;
}
drive() {
console.log(`Driving ${this.model}`);
}
}
class Truck {
constructor(options) {
this.type = 'truck';
this.model = options.model;
this.year = options.year;
this.payload = options.payload;
}
drive() {
console.log(`Driving truck ${this.model} with ${this.payload} payload`);
}
}
class VehicleFactory {
createVehicle(options) {
switch(options.type) {
case 'car':
return new Car(options);
case 'truck':
return new Truck(options);
default:
throw new Error('Unknown vehicle type');
}
}
}
const factory = new VehicleFactory();
const myCar = factory.createVehicle({type: 'car', model: 'Toyota', year: 2020});
const myTruck = factory.createVehicle({type: 'truck', model: 'Ford', year: 2019, payload: '2 tons'});
myCar.drive();
myTruck.drive();
// Observer Pattern (Publish-Subscribe)
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
}
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(listener => listener(...args));
}
}
off(eventName, listenerToRemove) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
listener => listener !== listenerToRemove
);
}
}
}
const emitter = new EventEmitter();
function logData(data) {
console.log('Data received:', data);
}
emitter.on('data', logData);
emitter.emit('data', {message: 'Hello World'}); // Data received: {message: 'Hello World'}
emitter.off('data', logData);
emitter.emit('data', {message: 'This won’t be logged'}); // No outputObject-Oriented Design Principles (SOLID)
SOLID principles guide the creation of maintainable and extensible software.
// SOLID Principles Examples
// 1. Single Responsibility Principle (SRP) - A class should have one reason to change
// Bad example: A class handling both user data and UI
class BadUserManager {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
// Directly manipulates DOM
document.getElementById('user-list').innerHTML += `<li>${user.name}</li>`;
}
removeUser(userId) {
this.users = this.users.filter(user => user.id !== userId);
// Directly manipulates DOM
document.getElementById('user-list').innerHTML = this.users
.map(user => `<li>${user.name}</li>`)
.join('');
}
}
// Good example: Separate responsibilities
class UserManager {
constructor() {
this.users = [];
}
addUser(user) {
this.users.push(user);
}
removeUser(userId) {
this.users = this.users.filter(user => user.id !== userId);
}
getUsers() {
return this.users;
}
}
class UserUI {
constructor(userManager) {
this.userManager = userManager;
}
renderUserList() {
const userListElement = document.getElementById('user-list');
userListElement.innerHTML = this.userManager.getUsers()
.map(user => `<li>${user.name}</li>`)
.join('');
}
}
// 2. Open-Closed Principle (OCP) - Open for extension, closed for modification
// Bad example: Modifying existing code to add functionality
class Calculator {
add(a, b) {
return a + b;
}
// Adding new functionality requires modifying this class
multiply(a, b) {
return a * b;
}
}
// Good example: Extend functionality via inheritance or composition
class BasicCalculator {
add(a, b) {
return a + b;
}
}
class AdvancedCalculator extends BasicCalculator {
multiply(a, b) {
return a * b;
}
power(base, exponent) {
return Math.pow(base, exponent);
}
}
// Or using composition
class Calculator {
add(a, b) {
return a + b;
}
}
class CalculatorExtensions {
constructor(calculator) {
this.calculator = calculator;
}
multiply(a, b) {
return a * b;
}
}
// 3. Liskov Substitution Principle (LSP) - Subclasses should be substitutable for their base classes
// Bad example: Subclass alters parent behavior
class Bird {
fly() {
console.log('Flying');
}
}
class Penguin extends Bird {
fly() {
throw new Error('Penguins can\'t fly');
}
}
// Good example: Subclass does not break parent behavior
class Bird {
fly() {
// Default or error
throw new Error('This bird can\'t fly');
}
}
class Sparrow extends Bird {
fly() {
console.log('Flying');
}
}
// 4. Interface Segregation Principle (ISP) - Clients should not depend on interfaces they don’t use
// Bad example: Interface with too many methods
class Worker {
work() {}
eat() {}
sleep() {}
}
class Robot implements Worker {
work() {}
// Must implement unused methods
eat() {}
sleep() {}
}
// Good example: Split interfaces
interface Workable {
work();
}
interface Eatable {
eat();
}
interface Sleepable {
sleep();
}
class Robot implements Workable {
work() {}
}
// 5. Dependency Inversion Principle (DIP) - High-level modules should not depend on low-level modules; both should depend on abstractions
// Bad example: High-level module depends on low-level module
class LightBulb {
turnOn() {
console.log('LightBulb: On');
}
turnOff() {
console.log('LightBulb: Off');
}
}
class Switch {
constructor() {
this.lightBulb = new LightBulb();
}
operate() {
this.lightBulb.turnOn();
}
}
// Good example: Depend on abstractions
interface Switchable {
turnOn();
turnOff();
}
class LightBulb implements Switchable {
turnOn() {
console.log('LightBulb: On');
}
turnOff() {
console.log('LightBulb: Off');
}
}
class Fan implements Switchable {
turnOn() {
console.log('Fan: On');
}
turnOff() {
console.log('Fan: Off');
}
}
class Switch {
constructor(switchableDevice) {
this.switchableDevice = switchableDevice;
}
operate() {
this.switchableDevice.turnOn();
}
}
const lightSwitch = new Switch(new LightBulb());
lightSwitch.operate();
const fanSwitch = new Switch(new Fan());
fanSwitch.operate();ES6+ Object-Oriented Features
class and extends
ES6’s class and extends provide a cleaner syntax for object-oriented programming, built on prototype-based inheritance.
// class and extends basics
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
speak() {
super.speak(); // Call parent method
console.log(`${this.name} barks.`);
}
getBreed() {
return this.breed;
}
}
const dog = new Dog('Rex', 'German Shepherd');
dog.speak();
console.log(dog.getBreed());Static Methods and Properties
Static methods and properties belong to the class itself, not its instances, and are used for class-related functionality.
// Static methods and properties
class MathOperations {
static PI = 3.14159; // Static property
static square(x) {
return x * x;
}
static circleArea(radius) {
return this.PI * this.square(radius);
}
}
console.log(MathOperations.PI); // 3.14159
console.log(MathOperations.square(5)); // 25
console.log(MathOperations.circleArea(2)); // ~12.566
// Instances cannot access static members
const math = new MathOperations();
// console.log(math.PI); // undefined
// console.log(math.square(5)); // TypeError
// Static members in inheritance
class Physics extends MathOperations {
static gravity = 9.8;
static fallingSpeed(time) {
return this.gravity * time;
}
}
console.log(Physics.PI); // Inherited from MathOperations
console.log(Physics.gravity); // 9.8
console.log(Physics.fallingSpeed(2)); // 19.6Getters and Setters
Getters and setters define accessor properties, allowing custom logic during property access or modification.
// Getters and setters
class Temperature {
constructor(celsius) {
this._celsius = celsius;
}
// Getter
get celsius() {
return this._celsius;
}
// Setter
set celsius(value) {
if (value < -273.15) {
throw new Error('Temperature below absolute zero is not possible');
}
this._celsius = value;
}
// Computed property
get fahrenheit() {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
temp.fahrenheit = 100;
console.log(temp.celsius); // ~37.777
console.log(temp.fahrenheit); // 100
// Invalid value
try {
temp.celsius = -300; // Throws error
} catch (e) {
console.error(e.message);
}Symbol and Unique Properties
Symbol is an ES6 primitive type for creating unique property keys, preventing naming conflicts.
// Symbol and unique properties
const idSymbol = Symbol('id');
const nameSymbol = Symbol('name');
class User {
constructor(id, name) {
this[idSymbol] = id;
this[nameSymbol] = name;
}
getId() {
return this[idSymbol];
}
getName() {
return this[nameSymbol];
}
}
const user = new User(1, 'Alice');
console.log(user.getId()); // 1
console.log(user.getName()); // Alice
// Symbols prevent direct access
console.log(user.id); // undefined
console.log(user.name); // undefined
// Accessing Symbol properties
const symbols = Object.getOwnPropertySymbols(user);
console.log(symbols); // [Symbol(id), Symbol(name)]
// Symbols are non-enumerable
for (const key in user) {
console.log(key); // No output
}
console.log(Object.keys(user)); // []
console.log(JSON.stringify(user)); // {}
// Access via specific methods
console.log(Object.getOwnPropertyNames(user)); // []
console.log(Reflect.ownKeys(user)); // [Symbol(id), Symbol(name)]Proxy and Object Interception
Proxy objects define custom behavior for operations like property access, assignment, and enumeration.
// Proxy and object interception
const target = {
name: 'John',
age: 30
};
const handler = {
// Intercept property read
get(target, prop, receiver) {
console.log(`Getting property "${prop}"`);
if (prop === 'age') {
return target[prop] + ' years'; // Modify return value
}
return Reflect.get(target, prop, receiver);
},
// Intercept property write
set(target, prop, value, receiver) {
console.log(`Setting property "${prop}" to ${value}`);
if (prop === 'age') {
if (typeof value !== 'number' || value < 0) {
throw new Error('Age must be a positive number');
}
}
return Reflect.set(target, prop, value, receiver);
},
// Intercept property deletion
deleteProperty(target, prop) {
console.log(`Deleting property "${prop}"`);
if (prop === 'name') {
throw new Error('Cannot delete name property');
}
return Reflect.deleteProperty(target, prop);
},
// Intercept `in` operator
has(target, prop) {
console.log(`Checking if property "${prop}" exists`);
return Reflect.has(target, prop);
},
// Intercept Object.keys, etc.
ownKeys(target) {
console.log('Getting own keys');
return ['name']; // Only return name property
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.name); // Getting property "name", John
console.log(proxy.age); // Getting property "age", 30 years
proxy.age = 31; // Setting property "age" to 31
// proxy.age = 'thirty'; // Error: Age must be a positive number
// console.log(delete proxy.name); // Error: Cannot delete name property
console.log(delete proxy.age); // Deleting property "age", true
console.log('age' in proxy); // Checking if property "age" exists, true
console.log('name' in proxy); // Checking if property "name" exists, true
console.log(Object.keys(proxy)); // Getting own keys, ["name"]Complex Proxy Example – Data Validation
// Data validation Proxy
function createValidatedObject(schema, initialData) {
return new Proxy(initialData, {
set(target, prop, value) {
if (!(prop in schema)) {
throw new Error(`Invalid property: ${prop}`);
}
const validator = schema[prop];
if (!validator(value)) {
throw new Error(`Validation failed for property ${prop}: ${value}`);
}
target[prop] = value;
return true;
}
});
}
const userSchema = {
name: value => typeof value === 'string' && value.length > 0,
age: value => typeof value === 'number' && value >= 0 && value <= 120,
email: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
};
const user = createValidatedObject(userSchema, {
name: 'Alice',
age: 25,
email: 'alice@example.com'
});
user.name = 'Bob'; // Valid
// user.age = -5; // Error: Validation failed for property age: -5
// user.email = 'invalid-email'; // Error: Validation failed for property email: invalid-email
// user.address = '123 Street'; // Error: Invalid property: addressProxy in Vue 3 Reactivity System (Simplified)
// Simplified Vue 3 reactivity system
function reactive(target) {
return new Proxy(target, {
get(target, prop, receiver) {
console.log(`Getting ${prop}`);
const result = Reflect.get(target, prop, receiver);
// Recursively proxy objects
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});
}
const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25
}
});
state.count++; // Getting count, Setting count to 1
state.user.name = 'Bob'; // Getting user, Getting name, Setting name to Bob
// Note: user.name modification is intercepted due to recursive proxy



