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>
21
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>
3