开发者问题收集

JS ECMA5:将对象转换为子类

2022-07-22
95

代码示例:

ClassA.js

var classB = require('./ClassB');

function ClassA() {
    this.ID = undefined;
    this.Type = undefined;
    ....
}

ClassA.prototype.init = function init(id){
    this.ID = id;

    this.get();

    if (this.Type === 'C' && Object.getPrototypeOf(this) === ClassA.prototype) {
        return new classB().init(this.ID);
    }
}

ClassB.js

function ClassB() {
    ClassA.call(this);
    this.additionalProp = undefined;
}

ClassB.prototype = Object.create(ClassA.prototype);
ClassB.prototype.constructor = ClassB;

我已实现两个类 ClassA 和 ClassB。 ClassB 是 CLassA 的子类,并具有一些附加属性。

ClassB 的原型链设置如下:

B.prototype = Object.create(A.prototype); B.prototype.constructor = B;

对象的信息是通过 ID 从 API 中检索的。 在调用 API 时,我不知道对象是否需要是 ClassA 或 ClassB 的实例。因此,我总是从 ClassA 的对象开始调用 API。 当 API 返回特定类型时,我想将我的对象从 ClassA 转换为更具体的 ClassB。

我试图在 ClassA 之外调用 ClassB 的构造函数 - 这会引发错误:

未捕获的 TypeError:对象原型可能只是一个对象或 null:未定义

我认为我根本不应该在 ClassA 中引用 ClassB,但值得一试...

感谢您帮助新手!:)

2个回答

At the time of the API call I do not know if the object needs to be an instance of ClassA or ClassB. So I always start with an object of one class, then when the API returns a specific type I want to convert my object.

千万不要这么做。在 API 返回后 构造您的对象 ,并首先使用正确的类型构造它。

I always start with an object of the class to call the API

这是您的设计出错的地方。该类不应负责调用 API,它应该只负责表示数据并让方法对数据起作用。不要从一个可以以某种方式自我补充的空对象开始,让构造函数从作为参数传递的数据中完全初始化对象。

将 API 调用放在类之外的单独函数中。也许创建一个单独的类来表示 API 端点。如果绝对必要,请将其设为基类的静态方法,然后它仍然可以创建 new ClassAnew ClassB 实例。

Bergi
2022-07-22

您在评论中提到了这个错误:

Uncaught TypeError: Object prototype may only be an Object or null: undefined

您之所以会得到这个错误,是因为 ClassA.jsClassB.js 之间存在循环关系:每个都试图导入其他导出的内容。在某些情况下,这样做可能没问题,但在这里, ClassB.js 中的代码试图在顶层代码中使用 ClassA.prototype ,而 ClassA.js 又从 ClassB.js 导入 ClassB 。您最终会得到一个 ClassA 导入的占位符,而 Object.create(ClassA.prototype) 调用不起作用。

如果您在同一个文件中定义它们,这将容易得多,这要归功于函数声明提升。

我还会修改 init ,以便它 始终 返回实例,因为您有时需要返回一个新对象,但其他时候则不需要。因此,让它始终返回实例可以简化调用代码。

这是一个最小更改示例:

function ClassA() {
    this.ID = undefined;
    this.Type = undefined;
    // ....
}
ClassA.prototype.get = function () {
    // *** Just for debugging, ID 2 is `ClassB`, any other is `ClassA`
    this.Type = this.ID === 2 ? "C" : "X";
};

ClassA.prototype.init = function init(id) {
    this.ID = id;

    this.get();

    if (this.Type === "C" && Object.getPrototypeOf(this) === ClassA.prototype) {
        return new ClassB().init(id); // *** Use `ClassB`, not `ClassA`
    }

    return this; // *** So the caller always gets an instance they can use
};

function ClassB() {
    ClassA.call(this);
    this.additionalProp = undefined;
}

ClassB.prototype = Object.create(ClassA.prototype);
ClassB.prototype.constructor = ClassB;

module.exports.ClassA = ClassA;
module.exports.ClassB = ClassB;

然后使用它(仅用于示例):

var both = require("./both");
var ClassA = both.ClassA;
var ClassB = both.ClassB;

var obj1 = new ClassA();
obj1 = obj1.init(1);
console.log(obj1 instanceof ClassA); // true
console.log(obj1 instanceof ClassB); // false

var obj2 = new ClassA();
obj2 = obj2.init(2);
console.log(obj2 instanceof ClassA); // true
console.log(obj2 instanceof ClassB); // true

话虽如此,我想我会重构它。您曾说过,有一个单独的 init 方法,因为有时您想在拥有 id 之前对对象使用方法。这让我觉得 ClassA (至少)试图做太多事情,既包括它在不知道它是什么时(没有 id )可以做的事情,也包括它在知道它是什么时可以做的事情。构造函数返回的实例应该完全准备好并随时可用。因此,最好将 ClassA 中不需要 id 的部分拆分成其他部分。这也意味着 ClassA 不必引用 ClassB ,这不是最佳实践。

我想我可能还会将 get 分离出来,与类分开,并让它返回适当的实例。

例如:

ClassA.js

function ClassA(data) {
    this.ID = data.id;
    this.Type = data.type;
    // ....
}

// ...other `ClassA` methods...

module.exports = ClassA;

ClassB.js

var ClassA = require("./ClassA");

function ClassB(data) {
    ClassA.call(this, data);
    this.additionalProp = data.additionalProp;
}

ClassB.prototype = Object.create(ClassA.prototype);
ClassB.prototype.constructor = ClassB;

// ...other `ClassB` methods...

module.exports = ClassB;

get.js (或其他):

var ClassA = require("./ClassA");
var ClassB = require("./ClassB");

function get(id) {
    var data = /*...get from API...*/;
    var cls = "additionalData" in data ? ClassB : ClassA;
    return new cls(data);
}

这提供了更好的关注点分离。

T.J. Crowder
2022-07-22