如何从方法中创建与当前对象相同 class 的新对象

How to create a new object of same class as current object from within a method

我有一个 Array 的子class,用于内部项目。我添加的一些方法需要 return 一个新数组。我试图弄清楚创建新数组的最佳方法是什么。

我不想硬编码它来创建我的特定子class的数组,因为如果其他人子class是我的class,那就是源数组class,那么它应该创建一个子class的对象。换句话说,我想创建一个与当前 this 对象 class 相同的新对象,无论在我的下面还有多少其他子class。

数组 class 本身已经做了类似的事情。如果您子 class 数组然后在您的子 class 的实例上使用正常的 .map() 函数,它 return 是一个使用您的 class 的新数组.

此 ECMAScript 规范是 .map() 使用的 here for .map() and here for ArraySpeciesCreate(),但我无法弄清楚所有这些规范逻辑在实际现实世界中的含义 Javascript代码。

现在,我正在使用:

let newArray = new this.constructor();

它似乎适用于我自己的小世界,但我想知道 ArraySpeciesCreate() 中的所有逻辑是否应该包含比这更多的代码?

仅供参考,这里是来自 ECMAScript 规范的 ArraySpeciesCreate().map() 应该遵循它来创建新数组 returns。这也是我想遵循的。

在您自己的 class 中,您会使用哪些实际的 Javascript 代码来实现这一点?


这是我的 Array subclass:

中的一个方法示例
// break an array up into chunks
// the last chunk may have fewer items
// returns an array of arrays
chunk(chunkSize) {
    if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
        throw new TypeError('chunkSize must be a positive integer');
    }
    const output = new this.constructor();
    const numChunks = Math.ceil(this.length / chunkSize);
    let index = 0;
    for (let i = 0; i < numChunks; i++) {
        output.push(this.slice(index, index + chunkSize));
        index += chunkSize;
    }
    return output;
}

该方法中的这行代码:

const output = new this.constructor();

是我要问的应该实现 ArraySpeciesCreate() 逻辑的那个。

我仍然认为你不应该 subclass Array,但我可以展示如果在 ECMAScript 中实现 ArraySpeciesCreate 会是什么样子:

if (!Array.isArray(this))               // if not called on an array
    return new Array(len);

const {constructor} = this;
if (typeof constructor == "function"    // if the constructor looks like a constructor,
  && !Function.prototype.isPrototypeOf(constructor) // but appears to stem from a
                                        // different realm (iframe etc)
  && constructor.name == "Array")       // and might be that realm's builtin Array
    return new Array(len);

const C = constructor?.[Symbol.species];
if (C == null)                          // if there's no subclass species
    return new Array(len);

return new C(len);

您可能可以忽略测试跨领域实例的奇怪边缘情况,无论如何它都不能准确工作。 (我怀疑是否有检查这些的好方法,而且似乎无法重现 GetFunctionRealm 步骤——尽管你可能想为 constructor 添加 some check作为本机函数)。

一般来说,它只是在 this.constructor 上访问 Symbol.species,并使用它的结果而不是当前的 class 来构建新实例。

或者,您可以作弊并使用 Array.prototype.slice.call(this, 0, 0) :-)

另一个好的解决方案是来自 es-abstract 库的 the ArraySpeciesCreate function,它试图尽可能精确地实现抽象操作。

感谢 Bergi 解释了 ECMAScript 逻辑在实际 Javascript 代码中的含义。

为了完整起见,我想分享一个通用实用函数,我正在使用它来创建另一个对象,就像我已经拥有的对象一样(即使它是派生的 class 我不知道)不仅仅是特定于数组的。由于任何 subclass 都可能想要使用这种类型的逻辑,这让我们想知道为什么这种逻辑没有内置到 Javascript.

// create a new object using the class of an existing object (which might be a derived class)
// by following the ECMAScript procedure except for the realm detection part

function isConstructor(f) {
    return typeof f === "function" && !!f.prototype;
}

function speciesCreate(originalObject, fallbackConstructor, ...args) {
    const {constructor} = originalObject;
    if (constructor) {
        const C = constructor[Symbol.species];
        if (isConstructor(C)) {
            return new C(...args);
        } else if (isConstructor(constructor)) {
            return new constructor(...args);
        }
    }
    return new fallbackConstructor(...args);
}

