object's initial state.
class PaymentGateway {
readonly instanceId: string;
private status: 'IDLE' | 'PROCESSING' | 'FAILED' = 'IDLE';
constructor(
private readonly apiKey: string,
private readonly endpoint: string
) {
if (!apiKey) {
throw new Error('API key is required for gateway initialization');
}
this.instanceId = crypto.randomUUID();
}
async processTransaction(amount: number): Promise<boolean> {
this.status = 'PROCESSING';
// Simulate network call
const success = await this.transmit(amount);
this.status = success ? 'IDLE' : 'FAILED';
return success;
}
private async transmit(amount: number): Promise<boolean> {
// Implementation details
return amount > 0;
}
}
Rationale:
- Constructor Validation: Checking
apiKey in the constructor fails fast, preventing invalid instances from entering the system.
- Readonly Fields: Using
readonly for instanceId and apiKey enforces immutability after construction, reducing mutation bugs.
- Private State:
status is private, ensuring state transitions only occur through controlled methods like processTransaction.
Step 2: Instantiate Using the new Operator
The new keyword must be used to create instances. This triggers the instantiation protocol, ensuring each object receives its own memory space and prototype linkage.
const stripeClient = new PaymentGateway('sk_live_abc123', 'https://api.stripe.com');
const paypalClient = new PaymentGateway('pk_test_xyz789', 'https://api.paypal.com');
console.log(stripeClient.instanceId); // Unique UUID
console.log(paypalClient.instanceId); // Different UUID
Rationale:
- Isolation:
stripeClient and paypalClient are distinct objects. Modifying stripeClient does not affect paypalClient.
- Prototype Linkage: Both instances share the
processTransaction method via the prototype chain, optimizing memory usage. The method code exists once; the context (this) changes per call.
Step 3: Verify Instance Independence
Confirm that state is isolated. This is critical when managing multiple connections or configurations.
stripeClient.processTransaction(100).then(success => {
console.log(`Stripe: ${success}`);
});
paypalClient.processTransaction(200).then(success => {
console.log(`PayPal: ${success}`);
});
// Instances operate independently
console.log(stripeClient === paypalClient); // false
Step 4: Understand the Internal Mechanics
When new PaymentGateway(...) executes, the JavaScript engine performs these steps:
- Allocation: A new empty object is created in memory.
- Prototype Linking: The internal
[[Prototype]] of the new object is set to PaymentGateway.prototype. This enables method inheritance.
- Constructor Execution: The constructor function is called with
this bound to the new object. Initialization logic runs.
- Return: The new object is returned automatically. If the constructor explicitly returns an object, that object replaces the instance; otherwise, the instance is returned.
Pitfall Guide
1. The "Missing New" Trap
Explanation: Calling a class constructor without new throws a TypeError. Unlike function constructors, class constructors detect the absence of new and abort.
Fix: Always use new. If you need a factory pattern, use a static method that internally calls new.
// ❌ const client = PaymentGateway('key', 'url'); // TypeError
// ✅ const client = new PaymentGateway('key', 'url');
2. Constructor Return Value Override
Explanation: If a constructor returns an object, new returns that object instead of the instance. This can silently break prototype linkage.
Fix: Avoid returning values from constructors unless intentionally implementing a custom allocation pattern.
class BadGateway {
constructor() {
return { foo: 'bar' }; // new BadGateway() returns { foo: 'bar' }, not instance
}
}
3. Method Detachment and this Loss
Explanation: Passing a class method as a callback loses the this context. The method is called without the instance binding.
Fix: Use arrow functions for class fields or bind the method in the constructor.
class Worker {
// ❌ this is undefined when called as callback
doWork() { console.log(this); }
// ✅ this is bound to instance
doWork = () => { console.log(this); }
}
4. Prototype Pollution via Instance
Explanation: Assigning properties directly to the prototype affects all instances. This is rarely intended and causes cross-contamination.
Fix: Define shared data in the class body or static properties. Define instance data in the constructor.
// ❌ Modifies all instances
PaymentGateway.prototype.sharedConfig = { timeout: 5000 };
// ✅ Use static for shared class-level data
class PaymentGateway {
static defaultTimeout = 5000;
}
5. Static vs. Instance Misalignment
Explanation: Attempting to access instance properties via the class or static properties via an instance leads to undefined.
Fix: Use ClassName.staticProp for static members and instance.instanceProp for instance members.
class Config {
static version = '1.0';
id: string;
}
// ✅ Config.version
// ❌ new Config().version // undefined
6. Forgetting new.target in Inheritance
Explanation: In complex inheritance hierarchies, constructors may need to detect how they were invoked to prevent abstract instantiation.
Fix: Check new.target to enforce abstract behavior or customize initialization.
class Base {
constructor() {
if (new.target === Base) {
throw new Error('Base cannot be instantiated directly');
}
}
}
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Isolated State Required | Instance via new | Each instance gets own memory for properties. | Memory scales with instance count. |
| Shared Configuration | Static Properties | Data is stored once on the class, not per instance. | Minimal memory overhead. |
| Complex Creation Logic | Static Factory Method | Encapsulates construction steps and validation. | Adds indirection; improves testability. |
| Method Sharing | Prototype Methods | Methods are shared via prototype chain. | Reduces memory footprint significantly. |
| Legacy Compatibility | Function Constructor | Supports older environments without class syntax. | Higher risk of this errors; no new enforcement. |
Configuration Template
A production-ready class template incorporating best practices:
export class DatabaseConnection {
readonly connectionId: string;
private isConnected: boolean = false;
constructor(
private readonly host: string,
private readonly port: number,
private readonly credentials: Record<string, string>
) {
if (port < 0 || port > 65535) {
throw new Error('Invalid port number');
}
this.connectionId = crypto.randomUUID();
}
async connect(): Promise<void> {
if (this.isConnected) return;
// Connection logic
this.isConnected = true;
}
async disconnect(): Promise<void> {
// Cleanup logic
this.isConnected = false;
}
// Arrow function preserves this context
getStatus = () => ({
id: this.connectionId,
connected: this.isConnected,
host: this.host
});
}
Quick Start Guide
- Define Class: Create a class with a constructor that accepts required parameters.
- Add Validation: Throw errors in the constructor for invalid inputs.
- Instantiate: Use
new ClassName(args) to create the object.
- Verify: Check that the instance has unique state and access to prototype methods.
- Deploy: Integrate the instance into your application logic, ensuring
this context is preserved in callbacks.