如何从方法中创建与当前对象相同 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
处理内置 类 就好了。从 Promise
到 RegExp
结果看起来是正确的,所以看起来很通用。
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个使用选项:
- 当给定 所有参数 时,它会创建一个派生对象。
- 当给定 仅后备构造函数 时,它 returns 一个工厂
函数
(target, ...args) => derived
便于重复使用。
- 当调用 不带参数时 ,它 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;
}
我有一个 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
处理内置 类 就好了。从 Promise
到 RegExp
结果看起来是正确的,所以看起来很通用。
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个使用选项:
- 当给定 所有参数 时,它会创建一个派生对象。
- 当给定 仅后备构造函数 时,它 returns 一个工厂
函数
(target, ...args) => derived
便于重复使用。 - 当调用 不带参数时 ,它 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;
}