19

I want create object factory using ES6 but old-style syntax doesn't work with new.

I have next code:

export class Column {}
export class Sequence {}
export class Checkbox {}

export class ColumnFactory {
    constructor() {
        this.specColumn = {
            __default: 'Column',
            __sequence: 'Sequence',
            __checkbox: 'Checkbox'
        };
    }

    create(name) {
        let className = this.specColumn[name] ? this.specColumn[name] : this.specColumn['__default'];
        return new window[className](name); // this line throw error
    }
}

let factory = new ColumnFactory();
let column = factory.create('userName');

What do I do wrong?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Serhii Popov
  • 3,326
  • 2
  • 25
  • 36
  • FYI, the manually coded ES5 version of this works here: http://jsfiddle.net/jfriend00/4x45gqLt/. Probably worth looking at what babeljs produces to see what is different. Apparently `Column` is not global (and thus not on the `window` object), but the generated ES5 code would show you for sure. – jfriend00 Aug 02 '15 at 22:09
  • Um, `window[className]` never worked reliably. – Bergi Aug 03 '15 at 14:23

9 Answers9

15

Don't put class names on that object. Put the classes themselves there, so that you don't have to rely on them being global and accessible (in browsers) through window.

Btw, there's no good reason to make this factory a class, you would probably only instantiate it once (singleton). Just make it an object:

export class Column {}
export class Sequence {}
export class Checkbox {}

export const columnFactory = {
    specColumn: {
        __default: Column,    // <--
        __sequence: Sequence, // <--
        __checkbox: Checkbox  // <--
    },
    create(name, ...args) {
        let cls = this.specColumn[name] || this.specColumn.__default;
        return new cls(...args);
    }
};
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
14

There is a small & dirty way to do that:

function createClassByName(name,...a) {
    var c = eval(name);
    return new c(...a);
}

You can now create a class like that:

let c = createClassByName( 'Person', x, y );
eco747
  • 227
  • 2
  • 6
6

The problem is that the classes are not properties of the window object. You can have an object with properties "pointing" to your classes instead:

class Column {}
class Sequence {}
class Checkbox {}
let classes = {
  Column,
  Sequence,
  Checkbox 
}

class ColumnFactory {
    constructor() {
        this.specColumn = {
            __default: 'Column',
            __sequence: 'Sequence',
            __checkbox: 'Checkbox'
        };
    }

    create(name) {
        let className = this.specColumn[name] ? this.specColumn[name] : this.specColumn['__default'];
        return new classes[className](name); // this line no longer throw error
    }
}

let factory = new ColumnFactory();
let column = factory.create('userName');

export {ColumnFactory, Column, Sequence, Checkbox};
Amit
  • 45,440
  • 9
  • 78
  • 110
2

For those of you that are not using ES6 and want to know how you can create classes by using a string here is what I have done to get this to work.

"use strict";

class Person {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
}
window.classes = {};
window.classes.Person = Person;

document.body.innerText = JSON.stringify(new window.classes["Person"](1, 2));

As you can see the easiest way to do this is to add the class to an object.

Here is the fiddle: https://jsfiddle.net/zxg7dsng/1/

Here is an example project that uses this approach: https://github.com/pdxjohnny/dist-rts-client-web

pdxjohnny
  • 21
  • 5
2

I prefer this method:

allThemClasses.js

export class A {}
export class B {}
export class C {}

script.js

import * as Classes from './allThemClasses';

const a = new Classes['A'];
const b = new Classes['B'];
const c = new Classes['C'];
fredrik.hjarner
  • 715
  • 8
  • 22
2

I know this is an old post, but recently I've had the same question about how to instance a class dynamically

I'm using webpack so following the documentation there is a way to load a module dynamically using the import() function

js/classes/MyClass.js

class MyClass {
    test = null;
    constructor(param) {
        console.log(param)
        this.test = param;
    }
}

js/app.js

var p = "example";
var className = "MyClass";

import('./classes/'+className).then(function(mod) {
    let myClass = new mod[className](p);
    console.log(myClass);
}, function(failMsg) {
    console.error("Fail to load class"+className);
    console.error(failMsg);
});

Beware: this method is asynchronous and I can't really tell the performance cost for it, But it works perfectly on my simple program (worth a try ^^)

Ps: To be fare I'm new to Es6 (a couple of days) I'm more a C++ / PHP / Java developer.

I hope this helps anyone that come across this question and that is it not a bad practice ^^".

Karl FARES
  • 21
  • 1
2

Clarification
There are similar questions to this, including this SO question that was closed, that are looking for proxy classes or factory functions in JavaScript; also called dynamic classes. This answer is a modern solution in case you landed on this answer looking for any of those things.

Answer / Solution
As of 2022 I think there is a more elegant solution for use in the browser. I made a class called Classes that self-registers the property Class (uppercase C) on the window; code below examples.

Now you can have classes that you want to be able to reference dynamically register themselves globally:

// Make a class:
class Handler {
    handleIt() {
        // Handling it...
    }
}

// Have it register itself globally:
Class.add(Handler);

// OR if you want to be a little more clear:
window.Class.add(Handler);

Then later on in your code all you need is the name of the class you would like to get its original reference:

// Get class
const handler = Class.get('Handler');

// Instantiate class for use
const muscleMan = new (handler)();

Or, even easier, just instantiate it right away:

// Directly instantiate class for use
const muscleMan = Class.new('Handler', ...args);

Code
You can see the latest code on my gist. Add this script before all other scripts and all of your classes will be able to register with it.

/**
 * Adds a global constant class that ES6 classes can register themselves with.
 * This is useful for referencing dynamically named classes and instances
 * where you may need to instantiate different extended classes.
 *
 * NOTE: This script should be called as soon as possible, preferably before all
 * other scripts on a page.
 *
 * @class Classes
 */
class Classes {

    #classes = {};

    constructor() {
        /**
         * JavaScript Class' natively return themselves, we can take advantage
         * of this to prevent duplicate setup calls from overwriting the global
         * reference to this class.
         *
         * We need to do this since we are explicitly trying to keep a global
         * reference on window. If we did not do this a developer could accidentally
         * assign to window.Class again overwriting any classes previously registered.
         */
        if (window.Class) {
            // eslint-disable-next-line no-constructor-return
            return window.Class;
        }
        // eslint-disable-next-line no-constructor-return
        return this;
    }

    /**
     * Add a class to the global constant.
     *
     * @method
     * @param {Class} ref The class to add.
     * @return {boolean} True if ths class was successfully registered.
     */
    add(ref) {
        if (typeof ref !== 'function') {
            return false;
        }
        this.#classes[ref.prototype.constructor.name] = ref;
        return true;
    }

    /**
     * Checks if a class exists by name.
     *
     * @method
     * @param {string} name The name of the class you would like to check.
     * @return {boolean} True if this class exists, false otherwise.
     */
    exists(name) {
        if (this.#classes[name]) {
            return true;
        }
        return false;
    }

    /**
     * Retrieve a class by name.
     *
     * @method
     * @param {string} name The name of the class you would like to retrieve.
     * @return {Class|undefined} The class asked for or undefined if it was not found.
     */
    get(name) {
        return this.#classes[name];
    }

