0

The following code, though cumbersome, allows me to define a prototype with a simple property that gets inherited by objects using the prototype:

const SCXMLState = Object.setPrototypeOf(
   Object.defineProperties({
      addChild() { }

      isState: true // an inherited property & value
   },{
      isSCXML: {
         get() { return this.nodeName==='scxml' }
      },
      id: {
         get() {
            return this.getAttribute('id') || (this.isSCXML ? '(scxml)' : null);
         }
      }
   }),
   Element.prototype
);

const markup = `<scxml xmlns="http://www.w3.org/2005/07/scxml" />`;
const xmldoc = (new DOMParser).parseFromString(markup, "text/xml");
const scxml  = xmldoc.documentElement;
Object.setPrototypeOf(scxml, SCXMLState);
console.log({id: scxml.id, isState: scxml.isState});
// {id: '(scxml)', isState: true}       

It seems more elegant (less typing, more visually clear) to use classes and inheritance…but instance fields like isState are not inherited via the prototype, but set on instances. Since I'm not creating the instance using new, the instance field does not work:

class SCXMLState extends Element {
   addChild() { }

   isState = true;

   get isSCXML() {
      return this.nodeName==='scxml';
   }

   get id() {
      return this.getAttribute('id') || (this.isSCXML ? '(scxml)' : null);
   }
}

const markup = `<scxml xmlns="http://www.w3.org/2005/07/scxml" />`;
const xmldoc = (new DOMParser).parseFromString(markup, "text/xml");
const scxml  = xmldoc.documentElement;
Object.setPrototypeOf(scxml, SCXMLState.prototype);
console.log({id: scxml.id, isState: scxml.isState});
// {id: '(scxml)', isState: undefined}

Note that while the rest of the code works, the isState instance field is not inherited, showing undefined instead of true.

Is there an alternative, hopefully ~cleaner (simpler, less typing, encapsulated) syntax to define inherited instance properties for class instances, other than:

SCXMLState.prototype.isState = true
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • In the working code, `isState` is not an "instance" property, since that property is only defined on the proto object. This means that when you have multiple objects with `SCXMLState` as proto object, and you would then do `SCXMLState.isState = false;`, that all these objects see that change. Is this the intended behaviour? – trincot Feb 23 '23 at 08:11
  • @trincot Yes, that is intended. These are properties that are never intended to change, but of course an instance can have a value set that shadows the real value. – Phrogz Feb 23 '23 at 13:53
  • if it never changes there's no reason we shouldn't use getter `get isState() { return true } ` also `Object.setPrototypeOf` makes engines slower you shouldn't use it – Tachibana Shin Feb 24 '23 at 05:52
  • @TachibanaShin That is certainly a functional option. Separate from my concern about performance of invoking a function call each time the property is accessed—which might get optimized away in JIT—there is a reason: it's a lot more typing for every single property. :) Regarding `setPrototypeOf`, do you have an alternative mechanism to cause an element created by `DOMParser` to inherit new methods and properties? – Phrogz Feb 24 '23 at 16:23
  • 1
    @TachibanaShin Using `Object.setPrototypeOf` like this, right where you create the object, [doesn't actually make anything slow](https://stackoverflow.com/a/23809148/1048572) – Bergi Feb 24 '23 at 16:35
  • @Phrogz you can use constructor to directly access `Element` of newly created `DOM` without creating a class – Tachibana Shin Feb 24 '23 at 16:39
  • @Bergi if you use setPrototypeOf it means that scxml's access cache is destroyed and the parts based on it. I bet `ValidityCell` is disabled now – Tachibana Shin Feb 24 '23 at 16:49
  • @TachibanaShin Well the object was never part of any cache since it wasn't used anywhere before the prototype is changed… – Bergi Feb 24 '23 at 19:09

2 Answers2

1

It seems more elegant (less typing, more visually clear) to use classes

Nah, the main difference is not using Object.defineProperties. You can just use getter syntax in the object literal as well:

const SCXMLState = Object.setPrototypeOf({
  addChild() { },
  isState: true,
  get isSCXML: {
    return this.nodeName==='scxml';
  },
  get id() {
    return this.getAttribute('id') || (this.isSCXML ? '(scxml)' : null);
  },
}, Element.prototype);

Maybe not even use Object.setPrototypeOf but

const SCXMLState = {
  __proto__: Element.prototype,
  addChild() { },
  isState: true,
  get isSCXML: {
    return this.nodeName==='scxml';
  },
  get id() {
    return this.getAttribute('id') || (this.isSCXML ? '(scxml)' : null);
  },
};

I wouldn't use class syntax if you don't plan on using the constructor. (Though if you were using custom elements, you actually could).

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks, I didn't know about `__proto__`, and I didn't know getter/method syntax was valid in a plain object. I clearly need to deep dive on the implementation of classes within the prototype system of JS. Care to followup on the negative implications of using `class` if all I care about is creating a prototype? – Phrogz Feb 24 '23 at 16:21
  • Not really negative - except you get a useless `.constructor` on your prototype. – Bergi Feb 24 '23 at 16:32
  • 1
    Btw, `get`/`set` accessor syntax in object literals was introduced in ES5, way before classes :-) – Bergi Feb 24 '23 at 16:32
0

The following works, and is slightly more elegant than explicitly referencing the prototype each time when multiple inherited properties are desired. I'll happily accept a less clunky answer if one is presented.

class SCXMLState extends Element {
   // …
}
Object.assign(SCXMLState.prototype, {
   isState: true,
   canHaveTransitions: true,
   // …
});
Phrogz
  • 296,393
  • 112
  • 651
  • 745