ES6:在不使用new关键字的情况下调用类构造函数

116

假设有一个简单的类

class Foo {
  constructor(x) {
    if (!(this instanceof Foo)) return new Foo(x);
    this.x = x;
  }
  hello() {
    return `hello ${this.x}`;
  }
}

在不使用new关键字的情况下调用类构造函数是否可能?

使用应允许

(new Foo("world")).hello(); // "hello world"
或者。
Foo("world").hello();       // "hello world"

但是后者失败了

Cannot call a class as a function

2
记住一个解决办法是定义一个工厂函数(名称稍微有所不同),该函数只需执行 return new Foo(arg); - jfriend00
是的,我考虑过这个问题,但构造函数名称和类名称之间存在不对称的情况 :{ - Mulan
有趣。JS程序员已经习惯了在调用构造函数时省略"new"关键字。这样做可以节省一些打字时间,有时会让代码看起来更加优雅,但也是产生错误和混淆的一个重要原因。未来几年这种做法如何变化将会很有意思。 - user949300
@user949300 我几乎总是使用 new 关键字。不过,我打算将其用于其他用途。 - Mulan
4
@user949300 我已经放弃了JavaScript,转而使用CoffeeScript。ES6和ES7包含许多天才功能,但它的新语法“丑陋至极”令人震惊。而且,新关键字也非常难看。只需将Foo().bar()代码与(new Foo()).bar()进行比较即可。它很糟糕。创建新对象有什么重要性呢?创建对象是日常例行公事,我不需要特殊的语法来完成它。 - user619271
显示剩余2条评论
21个回答

61

类有一个“类体”,它是一个构造函数。如果您使用一个内部的constructor()函数,那个函数也将成为类体,并且在调用类时被调用,因此类总是一个构造函数。

构造函数需要使用new运算符来创建一个新实例,因此如果没有使用new运算符调用类,就会出现错误,因为构造函数必须使用new运算符创建一个新实例。

错误消息也非常具体和正确:

TypeError:不能在没有使用 'new' 的情况下调用类构造函数

您可以:

  • 使用普通函数代替类1
  • 始终使用new调用类。
  • 在包裹的普通函数中调用类,始终使用new,这样您就能获得类的好处,但包裹函数仍然可以使用和不使用new运算符调用2

1)

function Foo(x) {
    if (!(this instanceof Foo)) return new Foo(x);
    this.x = x;
    this.hello = function() {
        return this.x;
    }
}
2)
class Foo {
    constructor(x) {
        this.x = x;
    }
    hello() {
        return `hello ${this.x}`;
    }
}

var _old = Foo;
Foo = function(...args) { return new _old(...args) };

3
在下一个版本中将添加调用构造函数:class Cat { call constructor(){ new Cat() } } - Maxmaxmaximus
@Maxmaxmaximus 我认为你应该把这个作为一个答案发布并加入出处。对我来说这是新闻,非常有趣。 - Tomáš Hübelbauer
1
虽然这个例子可以工作,但如果有人尝试使用 class Bar extends Foo {},它将会出错,因为它不再扩展预期的类。 - trusktr
为什么在 1)this.hello 返回的是 this.x 而不是 hello ${this.x} - Pang

48

正如其他人所指出的,ES2015规范严格规定这样的调用应该抛出TypeError错误,但同时,它提供了一个功能,可以用来实现完全符合要求的结果,即代理

代理允许我们对对象的概念进行虚拟化。例如,它们可以用于更改特定对象的某些行为,而不影响其他任何内容。

在您特定的用例中,class Foo是可以被调用的函数对象,这通常意味着将执行此函数的主体。但是,这可以使用Proxy进行更改:

const _Foo = new Proxy(Foo, {
  // target = Foo
  apply (target, thisArg, argumentsList) {
    return new target(...argumentsList);
  }
});

_Foo("world").hello(); 
const f = _Foo("world");
f instanceof Foo; // true
f instanceof _Foo; // true

(请注意,现在_Foo是您要公开的类,因此标识符应该反过来)

如果在支持代理的浏览器中运行,调用_Foo(...)将执行apply陷阱函数,而不是原始构造函数。

同时,这个“新”的_Foo类与原始的Foo类无法区分(除了能够像普通函数一样调用它)。同样,您无法通过任何方式区分使用Foo_Foo创建的对象。

最大的缺点是它无法被转译或填充, 但它仍然是JS中实现类似Scala类的可行解决方案。


2
这是唯一有效的解决方案。在某些情况下,所有其他答案都无法正常工作。惊讶于 StackOverflow 评级系统的不准确性,唯一正确的答案在列表底部。 - Kos
3
@wandalen - 显然这不是唯一可行的答案,实际上对于这个问题的正确答案只是“不,这是不可能的”。这是一个使用代理而不是通过new创建实例的不同答案,它是解决问题的一个巧妙方法。 - adeneo
1
如果先声明类,那么代理和类就不需要使用不同的名称。class Foo {}; const Foo = new Proxy(Foo, {apply(target, thisArg, args) { return new target(...args) }})。然而,现在Foo引用的是代理而不是原始类。 - jordanbtucker
@Kos,这不仅是错误的问题答案,而且您甚至不需要使用Proxy来执行此答案所做的事情...这不是Proxy的用途... _Foo可以是一个普通的JS function,它可以完全做同样的事情,并且更适合它成为这样。 - Andrew
同时也不提到这个方法没有提供任何方式来输入你自己的"this"... 这正是模拟ES5类继承所需的。 - kungfooman

