如何在JavaScript中克隆一个静态类?

3

我有一个模型的模拟,我想这样重复使用:

// simplified
class ModelMock {
    static async findOneAndUpdate() {

    }
    static async findOne() {

    }
    async save() {

    }
  }

但是需要根据模型分别对它们进行模拟,例如

const models = {
  User: ModelMock,
  Business: ModelMock
}

但我真的希望每个模拟类都是独立的实体,而不必使用原型语法或复制代码。
原因在于测试中...
sinon.mock(MockModule.prototype).expects('save').resolves({ specific: 'thing' })

由于每个模型都需要一个特定的类,因此这种方法将无法使用,而且静态方法当然是共享的。

请注意静态方法和实例方法

那么我该怎么做呢?


如果你想让每个实例都是独立的,为什么不使用“new ModelMock”,这样你的save()方法对于每个实例都是唯一的呢?const models = { User: new ModelMock, Business: new ModelMock } - nopuck4you
@nopuck4you 模型需要同时具有静态和实例方法,因为它既可以由客户端代码创建新实例,又可以同时静态使用。 - King Friday
你不应该只有静态方法的 class。应该使用对象字面量代替。 - Bergi
@Bergi 实际上两者都有,但我正在模拟一个外部类,例如 mongoose,因此在这种情况下我没有决定实现细节的奢侈。 - King Friday
3个回答

2

类工厂在这种情况下似乎有效

我采用了以下方式进行类工厂:

function getModelMock() {
  return class {
    static async findOneAndUpdate() {

    }
    static async findOne() {

    }
    async save() {

    }
  }
}

最初的回答是:你可以像这样使用:

就像这样:

const models = {
  Business: getModelMock(),
  User: getModelMock()
}

sinon.mock(models.Business.prototype).expects('save').resolves({ _id: 'businessId' })
sinon.mock(models.Business).expects('findOne').resolves({ _id: 'businessId' })
sinon.mock(models.User.prototype).expects('save').resolves({ _id: 'userId' })

你可以匿名地创建一个类,而无需声明它的名称,这让我觉得很有趣。但如果你不能创建工厂,是否有更好的方法使用实际克隆来完成这项任务呢?"最初的回答":据我所知,如果你想创建一个类的克隆,你需要声明其名称。没有工厂,你可能会考虑使用原型模式来进行克隆。

1
这个答案故意详细说明“我认为你不能”,以帮助其他试图做同样事情的人理解我为什么得出这个结论,至少不使用eval()可能甚至连这个都不行。我对JavaScript原型类实现有相当深入的了解,但有许多人比我更深入了解。

我的要求

我在这里提出了这个问题,并最终选择了一个类工厂。我的要求是克隆的类与使用类工厂获得的类完全相同。具体来说,我想要一个函数,使得这个函数:

class Parent {}
class Child extends Parent {}
const Sibling = cloneClass(Child)

结果与此完全相同:
class Parent {}
function classFactory() {
  return class Child extends Parent {}
}
const Child = classFactory()
const Sibling = classFactory()

我的目的中有以下是不能接受的:

