penHolder
penHolder

Reputation: 377

How to instantiate a Class from a String in JavaScript

I'm in a weird situation that i need to instantiate a new Class with a string stored in a variable but even i'm sure the class name is correct i get an error that given class name is not a constructor

Here is a dummy code that doesn't work:

class Foo {
    constructor(){
        console.log('Foo!');
    }
};
const foo = 'Foo';
const bar = new window[foo]();
console.log(bar);

This trow this error:

Uncaught TypeError: window[foo] is not a constructor

Upvotes: 11

Views: 15604

Answers (3)

Teocci
Teocci

Reputation: 8945

There are good solutions but I think we need a little bit more of theory

This questions is the best example to use a Factory Pattern

Design patterns can make your code more flexible, more resilient to change and easier to maintain.

Teo, is possible to use Factory Patterns in JavaScript?

Yes, my young Padawan.

But, what is the Factory pattern?

The factory pattern is a creational design pattern, which means it deals with object creation. There are three theoretical types of factory patterns:

  • Simple factory
  • Factory method
  • Abstract factory

Simple Factory is an object which encapsulates the creation of another object. In ES6 it could be a constructor being instantiated by new in a function and then return the instance like this:

class Player {...}

const PlayerFactory = {
  makePlayer: (type, level) => new Player(type, level),
}

In this example makePlayer returns the instance of the Player class.

Factory Method defines one method for creating a class instance, which is overridden by subclasses who decide what to return.

In ES6 it could be implemented extending classes because there are no interfaces and using a method to instantiate and object using new

class Dragon {...}

class Snake {...}

class Player {
  fightMonster() {
    const monster = this.makeMonster()
    monster.attack()
  }
}

class Warrior extends Player {
  makeMonster() {
    return new Dragon()
  }
}

class Knight extends Player {
  makeMonster() {
    return new Snake()
  }
}

In this example, Player class call makeMonster method then Warrior and Knight classes override makeMoster method to return either a Dragon or a Snake class instance.

Finally, the Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes

The interpretation of families could be like a category or list of classes. In ES6 it can be an instance that encapsulates a group of individual factories with a common goal. It separates the details of implementation of a set of objects from their general usage.

class WinButton {
  constructor(options) {
    this.name = 'WinButton'
    this.options = options
  }
}

class LinuxButton  {
  constructor(v) {
    this.name = 'LinuxButton'
    this.options = options
  }
}


// Mapping names and class definitions    
const supportedOS = new Map([
  ['Windows', WinButton],
  ['Linux', LinuxButton]
])

// Use a Factory Abstract pattern
const classFactory = (c, v) => {
  const maker = supportedOS.get(c)
  return new(maker)(v)
}

// Factory a class from a string
const name = 'Windows'
const param = {enabled: true}

const btn = classFactory(name, param)

console.log({btn})

In this final example we can see that the function classFactory uses the Factory Abstract pattern because instantiate a class from a list of supported OS (Linux or Windows) by setting maker with the constructor of the desired class, in this case WinButton class.

Upvotes: 5

MFAL
MFAL

Reputation: 1110

Similar to @jfriend00 ...

const className = "Foo";

const dynamicConstructor = {};
dynamicConstructor[className] = class {
  constructor() {
    console.log('Foo!');
  }
};

const fooInstance = new dynamicConstructor[className]();
console.log(fooInstance);

A sort of factory class constructor can also be used

const classFactory = (_className) => {
  let dynamicConstructor = {};
  dynamicConstructor[_className] = class {
    constructor(_code) {
      this.code = _code;
      console.log(`${_className} initialised with code: ${_code}!`);
    }
  };
  return dynamicConstructor[_className];
}

const MyClass = classFactory("Foo");
let fooInstance2 = new MyClass(123);

console.debug(fooInstance2);

Upvotes: 2

jfriend00
jfriend00

Reputation: 707826

One possibility is to use eval.

class Foo {
  constructor() {
    console.log('Foo!');
  }
};
const foo = 'Foo';
const bar = eval(`new ${foo}()`);
console.log(bar);

You will have to evaluate the safety of using eval() in your particular circumstances. If you know the origin of the string you are inserting into the code that you run eval() on or you can sanitize it first, then it may be safe.


I personally would prefer a lookup table. If you have a known number of classes that you want to map by string, then you can make your own lookup table and use that. This has the advantage of there can be no unintended consequences if the string has weird stuff in it:

class Foo {
  constructor() {
    console.log('Foo!');
  }
};

class Goo {
  constructor() {
    console.log('Goo!');
  }
};

// construct dict object that contains our mapping between strings and classes    
const dict = new Map([
  ['Foo', Foo],
  ['Goo', Goo]
]);

// make a class from a string
const foo = 'Foo';
let bar = new(dict.get(foo))()

console.log(bar);

If you were really going to go this route, you may want to encapsulate it in a function and then add error handling if the string is not found in the dict.

This should be better than using the global or Window object as your lookup mechanism for a couple reasons:

  1. If I recall, class definitions in ES6 are not automatically put on the global object like they would with other top level variable declarations (Javascript trying to avoid adding more junk on top of prior design mistakes).

  2. So, if you're going to manually assign to a lookup object, you might as well use a different object and not pollute the global object. That's what the dict object is used for here.

Upvotes: 27

Related Questions