Implementation techniques of the Lazy Initialization pattern
The lazy initialization pattern is a design pattern that delays the creation or computation of an object until it is actually needed. This pattern is particularly suitable for resource-intensive operations or scenarios requiring on-demand loading, effectively improving performance and reducing unnecessary memory consumption.
Basic Implementation Principle
The core idea of lazy initialization is to control the initialization timing through a proxy layer. In JavaScript, it is typically implemented in the following ways:
- Use a flag to record the initialization state
- Perform initialization upon first access
- Cache the initialization result for subsequent use
The simplest implementation uses closures to preserve the state:
function createLazyObject(initializer) {
let cached = null;
return {
get() {
if (cached === null) {
cached = initializer();
}
return cached;
}
};
}
// Usage example
const heavyObject = createLazyObject(() => {
console.log('Performing time-consuming initialization');
return { data: 'Large data' };
});
console.log(heavyObject.get()); // Initial call triggers initialization
console.log(heavyObject.get()); // Returns cached result directly
Property Descriptor Implementation
ES5 introduced property descriptors, enabling a more elegant implementation of lazy initialization:
function lazyProperty(obj, prop, initializer) {
Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
get() {
const value = initializer.call(this);
Object.defineProperty(this, prop, {
value,
writable: true,
configurable: true,
enumerable: true
});
return value;
}
});
}
// Usage example
const api = {};
lazyProperty(api, 'userData', function() {
console.log('Fetching user data');
return fetch('/user-data').then(res => res.json());
});
api.userData.then(data => console.log(data)); // First access triggers fetch
Lazy Initialization in Classes
There are multiple ways to implement lazy properties in ES6 classes:
Method 1: Getter/Setter
class HeavyCalculator {
constructor() {
this._result = null;
}
get result() {
if (this._result === null) {
console.log('Performing complex calculation');
this._result = this._compute();
}
return this._result;
}
_compute() {
// Simulate time-consuming calculation
let sum = 0;
for(let i = 0; i < 1000000; i++) {
sum += Math.sqrt(i);
}
return sum;
}
}
const calc = new HeavyCalculator();
console.log(calc.result); // First access triggers calculation
console.log(calc.result); // Returns cached result directly
Method 2: Proxy
function lazyInitClass(Class) {
return new Proxy(Class, {
construct(target, args) {
const instance = Reflect.construct(target, args);
return new Proxy(instance, {
get(obj, prop) {
if (prop in obj && typeof obj[prop] === 'function') {
return obj[prop].bind(obj);
}
if (prop.startsWith('_lazy_') && !(prop in obj)) {
const initProp = prop.slice(6);
if (initProp in obj && typeof obj[`_init_${initProp}`] === 'function') {
obj[prop] = obj[`_init_${initProp}`]();
}
}
return obj[prop];
}
});
}
});
}
// Usage example
const LazyUser = lazyInitClass(class User {
_init_profile() {
console.log('Loading user profile');
return { name: 'John Doe', age: 30 };
}
get profile() {
return this._lazy_profile;
}
});
const user = new LazyUser();
console.log(user.profile); // First access triggers initialization
console.log(user.profile); // Returns cached result directly
Asynchronous Lazy Initialization
For resources requiring asynchronous loading, Promises can be used:
class AsyncLazyLoader {
constructor(loader) {
this._loader = loader;
this._promise = null;
this._value = null;
}
get() {
if (this._value !== null) return Promise.resolve(this._value);
if (this._promise !== null) return this._promise;
this._promise = this._loader().then(value => {
this._value = value;
return value;
});
return this._promise;
}
// Force refresh
refresh() {
this._promise = null;
this._value = null;
return this.get();
}
}
// Usage example
const imageLoader = new AsyncLazyLoader(() => {
return new Promise(resolve => {
setTimeout(() => {
console.log('Image loaded');
resolve('Image data');
}, 1000);
});
});
imageLoader.get().then(data => console.log(data)); // First load
imageLoader.get().then(data => console.log(data)); // Uses cache
Application Scenarios and Optimization
Lazy Loading Images
class LazyImage {
constructor(placeholderSrc, realSrc) {
this.img = new Image();
this.img.src = placeholderSrc;
this.realSrc = realSrc;
this.loaded = false;
// Intersection Observer for lazy loading
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.loaded) {
this._loadRealImage();
this.observer.unobserve(this.img);
}
});
});
this.observer.observe(this.img);
}
_loadRealImage() {
this.loaded = true;
this.img.src = this.realSrc;
}
}
Lazy Module Loading
Combine with dynamic imports for on-demand loading:
const componentLoaders = {
Chart: () => import('./components/Chart.js'),
Map: () => import('./components/Map.js'),
Calendar: () => import('./components/Calendar.js')
};
class LazyComponentLoader {
constructor() {
this._components = {};
}
async load(name) {
if (this._components[name]) {
return this._components[name];
}
if (componentLoaders[name]) {
const module = await componentLoaders[name]();
this._components[name] = module.default;
return module.default;
}
throw new Error(`Unknown component: ${name}`);
}
}
// Usage example
const loader = new LazyComponentLoader();
document.getElementById('show-chart').addEventListener('click', async () => {
const Chart = await loader.load('Chart');
new Chart().render();
});
Performance Considerations and Pitfalls
-
Memory Leak Risk: Cached objects may cause memory leaks if not released timely
// Solution: Provide cleanup methods class LazyCache { constructor() { this._cache = new Map(); } get(key, initializer) { if (!this._cache.has(key)) { this._cache.set(key, initializer()); } return this._cache.get(key); } clear(key) { this._cache.delete(key); } clearAll() { this._cache.clear(); } }
-
Concurrent Initialization Issue: Multiple triggers may cause duplicate computation
// Use Promise to solve concurrency issues function createLazyAsync(initializer) { let promise = null; return () => { if (!promise) { promise = initializer().finally(() => { // Allow reload after initialization promise = null; }); } return promise; }; }
-
Increased Testing Complexity: Lazy initialization may complicate testing
// Provide forced initialization methods for testing class TestableLazy { constructor() { this._initialized = false; } get data() { if (!this._initialized) { this._initialize(); } return this._data; } _initialize() { this._data = /* initialization logic */; this._initialized = true; } // Test-only method __testOnlyInitialize() { this._initialize(); } }
Advanced Pattern: Multi-Level Caching
For scenarios requiring multi-level caching, combine WeakMap and Map:
class MultiLevelCache {
constructor() {
this._weakCache = new WeakMap(); // Level 1: Weak reference cache
this._strongCache = new Map(); // Level 2: Strong reference cache
this._maxSize = 100; // Maximum size for strong cache
}
get(key, initializer) {
// Try weak cache first
let value = this._weakCache.get(key);
if (value !== undefined) return value;
// Try strong cache
value = this._strongCache.get(key);
if (value !== undefined) {
// Promote to weak cache
this._weakCache.set(key, value);
return value;
}
// Initialize and cache
value = initializer();
this._weakCache.set(key, value);
this._strongCache.set(key, value);
// Control strong cache size
if (this._strongCache.size > this._maxSize) {
const oldestKey = this._strongCache.keys().next().value;
this._strongCache.delete(oldestKey);
}
return value;
}
}
Combination with Other Patterns
Lazy Initialization + Factory Pattern
class LazyFactory {
constructor(factoryFn) {
this._factory = factoryFn;
this._instances = new Map();
}
getInstance(key) {
if (!this._instances.has(key)) {
this._instances.set(key, {
initialized: false,
value: null,
promise: null
});
}
const record = this._instances.get(key);
if (record.initialized) {
return Promise.resolve(record.value);
}
if (record.promise) {
return record.promise;
}
record.promise = this._factory(key)
.then(value => {
record.value = value;
record.initialized = true;
return value;
});
return record.promise;
}
}
// Usage example
const userFactory = new LazyFactory(userId =>
fetch(`/api/users/${userId}`).then(res => res.json())
);
userFactory.getInstance(123).then(user => console.log(user));
Lazy Initialization + Decorator Pattern
In environments supporting decorators:
function lazy(target, name, descriptor) {
const { get, set } = descriptor;
if (get) {
descriptor.get = function() {
const value = get.call(this);
Object.defineProperty(this, name, {
value,
enumerable: true,
configurable: true,
writable: true
});
return value;
};
}
return descriptor;
}
class DecoratorExample {
@lazy
get expensiveData() {
console.log('Computing data');
return computeExpensiveData();
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn