在JavaScript ECMAScript 6中,如何通过类名创建对象?

20
我希望使用ES6创建对象工厂,但旧的语法在新版本中无法使用。
我有以下代码:

我想创建一个对象工厂,使用ES6语法,但是旧的语法在新版本中无法使用。

我有以下代码:

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');

我做错了什么?


FYI,手动编写的ES5版本在这里可以工作:http://jsfiddle.net/jfriend00/4x45gqLt/。可能值得看一下babeljs生成的代码,看看有什么不同。显然,“Column”不是全局变量(因此不在“window”对象上),但生成的ES5代码肯定会告诉你。 - jfriend00
嗯,window[className]从来没有可靠地工作过。 - Bergi
9个回答

15

有一种简单而不太正规的方法来做到这一点:

function createClassByName(name,...a) {
    var c = eval(name);
    return new c(...a);
}
你现在可以创建一个类,就像这样:
let c = createClassByName( 'Person', x, y );

我喜欢这个 eval 解决方案。 - Dee
5
我也点赞。我知道大家都谴责 eval 函数,但只要你对可能的值有完全控制,eval 是最简单、最灵活的解决方案。 - Iarwa1n
1
我喜欢这个 eval,但现在有问题了,在 ES6 类方法中无法使用 eval 创建变量。 - Dee
我认为使用eval()存在安全风险。eval()是一个函数,它可以将字符串作为代码来执行。这意味着任何具有恶意意图的字符串都可以被执行,从而导致代码注入和其他安全问题。建议尽可能避免使用eval(),而使用更安全的可替代方案。 - Frederick G. Sandalo
eval很棒 - user3413723
如何告诉 eval 在哪里查找类,以解决 "ReferenceError: Person is not defined." 的问题? - undefined

15
不要将类名放在该对象上,而是将类本身放在那里,这样您就不必依赖它们作为全局变量并且可以通过window在浏览器中访问。
顺便说一句,没有什么好理由使这个工厂成为一个类,您可能只会实例化它一次(单例)。只需将其设置为对象即可。
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);
    }
};

实际上是一个很棒的工厂示例,我在我的项目中使用了它 :) - Oleg Abrazhaev

6
问题在于类不是window对象的属性。你可以创建一个对象,其中包含指向你的类的属性:
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};

Column 构造函数不是全局的吗? - jfriend00
显然不在babeljs中(无法发布repl,太长了) - Amit

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'];

2

如果您尚未使用ES6并且想知道如何通过使用字符串创建类,以下是我所做的工作。

"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));

您可以看到,实现这一点最简单的方法是将类添加到一个对象中。

这里是代码演示: https://jsfiddle.net/zxg7dsng/1/

这是一个使用此方法的示例项目: https://github.com/pdxjohnny/dist-rts-client-web


2
我知道这是一个旧帖子,但最近我也有同样的问题,那就是如何动态实例化一个类。
我正在使用 webpack,所以根据文档,可以使用 import() 函数动态加载一个模块。

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);
});

警告:这种方法是异步的,我无法真正告诉你它的性能成本,但在我的简单程序中它完美地运行(值得一试^^)。
附注:公平地说,我是Es6的新手(几天),我更多地是C++/PHP/Java开发人员。
我希望这可以帮助任何遇到这个问题的人,并且这不是一个不好的做法^^"。

2

澄清
有类似的问题,包括这个SO问题,它们都在寻找JavaScript中的代理类或工厂函数;也称为动态类。如果您正在寻找这些内容之一,本答案是现代解决方案。

答案/解决方案
截至2022年,我认为浏览器中有一个更优雅的解决方案。我创建了一个名为Classes的类,该类自注册属性Class(大写C)在window上;以下是代码示例。

现在,您可以让想要能够动态引用的类全局自行注册:

// 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);

然后在您的代码中稍后,您只需要获取其原始引用的类名即可:

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

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

或者更简单的方法是,立即实例化它:
// Directly instantiate class for use
const muscleMan = Class.new('Handler', ...args);

代码
您可以在我的gist上查看最新代码。将此脚本添加到所有其他脚本之前,您的所有类都将能够注册它。

/**
 * 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;

谢谢你的解决方案!但在我的情况下,为了让它工作,我不得不在 classes.js 中添加 export { Class}; 并在 “主文件”(在浏览器的脚本标签中调用的文件)中导入它。我还需要做错其他什么吗? - nhaggen
1
嗨,@nhaggen,我的代码示例假定您将在头部使用“script”标记加载JS。您使用的是import/export,这完全没问题;您没有搞砸任何东西。如果您在项目中使用import/exports,则您所做的更改是正确的。 - Blizzardengle

1

这是一个老问题,但我们可以找到三种主要的方法,它们非常聪明和有用:

1. 丑陋的方法

我们可以使用eval来实例化我们的类,如下所示:

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})

然而,eval 有一个很大的缺陷,如果我们不进行清洗并添加一些安全层,则会存在一个严重的安全漏洞。

2. 好处

如果我们知道将要使用的类名,那么我们可以创建一个类似于以下的查找表:

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. 设计模式

最后,我们可以创建一个工厂类,安全地处理允许的类,并在无法加载时抛出错误。

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')

我推荐使用“良好”方法。它干净并且安全。此外,它应该比使用globalwindow对象更好:
ES6中的class定义不会像其他顶级变量声明那样自动放置在global对象上(JavaScript试图避免在以前的设计错误基础上添加更多垃圾)。
因此,我们将不会污染global对象,因为我们使用本地的classMap对象来查找所需的class

0

我发现在 TypeScript 上实现这很容易。让我们有一个现有的 Test 类和一个现有的 testMethod。您可以使用字符串变量动态初始化类。

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]();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接