Search code examples
javascriptangularjavascript-objectsangular-components

Angular variable reference persistence behavior after setting singleton instance to Undefined


I have this class, meant to work as a singleton,

export class SingletonClass {

    public static instance: SingletonClass | undefined;
    public name: string | undefined | null;

    static getInstance() {
        if (SingletonClass.instance == null || SingletonClass.instance == undefined)
            SingletonClass.instance = new SingletonClass();

        return SingletonClass.instance;
    }

    static destroy() {
        SingletonClass.instance = undefined;
    }
}

and a component class has this code,

  export class TestComponent implements OnInit {

    SingletonObject1: SingletonClass | undefined | null = SingletonClass.getInstance();
    SingletonObject2: SingletonClass | undefined | null;

    ngOnInit(): void {

      this.SingletonObject1!.name = "Name1";
      SingletonClass.destroy();

      this.SingletonObject2 = SingletonClass.getInstance();
      this.SingletonObject2.name = "Name2";
    }
  }

I expect both SingletonObject1 and SingletonObject2 to refer to the same singleton object, hence after running this code, value of the name property of both objects should be "Name2". But what happens here is SingletonObject1.name has "Name1" and SingletonObject2.name has "Name2" as their values.

If I remove the SingletonClass.destroy(); it works as expected. So it seems even after calling SingletonClass.instance = undefined; SingletonObject1 holds the reference to previously created object.

Can someone explain why does it create two variable instances when it set the instence as undefined and how to modify the destroy method to remove the whole singleton object and keep the reference to the same location from both variables.


Solution

  • This isn't unique to static classes, singletons, or Angular. Setting a reference to undefined doesn't delete/destroy/free the underlying object instance, it just clears the reference to it. Any other variables holding a reference to the object will still see the original object:

    // Create the first instance.
    let obj = { foo: 'bar' };
    
    // Capture a reference to the first instance.
    const a = obj;
    
    // Forget the reference to the first instance.
    // NOTE: this does not destroy the first instance,
    // it just removes obj's reference to the first
    // instance.
    obj = undefined;
    
    // Create the second instance.
    obj = { foo: 'baz' };
    
    // Capture a reference to the second instance.
    const b = obj;
    
    // Log the instances (Prints 'bar' and 'baz' respectively). 
    console.log(a.foo);
    console.log(b.foo);

    Why does it work when you don't call destroy()? Because the two references are still pointing to the same instance:

    // Create the first instance.
    let obj = { foo: 'bar' };
    
    // Capture a reference to the first instance.
    const a = obj;
    
    // Update a property on the first instance.
    obj.foo = 'baz';
    
    // Capture a second reference to the first instance.
    const b = obj;
    
    // Log the instances (Prints 'bar' and 'baz' respectively). 
    console.log(a.foo);
    console.log(b.foo);

    So how do you get the behavior you want? Either make sure you reassign your references once you destroy the instance:

    class SingletonClass {
    
        static #instance;
        name;
        
        static getInstance() {
            if (SingletonClass.#instance == undefined)
                SingletonClass.#instance = new SingletonClass();
            
            return SingletonClass.#instance;
        }
    
        static destroy() {
            SingletonClass.#instance = undefined;
        }
    }
    
    // This has to be 'let' now since we're going
    // to be reassigning our reference to point to the
    // new instance.
    let a = SingletonClass.getInstance();
    a.name = 'Foo';
    
    SingletonClass.destroy();
    
    // We know we've destroyed the first instance so we
    // should update our stored references to point to
    // the new instance instead.
    a = SingletonClass.getInstance();
    
    const b = SingletonClass.getInstance();
    b.name = 'Bar';
    
    console.log(a.name);
    console.log(b.name);

    Or let the variables have a dynamic reference to the instance. This does mean you have function calls each time you try to access the instance, but you're always guaranteed to get the latest instance:

    class SingletonClass {
    
        static #instance;
        name;
        
        static getInstance() {
            if (SingletonClass.#instance == undefined)
                SingletonClass.#instance = new SingletonClass();
            
            return SingletonClass.#instance;
        }
    
        static destroy() {
            SingletonClass.#instance = undefined;
        }
    }
    
    const a = () => SingletonClass.getInstance();
    a().name = 'Foo';
    
    SingletonClass.destroy();
    
    const b = () => SingletonClass.getInstance();
    b().name = 'Bar';
    
    console.log(a().name);
    console.log(b().name);