Advanced Data Types, Memory Management, and Type Safety
50 minJavaScript's type system is dynamic and loosely typed, which offers flexibility but requires discipline. Modern development practices emphasize type safety through runtime checks (like Joi/Zod) or static analysis (TypeScript/JSDoc) to prevent common bugs.
Understanding JavaScript's memory management is crucial for building performant applications. The garbage collector automatically frees memory, but developers must be aware of memory leaks caused by closures, detached DOM nodes, and uncleared timers.
JavaScript's primitive types (String, Number, Boolean, Symbol, BigInt, undefined, null) are immutable and passed by value. Objects (including Arrays and Functions) are mutable and passed by reference. This distinction is vital when modifying data.
Modern JavaScript uses `const` by default for variables that won't be reassigned, `let` for those that will, and avoids `var` entirely due to its confusing function-scoping and hoisting behavior.
Key Concepts
- Primitives (Value) vs. Objects (Reference).
- Mutability vs. Immutability.
- Garbage Collection and Memory Leaks.
- Type Coercion and Type Safety.
- Scope (Block vs. Function) and Hoisting.
Learning Objectives
Master
- Distinguishing between all 7 primitive types
- Understanding pass-by-value vs. pass-by-reference
- Identifying and fixing memory leaks
- Implementing runtime type validation
Develop
- Defensive programming habits
- Performance profiling skills
- Writing self-documenting code
Tips
- Run 'npm init -y' to quickly set up a new project.
- Install ESLint and Prettier as dev dependencies for code quality.
- Use 'npm run dev' to start the development server.
- Check package.json to understand project dependencies and scripts.
Common Pitfalls
- Modifying an object passed as an argument (side effect), affecting other parts of the app.
- Assuming `typeof null` is 'null' (it's 'object', a famous bug).
- Creating circular references that confuse the garbage collector (less common now but possible).
- Forgetting to remove event listeners on single-page apps (major memory leak source).
Summary
- Data types dictate how memory is used.
- References allow shared state but require care.
- Memory management is automatic but not magic.
- Type safety prevents runtime errors.
Exercise
Implement a comprehensive data type demonstration that showcases type safety, memory management, and modern JavaScript best practices.
// Advanced Data Types and Type Safety Demonstration
import { isType, validateSchema } from './type-utils.js';
class DataTypeAnalyzer {
constructor() {
this.typeRegistry = new Map();
this.memoryUsage = new Map();
this.initializeTypeRegistry();
}
initializeTypeRegistry() {
// Register type validators
this.typeRegistry.set('string', value => typeof value === 'string');
this.typeRegistry.set('number', value => typeof value === 'number' && !isNaN(value));
this.typeRegistry.set('boolean', value => typeof value === 'boolean');
this.typeRegistry.set('object', value => typeof value === 'object' && value !== null);
this.typeRegistry.set('array', value => Array.isArray(value));
this.typeRegistry.set('function', value => typeof value === 'function');
this.typeRegistry.set('symbol', value => typeof value === 'symbol');
this.typeRegistry.set('bigint', value => typeof value === 'bigint');
}
analyzeValue(value, label = 'value') {
const analysis = {
type: typeof value,
isPrimitive: this.isPrimitive(value),
isMutable: this.isMutable(value),
memorySize: this.estimateMemorySize(value),
validation: this.validateValue(value),
stringified: this.safeStringify(value)
};
console.log(`\nš Analysis of ${label}:`, analysis);
return analysis;
}
isPrimitive(value) {
return value !== null &&
(typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'symbol' ||
typeof value === 'bigint' ||
typeof value === 'undefined');
}
isMutable(value) {
return !this.isPrimitive(value) && value !== null;
}
estimateMemorySize(value) {
if (this.isPrimitive(value)) {
return this.getPrimitiveSize(value);
}
if (Array.isArray(value)) {
return this.getArraySize(value);
}
if (typeof value === 'object') {
return this.getObjectSize(value);
}
return 'Unknown';
}
getPrimitiveSize(value) {
switch (typeof value) {
case 'string': return value.length * 2; // UTF-16
case 'number': return 8; // 64-bit float
case 'boolean': return 1;
case 'symbol': return 8; // Approximation
case 'bigint': return 8; // Approximation
default: return 0;
}
}
getArraySize(array) {
let size = 0;
for (const item of array) {
size += this.getPrimitiveSize(item);
}
return size + array.length * 8; // Array overhead
}
getObjectSize(obj) {
let size = 0;
for (const [key, value] of Object.entries(obj)) {
size += key.length * 2; // Key size
size += this.getPrimitiveSize(value);
}
return size;
}
validateValue(value) {
const validations = [];
// Type validation
const type = typeof value;
validations.push({ type: 'type', valid: true, value: type });
// Null check
validations.push({ type: 'null', valid: value !== null, value: value === null });
// Undefined check
validations.push({ type: 'undefined', valid: value !== undefined, value: value === undefined });
// Array check
if (Array.isArray(value)) {
validations.push({ type: 'array', valid: true, length: value.length });
}
// Object check
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
validations.push({ type: 'object', valid: true, keys: Object.keys(value).length });
}
return validations;
}
safeStringify(value) {
try {
return JSON.stringify(value, null, 2);
} catch (error) {
return `[Circular or non-serializable: ${error.message}]`;
}
}
demonstrateMemoryLeaks() {
console.log('\nā ļø Memory Leak Demonstration:');
// Example 1: Event listener memory leak
const button = document.createElement('button');
button.textContent = 'Click me';
// Bad: Anonymous function creates closure
button.addEventListener('click', () => {
console.log('Button clicked');
});
// Good: Named function for removal
const handleClick = () => console.log('Button clicked');
button.addEventListener('click', handleClick);
// button.removeEventListener('click', handleClick); // Proper cleanup
// Example 2: Closure memory leak
const createLeakyFunction = () => {
const largeData = new Array(1000000).fill('data');
return () => {
console.log('Accessing large data:', largeData.length);
};
};
const leakyFunction = createLeakyFunction();
// largeData is kept in memory even after createLeakyFunction returns
console.log('Memory leak examples created. In production, ensure proper cleanup.');
}
demonstrateTypeSafety() {
console.log('\nš”ļø Type Safety Demonstration:');
// Runtime type checking
const validateUser = (user) => {
const schema = {
name: 'string',
age: 'number',
email: 'string',
isActive: 'boolean'
};
for (const [key, expectedType] of Object.entries(schema)) {
const actualType = typeof user[key];
if (actualType !== expectedType) {
throw new TypeError(
`Expected ${key} to be ${expectedType}, got ${actualType}`
);
}
}
return true;
};
try {
const validUser = {
name: 'John Doe',
age: 30,
email: 'john@example.com',
isActive: true
};
validateUser(validUser);
console.log('ā
Valid user passed validation');
const invalidUser = {
name: 'Jane Doe',
age: '25', // Should be number
email: 'jane@example.com',
isActive: true
};
validateUser(invalidUser);
} catch (error) {
console.log('ā Validation failed:', error.message);
}
}
}
// Usage demonstration
const analyzer = new DataTypeAnalyzer();
// Analyze different data types
const testValues = [
'Hello, World!',
42,
true,
null,
undefined,
Symbol('test'),
123n,
[1, 2, 3, 4, 5],
{ name: 'John', age: 30 },
() => console.log('function'),
new Date(),
new Map([['key', 'value']]),
new Set([1, 2, 3])
];
testValues.forEach((value, index) => {
analyzer.analyzeValue(value, `testValue${index + 1}`);
});
// Demonstrate memory management
analyzer.demonstrateMemoryLeaks();
// Demonstrate type safety
analyzer.demonstrateTypeSafety();
// Modern JavaScript best practices
console.log('\nš Modern JavaScript Best Practices:');
// 1. Use const by default
const API_BASE_URL = 'https://api.example.com';
const DEFAULT_TIMEOUT = 5000;
// 2. Use let when reassignment is needed
let retryCount = 0;
const maxRetries = 3;
// 3. Destructuring for cleaner code
const user = { name: 'Alice', age: 25, email: 'alice@example.com' };
const { name, age, email } = user;
// 4. Template literals for string interpolation
const greeting = `Hello, ${name}! You are ${age} years old.`;
// 5. Arrow functions for concise syntax
const multiply = (a, b) => a * b;
// 6. Default parameters
const createUser = (name, age = 18, email = '') => ({ name, age, email });
// 7. Rest and spread operators
const sum = (...numbers) => numbers.reduce((acc, num) => acc + num, 0);
const userCopy = { ...user, age: 26 };
console.log('Best practices demonstrated successfully!');