javascriptmodule-pattern

Using "this" in nested, modular methods and preserving namespaces


I have a giant JavaScript monolith and used a module based approach to make it more structured.

However, I'm a bit lost on how to use the "this" keyword for nested functions, without overwriting each other, like the example below might demonstrate

function Fruit() {
  const self = this;

  this.fruits = [];

  this.addFruit = function (fruit) {
    self.fruits.push(fruit);
  };

  // Buy locally
  this.localStore = (function () {
    const self_localStore = this;
    this.buyArray = [];

    this.buy = function (fruit) {
      console.log("Bought locally");
      self_localStore.buyArray = this;
    };

    return this;
  })();

  // Buy from online store
  this.onlineStore = (function () {
    const self_onlineStore = this;
    this.buyArray = [];

    this.buy = function (fruit) {
      console.log("Bought online");
      self_onlineStore.buyArray = this;
    };

    return this;
  })();

  return this;
}

let fruit = new Fruit();

fruit.localStore.buy("apple"); // -> Bought online, instead of Bought locally

The localStore.buy() method is for obvious reasons overwritten by the onlineStore.buy() method, as they both are returned to the Fruit() methods scope, use the same name and the last one overwrites the first one.

So, what would be the solution to allow "namespaces" like onlineStore and localStore without using an object that has these nested methods?


Solution

  • When you write an IIFE, this equals window or undefined (depending on whether you use strict mode or not):

    (function() { console.log(this === window); })() // True

    The this keyword is being assigned the object from which the function is called, for example:

    const obj = {
      fun: function() { console.log(this === obj); } 
    }
    
    obj.fun(); // True

    If you want to create some namespace which has its own state, I suggest you to do it with a simple object like this:

    function Fruit() {
      const self = this;
    
      this.fruits = [];
    
      this.addFruit = function (fruit) {
        self.fruits.push(fruit);
      };
    
      // Buy locally
      this.localStore = {
        buyArray: [],
    
        buy: function (fruit) {
          this.buyArray.push(fruit);
          console.log("Bought locally:", this.buyArray.join(', '));
        }
    
      };
    
      // Buy from online store
      this.onlineStore =  {
        buyArray: [],
    
        buy: function (fruit) {
          this.buyArray.push(fruit);
          console.log("Bought online:", this.buyArray.join(', '));
        }
    
      };
    
      return this;
    }
    
    let fruit = new Fruit();
    
    fruit.localStore.buy("apple"); // -> Bought online, instead of Bought locally
    fruit.localStore.buy("banana");
    fruit.onlineStore.buy("orange");

    However, you may not like the literal objects, because they don't provide real encapsulation (the buyArray property can be accessed from outside of the buy function).

    So you could also encapsulate the state in an IIFE like you initially tried to do:

    function Fruit() {
      const self = this;
    
      this.fruits = [];
    
      this.addFruit = function (fruit) {
        self.fruits.push(fruit);
      };
    
      // Buy locally
      this.localStore = (function() {
        const buyArray = [];
    
        const buy = function (fruit) {
          buyArray.push(fruit);
          console.log("Bought locally:", buyArray.join(', '));
        };
        
        return { buy };
    
      })();
    
      // Buy from online store
      this.onlineStore = (function() {
        const buyArray = [];
    
        const buy = function (fruit) {
          buyArray.push(fruit);
          console.log("Bought online:", buyArray.join(', '));
        };
        
        return { buy };
    
      })();
    
      return this;
    }
    
    let fruit = new Fruit();
    
    fruit.localStore.buy("apple"); // -> Bought online, instead of Bought locally
    fruit.localStore.buy("banana");
    fruit.onlineStore.buy("orange");