skip to Main Content

How do you define a static constructor within a JS class?

I managed to write something that somehow works but it requires to write a specific codepath in constructor for when an IngredientsDB object is instantiated from the static method IngredientsDB.fromXML():

class IngredientsDB {
  constructor(...args) {
    if (arguments.length === 0) {
      this.document = null;
    } else {
      this.document = new DOMParser().parseFromString("<ingredients/>", "text/xml");
      // ... process args
    }
  }
  static fromXML(xmlString) {
    const db = new this;
    db.document = new DOMParser().parseFromString(xmlString, "text/xml");
    return db;
  }
  toString() { // concise output for testing
    const ingredientsNode = this.document.firstChild;
    let str = ingredientsNode.tagName + ":";
    let childNode;
    for (childNode of ingredientsNode.children)
      str += " " + childNode.tagName;
    return str;
  }
}

const xml = document.getElementById("ingredients").textContent.trimStart();
console.log(IngredientsDB.fromXML(xml).toString());
<script id="ingredients" type="text/xml">
<ingredients>
  <tomato/>
  <meat/>
  <cheese/>
</ingredients>
</script>

2

Answers


  1. Chosen as BEST ANSWER

    Here's an attempt at addressing the problem of detecting when the constructor is called from a class method. The idea is to use a private static property that defines the "operating mode" of the constructor:

    class MyClass {
    
      static #constuctorMode = null; // default behavior
    
      constructor() {
        switch (MyClass.#constuctorMode) {
          case "fromFactory":
            console.log("fromFactory instantiation");
            break;
          default:
            console.log("normal instantiation");
        }
      }
    
      static fromFactory() {
        this.#constuctorMode = "fromFactory";
        const obj = new this;
        this.#constuctorMode = null;
        return obj;
      }
    }
    
    MyClass.fromFactory();
    new MyClass;


  2. You can define a normal static method, do the XML parsing and pass the parsed arguments to the normal constructor, then return the constructed object.

    class IngredientsDB {
      static fromXML(xml) {
        // parse the XML and extract the normal arguments
        const xmlDocument = new DOMParser().parseFromString(xml, "text/xml");
        const root = xmlDocument.documentElement;
        if (root.tagName === "ingredients") {
          const ingredients = Array.from(root.children, child => child.tagName);
          return new this(...ingredients);
        } else {
          return new this();
          // or throw new Error("expected root element <ingredients>");
        }
      }
    
      constructor(...ingredients) {
        // handle the arguments normally
        this.ingredients = ingredients;
      }
    
      toString() {
        // create string without the knowledge of the XML document
        return `ingredients: ${this.ingredients.join(" ")}`
      }
    }
    
    const xml = document.getElementById("ingredients").textContent.trimStart();
    console.log(IngredientsDB.fromXML(xml).toString());
    console.log(new IngredientsDB("foo", "bar", "baz").toString());
    <script id="ingredients" type="text/xml">
    <ingredients>
      <tomato/>
      <meat/>
      <cheese/>
    </ingredients>
    </script>

    Note this refers to the "receiver" of a method call. Which in the case of IngredientsDB.fromXML(xml) is IngredientsDB. You could also use IngredientsDB instead of this.


    If a constructor does not allow for a specific scenario you could define instance methods that allow you to construct a specific instance and then alter the state.

    class IngredientsDB {
      static fromXML(xml) {
        // parse the XML and extract the normal arguments
        const xmlDocument = new DOMParser().parseFromString(xml, "text/xml");
        const root = xmlDocument.documentElement;
        if (root.tagName === "ingredients") {
          const db = new this(); // assume we can't just pass the data through the constructor
          for (const child of root.children) db.addIngredient(child.nodeName);
          return db;
        } else {
          return new this();
          // or throw new Error("expected root element <ingredients>");
        }
      }
    
      constructor(...ingredients) {
        // handle the arguments normally
        this.ingredients = ingredients;
      }
    
      addIngredient(ingredient) {
        this.ingredients.push(ingredient);
      }
    
      toString() {
        // create string without the knowledge of the XML document
        return `ingredients: ${this.ingredients.join(" ")}`
      }
    }
    
    const xml = document.getElementById("ingredients").textContent.trimStart();
    console.log(IngredientsDB.fromXML(xml).toString());
    console.log(new IngredientsDB("foo", "bar", "baz").toString());
    <script id="ingredients" type="text/xml">
    <ingredients>
      <tomato/>
      <meat/>
      <cheese/>
    </ingredients>
    </script>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search