返回

绕开 JS 陷阱:让构造函数只对 new 低头

前端

在 JavaScript 的函数王国里,存在两种截然不同的角色:普通函数和构造函数。普通函数就如其名,可以随意调用;而构造函数的职责则是创建对象,需要通过 new 来召唤。

然而,蹊跷的是,JavaScript 允许构造函数以普通方式被调用,此时它不会报错,而是返回一个普通对象。这种看似友好的灵活性,却为潜在的 Bug 埋下了伏笔。

举个例子,假设我们有一个名为 Person 的构造函数,用于创建人物对象:

function Person(name) {
  this.name = name;
}

如果不小心以普通方式调用了 Person,比如:

const person = Person("John");

程序并不会报错,但返回的 person 却是一个空对象,因为 this 指向了全局对象,而不是新创建的对象。

这种误用会带来一系列令人头疼的问题:对象属性缺失、方法调用失败,甚至导致程序崩溃。为了避免这种痛苦,我们必须让构造函数只对 new 低头。

方案一:检查 this 的类型

一种简单的方法是检查 this 的类型。构造函数中,this 指向新创建的对象,而普通调用时,this 指向全局对象或调用者的上下文。因此,我们可以添加一个条件判断:

function Person(name) {
  if (!(this instanceof Person)) {
    throw new Error("必须通过 new 调用 Person");
  }
  this.name = name;
}

如果 this 不是 Person 的实例,我们就抛出错误,阻止构造函数的普通调用。

方案二:使用箭头函数

箭头函数没有自己的 this,它会继承外层函数的 this。我们可以利用这一特性,将构造函数包装在一个箭头函数中:

const Person = () => {
  if (!(this instanceof Person)) {
    throw new Error("必须通过 new 调用 Person");
  }
  this.name = name;
};

这样,即使以普通方式调用 Person,也会继承箭头函数的 this,即全局对象,从而触发错误。

方案三:使用 Symbol

Symbol 是 JavaScript 中一种独特的类型,可以作为私有属性或方法的标识符。我们可以利用 Symbol 来创建一个私有属性,仅在构造函数内部可访问:

const PersonSymbol = Symbol();

function Person(name) {
  if (!this[PersonSymbol]) {
    throw new Error("必须通过 new 调用 Person");
  }
  this.name = name;
}

Person.prototype[PersonSymbol] = true;

在构造函数内部,我们检查是否存在 PersonSymbol 属性。如果不存在,则说明是以普通方式调用,此时抛出错误。我们还将 PersonSymbol 作为原型上的一个私有属性,确保只有构造函数本身可以访问。

这三种方案各有千秋,开发者可以根据自己的喜好和代码风格选择适合自己的方法。重要的是,通过这些限制措施,我们能有效防止构造函数的误用,提升代码的鲁棒性和可维护性。