在TypeScript中,一个类是否可以不使用"new"关键字而被使用?

11

TypeScript是ES6 Javascript的一个超集,它包含类型。使用 class 关键字声明类,并使用 new 关键字实例化类,就像在Java中一样。

我想知道在TypeScript中是否有任何情况下可以不使用 new 关键字实例化类。

我之所以问这个问题,是因为我想知道,假设我有一个名为 Bob 的类,我能否假定任何 Bob 的实例都是用 new Bob() 实例化的。

2个回答

14
Typescript默认会防止这种情况发生,因此如果您这样做:
class A {}
let a = A();

您会收到一个错误:

类型 typeof A 的值不可调用。您是否意味着包括 'new'?

但是,有一些对象可以在不使用 new 关键字的情况下创建,基本上所有的原生类型都可以。
如果您查看 lib.d.ts,可以看到不同构造函数的签名,例如:

StringConstructor

interface StringConstructor {
    new (value?: any): String;
    (value?: any): string;
    ...
}

ArrayConstructor

interface ArrayConstructor {
    new (arrayLength?: number): any[];
    new <T>(arrayLength: number): T[];
    new <T>(...items: T[]): T[];
    (arrayLength?: number): any[];
    <T>(arrayLength: number): T[];
    <T>(...items: T[]): T[];
    ...
}

您可以看到,始终存在带有和不带有new关键字的相同构造函数。
如果您愿意,当然可以模仿这种行为。

重要的是要理解,尽管 TypeScript 会检查以确保不会发生这种情况,但 JavaScript 不会检查,因此如果有人编写将使用您的代码的 js 代码,他可能会忘记使用new,因此仍然存在这种情况。

在运行时很容易检测是否发生了这种情况,然后根据需要处理它(抛出错误、使用new返回实例并记录)。
这里有一篇讨论它的文章:创建实例而不使用 new(纯 js),但简而言之:

class A {
    constructor() {
        if (!(this instanceof A)) {
            // throw new Error("A was instantiated without using the 'new' keyword");
            // console.log("A was instantiated without using the 'new' keyword");

            return new A();
        }
    }
}

let a1 = new A(); // A {}
let a2 = (A as any)(); // A {}

(code in playground)


编辑

据我所知,编译器无法理解可以在不使用new关键字的情况下调用A,除非将其转换为any
我们可以做得比将其强制转换为any更好:

interface AConstructor {
    new(): A;
    (): A;
}

let a2 = (A as AConstructor)(); // A {}

我们无法像在 lib.d.ts 中的 Array 那样进行技巧操作的原因是:

interface Array<T> {
    ...
}

interface ArrayConstructor {
    ...
}

declare const Array: ArrayConstructor;

这里他们一次将 Array 用作类型,一次用作值,但是类既是类型又是值,因此尝试使用此技巧将以以下方式结束:

重复标识符“A”


3
听起来你是在说简短的回答是“是”。 - Vivian River
的确,简短的回答是:是的,可以在不使用 new 的情况下调用构造函数(但这种情况下你将没有实例)。 - Nitzan Tomer
现在,真正的问题是:如何使其与A()一起工作,而不是(A as any)() - rsp
@rsp 是的,没错。当编译后的 JavaScript 使用类时,运行时环境将不允许这种情况发生。 - Nitzan Tomer
嗯,关于如何在JavaScript库的类型中处理Object及其相关内容似乎非常有用... - SamB
显示剩余3条评论

7

这是一个相当棘手的问题,但可以通过多种方式解决,具体取决于您希望它与内置Array构造函数的行为有多接近。

问题#1-构造函数不能在没有“new”的情况下调用

这不仅适用于TypeScript,也适用于JavaScript。

使用class关键字创建的构造函数在JavaScript中无法在没有new的情况下调用:

> class A {}
undefined
> new A()
A {}
> A()
TypeError: Class constructor A cannot be invoked without 'new'

就像箭头函数不能使用 new 关键字调用一样:

> B = () => {}
[Function: B]
> B()
undefined
> new B()
TypeError: B is not a constructor

只有使用function关键字创建的函数才可以通过new关键字调用:

> function C() {}
undefined
> C()
undefined
> new C()
C {}