因此,在我的 ArrayEx class 中,不是在方法内部使用 this 来创建与当前实例具有相同 class 的新对象:

let newObj = new this.constructor();

我会用这个:

let newObj = speciesCreate(this, ArrayEx);

并且,如果任何特定情况需要,您可以将参数传递给构造函数。


我在这个逻辑中看到的一个问题是,如果派生 class 覆盖 Symbol.species 并将其设置为某个基础 class,但我打算创建一个新对象至少具有我的 class 的功能,派生的 class 会阻止它。我想就是这样。如果派生 class 通过这样做破坏了东西,我想他们会处理破坏的后果。

现在我知道这是一个老问题,但我试了一下,我想我为此编写了一些漂亮的代码。我没有做太多测试,但从外观上看,它可以完成工作。完整的 TypeScript 源代码——包括文档注释和类型化重载签名等。can be found in this gist 作为一个独立的模块。

我的版本的主要区别——除了调用签名和部分应用之外——是它如何对目标执行instanceof检查而不是回退,作为isArray操作的替代在第 3 步和第 4 步中。我认为这有助于使一切更容易预测:目标是回退构造函数的实例吗?如果否,只需使用后备而不是可能返回调用者不准备处理的内容。


运行 节点中的转译代码(包含在末尾)设法使用 @@species 处理内置 类 就好了。从 PromiseRegExp 结果看起来是正确的,所以看起来很通用。

console.log(speciesCreate(Map)(new Map(), [[1, 2], [3, 4]]));
// --> Map(2) { 1 => 2, 3 => 4 }
console.log(speciesCreate(Set, new Set(), [1, 2, 3]));
// --> Set(3) { 1, 2, 3 }
console.log(speciesCreate()(/abc*/g, '123', 'g'));
// --> /123/g
console.log(speciesCreate(Promise)((async () => { })(), (resolve, reject) => setTimeout(resolve, 100)));
// --> Promise { <pending> }
console.log(speciesCreate(Array, [], 10));
// --> [ <10 empty items> ]
console.log(speciesCreate()(Uint8Array.from([]), [1, 2, 3]));
// --> Uint8Array(3) [ 1, 2, 3 ]

该函数包括基本的输入验证和错误消息,并且应该非常准确地重现规范中的行为。它也超载给了3个使用选项:

  1. 当给定 所有参数 时,它会创建一个派生对象。
  2. 当给定 仅后备构造函数 时,它 returns 一个工厂 函数 (target, ...args) => derived 便于重复使用。
  3. 当调用 不带参数时 ,它 returns 另一个工厂函数试图推断要使用的构造函数而不是执行 instanceof 操作。

因此版本 3 没有后备方案,如果无法解决问题,只会 throw。我认为这让一切都变得更紧凑、更可预测。

同样,要更详细地查看它并使用文档注释和类型注释 check out the gist

// # speciesCreate.ts
// ## TypeGuards
const isConstructor = (arg) =>
  typeof arg === 'function' && typeof arg.prototype === 'object';
const isNullish = (arg) =>
  arg === undefined || arg === null;
const isObject = (arg) =>
  typeof arg === 'function' || (typeof arg === 'object' && arg !== null);

// ## Main function
export function speciesCreate(Fallback, ...args) {
    // pre-check if `Fallback` is a constructor if the argument was provided
    if (Fallback !== undefined && !isConstructor(Fallback))
        throw new TypeError('`Fallback` must be a constructor function');
    // Inner core function for automatic partial application.
    function speciesCreate(target, ...args) {
        // if `Fallback` wasn't provided
        if (!Fallback && !isConstructor(Fallback ??= target?.constructor))
            throw new Error('Cannot automatically infer from `target` what fallback to use for `@@species`.');
        if (target instanceof Fallback) {
            let C = target.constructor;
            if (isObject(C))
                C = C[Symbol.species];
            if (isConstructor(C))
                return Reflect.construct(C, args);
            if (!isNullish(C))
                throw new TypeError('Invalid `target` argument for `@@species` use.');
        }
        return new Fallback(...args);
    }
    return args.length ? speciesCreate(...args) : speciesCreate;
}