32

以下是我发现的一种非常有用的模式。它不使用 class,但也不需要使用 new。双赢。

const Foo = x => ({
  x,
  hello: () => `hello ${x}`,
  increment: () => Foo(x + 1),
  add: ({x: y}) => Foo(x + y)
})

console.log(Foo(1).x)                   // 1
console.log(Foo(1).hello())             // hello 1
console.log(Foo(1).increment().hello()) // hello 2
console.log(Foo(1).add(Foo(2)).hello()) // hello 3


13
这值得加分。我真的想知道在JS中添加“class”是否是一项改进。这展示了JS代码应该是什么样子的。对于那些好奇为什么没有使用“this”的人,创建的对象只是使用传递到“构造函数”(箭头函数)中的“x”。每当需要进行改变时,它就会返回一个对象。这些对象是不可变的。 - Stijn de Witt
4
它将创建新的功能。 - Mulan
1
JavaScript 没有接口,我不知道你在说什么。 - Mulan
20
技术的问题在于每次调用Foo时,必须重新创建所有方法。使用类时,“prototype”方法可以在实例之间有效共享,而无需为每个实例重新创建它们。由于方法被重新创建,因此也会占用更多的内存。为了生产目的,最好使用类似Tim答案中提到的方法来创建一个新类。 - Babakness
我添加了一个使用 of 并参考 Fantasy-Land 规范的答案。 - Babakness
显示剩余16条评论

21

我刚为您创建了这个npm模块 ;)

https://www.npmjs.com/package/classy-decorator

import classy from "classy-decorator";
 
@classy()
class IamClassy {
    constructor() {
        console.log("IamClassy Instance!");
    }
}
 
console.log(new IamClassy() instanceof IamClassy());  // true 
 
console.log(IamClassy() instanceof IamClassy());  // true 

5
历史上最被低估的回答之一。 - user13774796
对我来说没问题,但是这是怎么运作的? - undefined

16
不,这是不可能的。使用 class 关键字创建的构造函数只能通过 new 构造,如果它们在没有被调用的情况下被 [[call]]ed,则始终会抛出 TypeError1(甚至无法从外部检测到)。
1:我不确定转译器是否正确处理这一点

然而,您可以使用普通函数作为解决方法:

class Foo {
  constructor(x) {
    this.x = x;
  }
  hello() {
    return `hello ${this.x}`;
  }
}
{
  const _Foo = Foo;
  Foo = function(...args) {
    return new _Foo(...args);
  };
  Foo.prototype = _Foo.prototype;
}

免责声明:instanceof和扩展Foo.prototype的使用方式与通常情况下相同,但是Foo.length不适用,.constructor和静态方法也不适用,但可以通过添加Foo.prototype.constructor = Foo;Object.setPrototypeOf(Foo, _Foo)来解决必要的问题。

如果要用class Bar extends Foo …子类化Foo(而不是_Foo),则应该使用return Reflect.construct(_Foo, args, new.target)代替new _Foo调用。 在ES5样式中进行子类化(使用Foo.call(this, …))是不可能的。


这是我想要的唯一有效的解决方案,因为我需要创建一个动态类继承结构(将混合定义为类而不是函数),所以我需要能够使用子类的原型实例化基类。 - Griffork

11
class MyClass {

  constructor(param) {
     // ...
  }

  static create(param) {
    return new MyClass(param);
  }

  doSomething() {
    // ...
  }

}

MyClass.create('Hello World').doSomething();

那是你想要的吗?

如果在创建 MyClass 的新实例时需要一些逻辑,实现“CreationStrategy”可能会有所帮助,以外包逻辑(例如具有验证的复杂构建器逻辑)

编辑:如评论中所讨论的,使用单独的类创建某种 Builder 模式在 JavaScript 中没有意义。已删除相关示例。


2
提示:只有静态成员的类不应该是“类”,而应该是普通对象。如果只有一个成员,它们甚至根本不应该存在。 - Bergi
1
刚才提到的是您的“Strategy”类,希望您没有主张将“create”作为这些类的实例方法?使用“static”完全没问题。 - Bergi
4
在JavaScript中,如果您需要做一件事情,您只需去做即可。您不需要编写一个类并创建一个实例来完成。那样太过冗余了。只需使用一个简单的函数即可。 - Bergi
1
因为仅仅为了创建一个函数(并将其称为“方法”)而声明一个class 不能组织代码。只需声明该函数即可。不要仅仅因为ES6功能存在或使您的代码看起来像Java而使用它们。 - Bergi
1
在这种特定情况下,由于创建逻辑属于类,我看不出任何外包的理由。只需将其保留在static create方法中即可。 - Bergi
显示剩余5条评论

6

