阿里云主机折上折
  • 微信号
Current Site:Index > Implementation techniques of the Lazy Initialization pattern

Implementation techniques of the Lazy Initialization pattern

Author:Chuan Chen 阅读数:20812人阅读 分类: JavaScript

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:

  1. Use a flag to record the initialization state
  2. Perform initialization upon first access
  3. 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

  1. 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();
      }
    }
    
  2. 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;
      };
    }
    
  3. 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.