有趣的是,如果将箭头函数和class关键字构造函数都转换为ES6以下的JS,则上面所有的A(), B()和C()函数都可以正常工作,无论是否带有new,因为它们都会被转换为旧式函数,并使用function关键字正常工作,即使在当前引擎上也是如此。
问题#2-构造函数在没有new的情况下无法获得正确的“this”
一旦您克服了调用构造函数时出现的错误问题,您需要确保构造函数实际上获得了一个新对象。
在JavaScript(以及TypeScript)中,new关键字创建一个新对象并将其绑定到构造函数中的this,并返回该对象。因此,如果您有一个函数:
function f() {
  console.log(this);
}

如果你以new f()的形式调用它,它将打印一个空对象并返回它。

如果你不使用newf()的形式调用它,则会打印全局对象(在浏览器中为window,在Node中为global或在Web Worker中为self - 有关更多信息,请查看我在npm上的模块[the-global-object](https://www.npmjs.com/package/the-global-object)),并且它将返回undefined

问题#3-静态类型定义很棘手

这个问题是特定于TypeScript的。您需要确保所有类型按预期工作,并且它们以有用的方式工作。将所有内容声明为any很容易,但是这样您将失去编辑器中的所有提示,并且TypeScript编译器将无法在编译期间检测到类型错误。

问题#4-很容易制作不起作用的解决方案

这个问题再次不特定于TypeScript而是一般适用于JavaScript。 您希望所有内容都按预期工作-使用旧式函数和显式原型进行继承以及使用classextends关键字进行继承等。

换句话说,该对象应与使用class声明并使用new实例化的其他对象一样工作,没有花哨的东西。

我的经验法则:如果您可以使用内置函数(例如Array)执行某些操作(既可以带有new也可以不带),那么您也应该使用我们的构造函数来执行此操作。

问题#5-很容易制作具有不同元数据的解决方案

再次适用于JavaScript。 您想要的不仅是在调用A()而不使用new时按预期工作的对象,而且您实际上希望x instanceof A按预期工作,您希望console.log()在打印对象时写入正确的名称等。

这可能对每个人都不是一个问题,但需要考虑。

问题#6-很容易制作具有老派function的解决方案

它应支持class语法,而不是回到function构造函数和原型,否则您将失去许多有用的TypeScript功能。

问题#7-某些解决方案仅在转换为ES5或更早版本时才起作用

这与上面的问题#6有关-如果转换目标是ES6之前的版本,则结果将使用旧式function构造函数,这不会产生错误:

TypeError: Class constructor A cannot be invoked without 'new'

(见上面的问题#1)。
这可能对你有问题,也可能没有。如果你已经为旧版引擎进行转译,那么你就不会遇到这个问题,但是当你更改转译目标(例如,避免使用async/await填充等高运行时成本)时,你的代码将会出现问题。如果它是一个库代码,则不会适用于所有人。如果仅供您自己使用,则至少要记住这一点。

解决方案

以下是我在一段时间前思考时想出的一些解决方案。我并不完全满意它们,我想避免代理,但这些是目前我找到的唯一解决所有上述问题的解决方案之一。

解决方案#1

我的第一次尝试之一(有关更一般的类型,请参见后面的示例):
type cA = () => A;

function nonew<X extends Function>(c: X): AI {
  return (new Proxy(c, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as any as AI);
}

interface A {
  x: number;
  a(): number;
}

const A = nonew(
  class A implements A {
    x: number;
    constructor() {
      this.x = 0;
    }
    a() {
      return this.x += 1;
    }
  }
);

interface AI {
  new (): A;
  (): A;
}

const B = nonew(
  class B extends A {
    a() {
      return this.x += 2;
    }
  }
);

其中一个缺点是,虽然构造函数名称正确并且可以正常打印,但是 constructor 属性本身指向作为参数传递给 nonew()函数的原始构造函数,而不是指向函数返回的内容(这可能是个问题,具体取决于您如何看待它)。

另一个缺点是需要声明接口以公开类型。

解决方案#2

另一种解决方案:

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

class $A {
  x: number;
  constructor() {
    this.x = 0;
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
const A: MC<A> = nn($A);
Object.defineProperty(A, 'name', { value: 'A' });

class $B extends $A {
  a() {
    return this.x += 2;
  }
}
type B = $B;
const B: MC<B> = nn($B);
Object.defineProperty(B, 'name', { value: 'B' });

这里不需要在冗余的接口中重复类型定义,而是可以使用带有$前缀的原始构造函数。

这里还可以使用继承和instanceof,构造函数名称和打印都可以正常工作,但constructor属性指向带有$前缀的构造函数。

解决方案 #3

另一种方法:

type NC<X> = { new (): X };
type FC<X> = { (): X };
type MC<X> = NC<X> & FC<X>;
function nn<X>(C: NC<X>): MC<X> {
  return new Proxy(C, {
    apply: (t, _, a) => new (<any>t)(...a)
  }) as MC<X>;
}

type $c = { $c: Function };

class $A {
  static $c = A;
  x: number;
  constructor() {
    this.x = 10;
    Object.defineProperty(this, 'constructor', { value: (this.constructor as any as $c).$c || this.constructor });
  }
  a() {
    return this.x += 1;
  }
}
type A = $A;
var A: MC<A> = nn($A);
$A.$c = A;
Object.defineProperty(A, 'name', { value: 'A' });

class $B extends $A {
  static $c = B;
  a() {
    return this.x += 2;
  }
}
type B = $B;
var B: MC<B> = nn($B);
$B.$c = B;
Object.defineProperty(B, 'name', { value: 'B' });

这种解决方案使实例的constructor属性指向公开的构造函数(而不是以$为前缀的构造函数),但使constructor属性返回true,从而可以使用hasOwnProperty() - 但对于propertyIsEnumerable()返回false,因此这应该不是问题。

更多解决方案

我将所有尝试以及一些更多的解释都放在了 GitHub 上:

我并不完全满意其中任何一种,但它们都能够完成任务。

另请参阅我的回答:在没有new的情况下调用TypeScript类的构造函数


在我看来,所有这些解决方案都过于复杂了。我认为只需要有一个 AContrcutor 类型,然后执行 const a = (A asAContrcutor)() 即可。你们所有的解决方案都试图避免这种方式,但最终变得过于冗长,不仅增加了太多的 ts 代码,而且还会导致更多冗余的运行时(js)代码。 - Nitzan Tomer
@NitzanTomer 我理解问题中要求不使用new关键字,那么其他关键字也应该不引入。否则我们可以更容易地为每个类添加一个静态工厂函数,称为例如create并使用a = A.create()而不是a = new A(),但我认为a = new A()没有new应该实际上是a = A(),仅此而已。也许这不是OP所想的,但到目前为止还没有接受的答案,所以我们还不知道。我的自己的需求在github上描述(在答案中)如果你有一个想法来完成它没有代理让我知道。 - rsp
你的所有代码都添加了一些不必要的js代码。你为一些本来并不重要的东西增加了太多复杂性。如果你提供一个sdk,只需隐藏类并仅公开一个工厂函数,那么你就不需要所有这些复杂性。你想避免复杂性,在你的代码中(以便其他开发人员能够轻松理解它)和删除不需要的运行时代码。 - Nitzan Tomer
@NitzanTomer 对你来说可能不重要,但对我来说很重要。我想要一个可以替代Array和其他内置类的插件。为此,a = A()必须等同于a = new A()instanceof也必须正常工作。a = A()等同于a = new A()是一个硬性要求。如果您有简化它的想法,那么我非常乐意听取您的意见,就像我在https://github.com/rsp/ts-no-new上所写的一样,但是告诉我“拥有一个可替换的Array并不是很重要”令人失望。我已经解决了这个问题,如果您有更好的解决方案,请让我知道。 - rsp
@NitzanTomer 关于复杂性的解决方案基本上是:const nonew = c => new Proxy(X, { apply(t, i, a) { return new t(...a); } }); 其他的都是打字和确保返回的元数据正确,但如果您不关心元数据或者您不介意使用 any 类型,那么它就非常简单。至于增加的运行时成本,我不会在没有进行基准测试的情况下抱怨。 - rsp
显示剩余4条评论

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