Typescript 导出类不像使用私有构造函数那样有限制吗?
对于我正在编写的API,我希望确保传递的值只能由我自己的特定函数实例化,以确保所述值的有效性。我知道,经过验证的方法是使用私有构造函数和静态工厂方法创建一个类,如下面的示例(假设的情况;实际的类更复杂,并且有方法) 但是为了允许更功能化的方法,我希望工厂函数位于类本身之外(但在同一个模块中),以便能够更自由地组合不同的变体。我提出了以下方法,将构造函数保持为公共的,但通过不导出将其限制为同一模块中的函数:Typescript 导出类不像使用私有构造函数那样有限制吗?,typescript,ecmascript-6,Typescript,Ecmascript 6,对于我正在编写的API,我希望确保传递的值只能由我自己的特定函数实例化,以确保所述值的有效性。我知道,经过验证的方法是使用私有构造函数和静态工厂方法创建一个类,如下面的示例(假设的情况;实际的类更复杂,并且有方法) 但是为了允许更功能化的方法,我希望工厂函数位于类本身之外(但在同一个模块中),以便能够更自由地组合不同的变体。我提出了以下方法,将构造函数保持为公共的,但通过不导出将其限制为同一模块中的函数: class ServiceEndpoint { constructor(
class ServiceEndpoint {
constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {}
}
export function getEndpointByName(name: string): ServiceEndpoint {
// ... polling operation that results in an address
return new ServiceEndpoint(name, address, port);
}
测试这个似乎会产生相同的结果,但我没有看到其他人这样做,所以我有点谨慎。我是否正确地假设这会阻止模块用户自己实例化类,就像私有构造函数那样?我正在监督的这种方法有缺点吗?我对你的问题很感兴趣,我做了一些测试,试图以最好的方式回答这个问题 首先,请记住TypeScript不是JavaScript,它最终将在运行之前进行编译。在声明构造函数之前编写
private
不会对编译后的代码产生特别的影响,换句话说,这样做不会增加一点“安全性”
正如@Bergi在注释中正确指出的那样,通常您可以通过实例构造函数
属性访问构造函数,并可能执行const illagalinstance=new-laginstance.constructor()代码>
通过完全删除构造函数
引用,可以避免最后一种情况。比如:
class-MyClass{
构造函数(){}
}
删除MyClass.prototype.constructor;
导出函数myFactory(){
返回新的MyClass();
}
为了更具体地解决您的问题,在删除构造函数
引用之后,不导出类就足以假定在该模块之外不会创建非法实例。(然而,我决不会依赖内存中的任何内容来实现安全关键场景)
最后,您可以在构造函数中执行一些检查,如果这些检查不通过,则抛出错误。这将防止类被实例化
我没见过其他人这样做
请记住,class
语法只是构造函数的语法糖。最后,唯一重要的是结果对象及其原型的最终结果
啊!!不要担心导出类型{ServiceEndpoint}代码>,同样,这不是JavaScript,将在编译时删除。我可以想到的一种方法是在实例化之前检查模块明智的/privatesecret
,如下所示
// module -- ServiceEndpoint.ts
const secret = Symbol('secret'); // do not export it so that it can be kept "private"
class ServiceEndpoint {
// similar to class decorator ...
static _secret: Symbol | undefined;
constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {
if(ServiceEndpoint._secret !== secret) {
throw 'you can only instantiate this class using factory method'
} else {
// normal instantiation ....
}
}
}
export type { ServiceEndpoint}
export function getEndpointByName(name: string): ServiceEndpoint {
ServiceEndpoint._secret = secret;
// ... polling operation that results in an address
const instance = new ServiceEndpoint(name, '', 0);
ServiceEndpoint._secret = undefined;
return instance;
}
// -----------------------------------------------------------------
// run example.ts
// outside the above module, you can call
getEndpointByName('haha');
// but the following will fail, i.e. throw exception
new ServiceEndpoint('', '', 1);
另外,如果您使用此实例的构造函数,它也会失败。在ES6中,您可以始终执行const someEndpoint=getEndPointByName(…);const myEndpoint=new(someEndpoint.constructor)(…)代码>。但是,如果您将构造函数设置为私有的,我想TypeScript会抱怨。在后一种情况下,类ServiceEndpoint
不会暴露在此模块之外,这就是您想要的吗?我希望ServiceEndpoint
只能作为返回值访问。在这两种情况下,我都可以导出类型{ServiceEndpoint}
要访问声明中使用的类型(我假设:这可能是gotcha的一部分),您不应该通过将机密存储为类上的静态属性来进行通信。最坏的情况,就是这样泄露的。相反,将该秘密作为一个附加构造函数参数传递。我可以通过执行Object.defineProperty(ServiceEndpoint,“\u secret”,{set(value){console.log(secret,“谢谢!”;}})访问该秘密;getEndpointByName('haha')代码>(或其变体)。它在构造函数签名中是否可见并不重要,因为构造函数本身并不公开可见。如果您坚持使用标志进行通信(这是一种脆弱的方法,在任何情况下都不能很好地处理可重入性、回调或异常),至少不要将其作为公共静态属性!您有一个非常好的模块范围。好吧,但即使您不关心安全性,它仍然是一个过于复杂和脆弱的方法。使用一个简单的let callFromFactory=false
boolean标志。如果let callFromFactory
是私有的,谁会关心false
是否为“私有”呢?它将被定义在与您的const secret
相同的范围内。另请参阅,以了解仍然提供用于instanceof
的构造函数的类似方法
// module -- ServiceEndpoint.ts
const secret = Symbol('secret'); // do not export it so that it can be kept "private"
class ServiceEndpoint {
// similar to class decorator ...
static _secret: Symbol | undefined;
constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {
if(ServiceEndpoint._secret !== secret) {
throw 'you can only instantiate this class using factory method'
} else {
// normal instantiation ....
}
}
}
export type { ServiceEndpoint}
export function getEndpointByName(name: string): ServiceEndpoint {
ServiceEndpoint._secret = secret;
// ... polling operation that results in an address
const instance = new ServiceEndpoint(name, '', 0);
ServiceEndpoint._secret = undefined;
return instance;
}
// -----------------------------------------------------------------
// run example.ts
// outside the above module, you can call
getEndpointByName('haha');
// but the following will fail, i.e. throw exception
new ServiceEndpoint('', '', 1);