这里介绍一种可以使用的“范围安全构造函数”。 请看以下代码:

function Student(name) {
  if(this instanceof Student) {
    this.name = name;
  } else {
    return new Student(name);
  }
}

现在您可以按照以下方式创建一个学生对象,而无需使用new关键字:
var stud1 = Student('Kia');

4

我在草案中找到了这个。

使用类定义语法定义的构造函数,当作为函数调用时将会抛出异常

因此我猜想在类中不可能这样做。


2
我在扩展使用转换函数转换的类时遇到了问题。问题似乎是因为节点(截至v9.4.0)不支持参数展开运算符((...args) =>)。
这个函数基于 classy-decorator 的转译输出(在另一个答案中提到),对我有效,并且不需要支持装饰器或参数展开运算符。
// function that calls `new` for you on class constructors, simply call
// YourClass = bindNew(YourClass)
function bindNew(Class) {
  function _Class() {
    for (
      var len = arguments.length, rest = Array(len), key = 0;
      key < len;
      key++
    ) {
      rest[key] = arguments[key];
    }

    return new (Function.prototype.bind.apply(Class, [null].concat(rest)))();
  }
  _Class.prototype = Class.prototype;
  return _Class;
}

使用方法:

class X {}
X = bindNew(X);

// or

const Y = bindNew(class Y {});

const x = new X();
const x2 = X(); // woohoo

x instanceof X; // true
x2 instanceof X; // true

class Z extends X {} // works too

作为额外的好处,TypeScript(使用“es5”输出)似乎对旧的instanceof技巧没有问题(嗯,如果没有new使用,它不会进行类型检查,但是无论如何都可以工作):
class X {
  constructor() {
    if (!(this instanceof X)) {
      return new X();
    }
  }
}

因为它将其编译成:
var X = /** @class */ (function () {
    function X() {
        if (!(this instanceof X)) {
            return new X();
        }
    }
    return X;
}());

2
好的,我有另一个答案,在我看来这个答案相当创新。
基本上,与Naomik的答案类似的做法的问题在于,您每次链接方法时都会创建函数。
编辑:此解决方案存在相同问题,但是为了教育目的而保留此答案。
因此,在这里,我提供一种仅将新值绑定到您的方法(基本上只是独立函数)的方法。这还提供了从不同模块导入函数到新构建的对象的额外好处。
好的,下面开始。
const assoc = (prop, value, obj) => 
  Object.assign({},obj,{[prop]: value})

const reducer = ( $values, accumulate, [key,val] ) => assoc( key, val.bind( undefined,...$values ), accumulate )

const bindValuesToMethods = ( $methods, ...$values ) => 
  Object.entries( $methods ).reduce( reducer.bind( undefined, ...$values), {} )

const prepareInstance = (instanceMethods, staticMethods = ({}) ) => Object.assign(
  bindValuesToMethods.bind( undefined, instanceMethods ),
  staticMethods
)

// Let's make our class-like function

const RightInstanceMethods = ({
  chain: (x,f) => f(x),
  map: (x,f) => Right(f(x)),
  fold: (x,l,r) => r(x),
  inspect: (x) => `Right(${x})`
})

const RightStaticMethods = ({
  of: x => Right(x)
})

const Right = prepareInstance(RightInstanceMethods,RightStaticMethods)

现在您可以做到:
Right(4)
  .map(x=>x+1)
  .map(x=>x*2)
  .inspect()

您也可以这样做。
Right.of(4)
  .map(x=>x+1)
  .map(x=>x*2)
  .inspect()

你还有一个额外的好处,即可以从这些模块中导出内容。
export const Right = prepareInstance(RightInstanceMethods,RightStaticMethods)

虽然你无法获得ClassInstance.constructor,但你可以使用FunctorInstance.name(注意,你可能需要使用polyfill来填充Function.name并且不使用箭头函数导出以便与Function.name的浏览器兼容性保持一致)

export function Right(...args){
  return prepareInstance(RightInstanceMethods,RightStaticMethods)(...args)
}

PS - 欢迎为“prepareInstance”提供新的命名建议,有关详细信息,请参见Gist。

https://gist.github.com/babakness/56da19ba85e0eaa43ae5577bc0064456


我认为我看到了一个可修复的问题,但我可能错了。每次我们应用 Right(例如 Right(1)Right(2)),都会调用 Object.entries($methods).reduce 部分。我认为你打算只执行一次这个 reduce。是这样吗? - Mulan
“嗯”确实如此……但从根本上讲,这是有道理的:只需采用map: (x,f) => Right(f(x)),如果x将代表不同的值,则必须重新使用该值重新绑定map。重新绑定会创建一个新函数,因此我们又回到了同样的情况。 - Mulan
我会稍微尝试一下。你的编辑仍然每次构造新值时都调用Object.entries($methods).reduce(。绑定会延迟评估,因此您需要以不同的方式解决这个问题。感谢分享这个有趣的练习。 - Mulan
请分享一个链接到你在实验过程中所发现的任何东西,这样我就可以了解它! - Babakness
让我们在聊天中继续这个讨论 - Mulan
显示剩余3条评论

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