Search code examples
javascriptclassdynamicconstructorsubclassing

How does JavaScript "decide" how to print a class name?


I'm trying to write an inheritence logic where I clone an input class and inherit from remaining parent classes. In order to do that, I need to create a new class, deep copying one of the classes exactly. I'm trying to achieve something like:

class Original {static b=1; static test2(){}; test(){}}
var CopyClass = DeepCopySomehow(Original)

class NotWorking extends Original{}

console.log(Object.getOwnPropertyNames(NotWorking))
// [ 'length', 'name', 'prototype']

console.log(Object.getOwnPropertyNames(Original))
// [ 'length', 'name', 'prototype', 'test2', 'b' ]

console.log(Object.getOwnPropertyNames(NotWorking.prototype))
// ['constructor']

console.log(Object.getOwnPropertyNames(Class.prototype))
// ['constructor', 'test']

I have something like (simplified):

function inherit(Class1, Class2) {
  class Base extends Class1 {...stuff...}
  Object.defineProperty(Base, 'name', {value: Class1.name});
 
  copy_props(Base.prototype, Class1.prototype);
  copy_props(Base, Class1.prototype);
  copy_props(Base.prototype, Class2.prototype);
  copy_props(Base, Class2.prototype);
}

however, this still keeps the information of "Base" somehow.
Browser side -- Here is a reproducable example:

class SpecificParentName{constructor() {return new Proxy(this, {})}}
const Base = class extends SpecificParentName{constructor(...args){super(...args)}}
Base.toString = () => SpecificParentName.toString()
Object.defineProperty(Base, 'name', {value: SpecificParentName.name});
console.log(Base)
// class extends SpecificParentName{constructor(...args){super(...args)}}
// reasonable output, although I would have wanted it to be just class SpecificParentName if possible

console.log(new Base())
// Proxy(Base) {} // definitely not desired, because it doesn't point to SpecificParentName

console.log(new Proxy(Base, {}))
// Proxy(Function) {length: 0, name: 'SpecificParentName', prototype: SpecificParentName}
// it's ok since points to SpecificParentName

Nodejs side -- I also had a similar problem in nodejs side before:

class SpecificParentName{}
console.log(SpecificParentName)
// "[class SpecificParentName]"
const Base = class extends SpecificParentName{}
console.log(Base)
// [class Base extends SpecificParentName]
// I'd like this^ to be just "[class SpecificParentName]"

// hacky fix on nodejs:
const Base2 = class extends SpecificParentName{
    static [require('util').inspect.custom]() {return `[class ${SpecificParentName.name}]`}
console.log(Base2)
// "[class SpecificParentName]"
}

so my question is, why and how javascript knows about the variable name I use when defining a class when printing, and is there a way to customize it?


Solution

  • Q. "How does JavaScript "decide" how to print a class name?"

    Firstly, every function (including class constructor function) has an own name property.

    Secondly, how this name is going to be exposed is part of the function-type specific toString implementation. In case of instances of built-in types the class name gets additionally exposed by the initial Symbol.toStringTag implementation. The latter behavior is not shown by instances of custom class implementations (theirs default always returns '[object Object]').

    Last, both implementation can be overwritten; and as for function names (class constructor functions included), any unnamed/anonymous function or class expression that gets assigned to a variable or property does get assigned this variable's or property's name to its own name property.

    Thus, everything the OP wants to achieve can be done through a pattern of dynamic subclassing/sub-typing, implemented as single factory function which creates a named subclass.

    Said function does expect 3 parameters ...

    1. the later to be exposed class name

      • once for the created and returned subclass,
      • twice for the subclass' toString method,
      • and third for returning the implementation detail for every instance that does get processed by Object.prototype.toString.call(subclassInstance)
    2. the to be extended super or base class

    3. an optional initializer function which to a certain extent does cover the functionality of a constructor function.

    Example code ...

    class SpecificParentName {}
    
    console.log({
      SpecificParentName,
      'SpecificParentName.name': SpecificParentName.name,
    });
    
    /*
     * ... OP's request ...
     */
    const Base = createNamedSubclass('SpecificParentName', SpecificParentName);
    
    console.log({
      Base,
      'Base.name': Base.name,
    });
    
    const baseInstance = new Base;
    
    console.log(
      'Object.prototype.toString.call(baseInstance) ...',
      Object.prototype.toString.call(baseInstance)
    );
    .as-console-wrapper { min-height: 100%!important; top: 0; }
    <script>
    function createNamedSubclass(
      className = 'UnnamedType' ,
      baseClass = Object,
      initializer = (...args) => args,
    ) {
      const subClass = ({
      
        // - ensures the class constructor's name.
        [className]: class extends baseClass {
        
          constructor(...args) {
    
            super(...args);
    
            initializer.apply(this, args);
          }
        },
      })[className];
    
      // - responsible for exposing the constructor's intended stringification.
      Reflect.defineProperty(subClass, 'toString', {
        value: () => `class ${ className }{}]`,
      });
      
      // - responsible for exposing an instance' class implementation detail.
      Reflect.defineProperty(subClass.prototype, Symbol.toStringTag, {
        get: () => className,
      });
    
      return subClass;
    }
    </script>

    One could dive deeper into the dynamic subclassing/sub-typing matter by reading e.g. "How to dynamically create a subclass from a given class and enhance/augment the subclass constructor's prototype?"