Things that are thus dealbreakers for my purposes:

  • SiblingChild 的实例或者原型继承自 Child(它的第一个原型应该是 Parent
  • SiblingChild 共享原型或函数定义

对于我的需求,如果 Sibling.name == Child.name == 'Child',那么这将非常有用,这在类工厂设置中是成立的。

严格来说,还有更多要求才能完全相同(例如,Sibling 的方法不能从 Child 的方法原型继承下来),但我认为这是无关紧要的,很快就会明白为什么。

难点

根据我的理解,使这件事情变得不可能的原因其实很简单:它需要克隆函数,而你无法做到这一点。这里有一些关于此的问题解决方案,它们在某些情况下可以工作,但并非真正意义上的克隆,并且不能满足我的要求。值得一提的是,函数也被列为 lodash 的“无法克隆的值”之一。
这与类方法相关,但更为关键的是,它与类实际上是函数有关。从字面上讲,一个类就是它的构造函数。因此,即使你能够处理从 Child 方法继承的类方法,我认为你也无法避免这样一个事实,即 Sibling 类本身 - 它是一个函数 - 也必须从 Child 继承。
英译中:

编辑:如果Child没有构造函数,那么您可能可以绕过此要求。

其他想法/注意事项

我很高兴被证明是错误的,或者我的理解被纠正了,但我认为这是中心障碍:类是函数,你不能克隆一个函数。

我没有追求的一条路是使用Function()构造函数,几乎但不完全地评估自己成为一个真正的“克隆”函数。我不确定这是否可能,而且我对这样做的影响不够了解,因此不敢尝试。

要求演示

如果您想尝试一下,我制作了一个片段,其中包含一些测试,这些测试断言我的要求-如果您可以使用克隆函数使我的要求通过,请告诉我!

编辑:@Bergi提供了一个解决方案,我已将其包含在下面的代码片段中。它似乎能够完成工作!我相信它避免了通过克隆构造函数来实现的问题,因为在我的情况下,子类没有自己的构造函数。因此,一个空函数(其他所有内容都附加在其中)确实等同于克隆。它还带有关于使用setPrototype的所有标准免责声明。

'use strict'
let hadWarning = false
const getProto = Object.getPrototypeOf

class Parent {}
function classFactory () {
  return class Child extends Parent {}
}

function factoryTest () {
  const Child = classFactory()
  const Sibling = classFactory()

  runTest(Child, Sibling, 'classFactory')
}

/* Adapted from @Bergi */

function cloneClass (Target, Source) {
  return Object.defineProperties(
    Object.setPrototypeOf(
      Target,
      Object.getPrototypeOf(Source)
    ),
    {
      ...Object.getOwnPropertyDescriptors(Source),
      prototype: {
        value: Object.create(
          Object.getPrototypeOf(Source.prototype),
          Object.getOwnPropertyDescriptors(Source.prototype)
        )
      }
    }
  )
}

function berghiTest () {
  class Child extends Parent {}
  const Sibling = cloneClass(function Sibling () {}, Child)

  runTest(Child, Sibling, 'Bergi\'s clone')
}

factoryTest()
berghiTest()

/* Assertion support */
function fail (message, warn) {
  if (warn) {
    hadWarning = true
    console.warn(`Warning: ${message}`)
  } else {
    const stack = new Error().stack.split('\n')
    throw new Error(`${message} ${stack[3].trim()}`)
  }
}

function assertEqual (expected, actual, warn) {
  if (expected !== actual) {
    fail(`Expected ${actual} to equal ${expected}`, warn)
  }
}
function assertNotEqual (expected, actual, warn) {
  if (expected === actual) {
    fail(`Expected ${actual} to not equal ${expected}`, warn)
  }
}

function runTest (Child, Sibling, testName) {
  Child.classTag = 'Child'
  Sibling.classTag = 'Sibling'

  hadWarning = false

  assertEqual(Child.name, 'Child')
  assertEqual(Sibling.name, Child.name, true) // Maybe not a hard requirement, but nice
  assertEqual(Child.classTag, 'Child')
  assertEqual(Sibling.classTag, 'Sibling')

  assertEqual(getProto(Child).name, 'Parent')
  assertEqual(getProto(Sibling).name, 'Parent')
  assertEqual(getProto(Child), Parent)
  assertEqual(getProto(Sibling), Parent)

  assertNotEqual(Child.prototype, Sibling.prototype)
  assertEqual(getProto(Child.prototype), Parent.prototype)
  assertEqual(getProto(Sibling.prototype), Parent.prototype)

  const child = new Child()
  const sibling = new Sibling()

  assertEqual(sibling instanceof Child, false)
  assertEqual(child instanceof Parent, true)
  assertEqual(sibling instanceof Parent, true)

  if (hadWarning) {
    console.log(`${testName} passed (with warnings)`)
  } else {
    console.log(`${testName} passed!`)
  }
}


你需要“完全相同的状态”吗?你期望Sibling.toString() === Child.toString()吗?否则,很容易创建一个新函数,它与某些现有函数执行的操作相同。那么Sibling.prototype.someMethod === Child.prototype.someMethod又怎么样呢?你关心它们是否相同吗? - Bergi
好的观点。我认为你需要使用一堆工厂来制造无法复制的东西。 - King Friday
我已经稍微编辑了一下 - 继承类/实例方法和/或它们的副本可能是可以的,只要它们有正确的this上下文。我的主要关注点是原型链,因为我无法控制的事情会假设Sibling直接从Parent继承。 - cincodenada
Object.defineProperties(Object.setPrototypeOf(function Sibling() {}, Object.getPrototypeOf(Child)), {...Object.getOwnPropertyDescriptors(Child), prototype: {value: Object.create(Object.getPrototypeOf(Child.prototype), Object.getOwnPropertyDescriptors(Child.prototype))}}); 通过了你的测试(SCNR,让它成为一行代码 :P) - Bergi
哦,真的吗?好的,那就这样做吧......如果我理解正确,我认为这取决于子类没有自己的构造函数,是吗?因此,你实际上不需要克隆构造函数,因为空函数是等效的。我已经将你的方法包括在我的代码片段中作为“cloneClass()”方法,但你也可以将它发布为独立的答案。 - cincodenada

0

如果您不想使用工厂函数,您可以使用new

const models = {
  User: new ModelMock(),
  Business: new ModelMock()
};

1
ModelMock 用于静态方法和实例方法,这是需要注意的地方。我已经更新了描述以使其更清晰。谢谢。 - King Friday
@JasonSebring 静态方法通常可以通过使用 this.constructor.foo 访问。 - jhpratt

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