    /**
     * Instantiate a new instance of a class by reference or name.
     *
     * @method
     * @param {Class|name} name A reference to the class or the classes name.
     * @param  {...any} args Any arguments to pass to the classes constructor.
     * @returns A new instance of the class otherwise an error is thrown.
     * @throws {ReferenceError} If the class is not defined.
     */
    new(name, ...args) {
        // In case the dev passed the actual class reference.
        if (typeof name === 'function') {
            // eslint-disable-next-line new-cap
            return new (name)(...args);
        }
        if (this.exists(name)) {
            return new (this.#classes[name])(...args);
        }
        throw new ReferenceError(`${name} is not defined`);
    }

    /**
     * An alias for the add method.
     *
     * @method
     * @alias Classes.add
     */
    register(ref) {
        return this.add(ref);
    }

}

/**
 * Insure that Classes is available in the global scope as Class so other classes
 * that wish to take advantage of Classes can rely on it being present.
 *
 * NOTE: This does not violate https://www.w3schools.com/js/js_reserved.asp
 */
const Class = new Classes();
window.Class = Class;
Blizzardengle
  • 992
  • 1
  • 17
  • 30
  • Thanks for your solution! But in my case, to have it working I had to add `export { Class}; ` in **classes.js** and import it in the **"main-file"** (the one that is called in the script-tag of the browser). Did I mess up something else to need this? – nhaggen Apr 13 '22 at 10:19
  • 1
    Hi @nhaggen, my code examples assumed you would be using `script` tags in the head to load your JS. You used import/export which is totally fine; you didn't mess anything up. The changes you made are correct if you use import/exports in you project. – Blizzardengle Apr 14 '22 at 11:11
1

This is an old question but we can find three main approaches that are very clever and useful:

1. The Ugly

We can use eval to instantiate our class like this:

class Column {
  constructor(c) {
    this.c = c
    console.log(`Column with ${this.c}`);
  }
}

function instantiator(name, ...params) {
  const c = eval(name)
  return new c(...params)
}

const name = 'Column';
const column = instantiator(name, 'box')
console.log({column})

However, eval has a big caveat, if we don't sanitize and don't add some layers of security, then we will have a big security whole that can be expose.

2. The Good

If we know the class names that we will use, then we can create a lookup table like this:

class Column {
  constructor(c) {
    console.log(`Column with ${c}`)
  }
}

class Sequence {
  constructor(a, b) {
    console.log(`Sequence with ${a} and ${b}`)
  }
}

class Checkbox {
  constructor(c) {
    console.log(`Checkbox with ${c}`)
  }
}

// construct dict object that contains our mapping between strings and classes    
const classMap = new Map([
  ['Column', Column],
  ['Sequence', Sequence],
  ['Checkbox', Checkbox],
])

function instantiator(name, ...p) {
  return new(classMap.get(name))(...p)
}

// make a class from a string
let object = instantiator('Column', 'box')
object = instantiator('Sequence', 'box', 'index')
object = instantiator('Checkbox', 'box')

3. The Pattern

Finally, we can just create a Factory class that will safety handle the allowed classes and throw an error if it can load it.

class Column {
  constructor(c) {
    console.log(`Column with ${c}`)
  }
}

class Sequence {
  constructor(a, b) {
    console.log(`Sequence with ${a} and ${b}`)
  }
}

class Checkbox {
  constructor(c) {
    console.log(`Checkbox with ${c}`)
  }
}

class ClassFactory {
  static class(name) {
    switch (name) {
      case 'Column':
        return Column
      case 'Sequence':
        return Sequence
      case 'Checkbox':
        return Checkbox
      default:
        throw new Error(`Could not instantiate ${name}`);
    }
  }

  static create(name, ...p) {
    return new(ClassFactory.class(name))(...p)
  }
}

// make a class from a string
let object
object = ClassFactory.create('Column', 'box')
object = ClassFactory.create('Sequence', 'box', 'index')
object = ClassFactory.create('Checkbox', 'box')

I recommend The Good method. It is is clean and safe. Also, it should be better than using global or window object:

  • 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).

  • Therefore we will not pollute the global object because we are using a local classMap object to lookup the required class.

Teocci
  • 7,189
  • 1
  • 50
  • 48
0

I found this easy to implement on TypeScript. Let's have an existing Test class with existing testMethod. You can initiate your class dynamically with string variables.

class Test {
     constructor()
     {
     }
     testMethod() 
     { 
     }
   }

    // Class name and method strings 
    let myClassName = "Test";
    let myMethodName = "testMethod";

    let myDynamicClass = eval(myClassName);

    // Initiate your class dynamically
    let myClass = new myDynamicClass();

    // Call your method dynamically
    myClass[myMethod]();
doraemon
  • 325
  • 1
  • 4
  • 16