限制构造函数只能通过 new 调用

三种方法确保构造函数必须使用 new 调用,避免普通函数调用

问题

JavaScript 中的函数可以作为构造函数使用(new Func())或作为普通函数调用(Func()),但语言本身不会区分这两种调用方式。如何限制构造函数只能通过 new 调用?

function Person(firstName, lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

// 使用 new 调用
console.log(new Person("战场", "小包")); // Person {firstName: "战场", lastName: "小包"}

// 普通函数调用 - 不会报错,但行为异常
console.log(Person("战场", "小包")); // undefined

解答

方案一:使用 instanceof

利用 new 调用时 this 指向实例,普通调用时 this 指向 window(非严格模式)或 undefined(严格模式)的特性:

function Person(firstName, lastName) {
    if (!(this instanceof Person)) {
        throw new TypeError('Function constructor Person cannot be invoked without "new"');
    }
    this.firstName = firstName;
    this.lastName = lastName;
    this.fullName = this.firstName + this.lastName;
}

// 普通调用会抛出错误
console.log(Person("战场", "小包")); 
// Uncaught TypeError: Function constructor Person cannot be invoked without "new"

// new 调用正常
console.log(new Person("战场", "小包")); // Person {...}

这种方案存在瑕疵,可以通过 call/apply 伪造实例绕过检测:

console.log(Person.call(new Person(), "战场", "小包")); // 可以执行

方案二:使用 new.target(推荐)

ES6 引入的 new.target 属性专门用于检测函数是否通过 new 调用:

function Person(firstName, lastName) {
    if (!new.target) {
        throw new TypeError('Function constructor Person cannot be invoked without "new"');
    }
    this.firstName = firstName;
    this.lastName = lastName;
}

// 普通调用返回 undefined
console.log("not new:", Person("战场", "小包")); 
// Uncaught TypeError: Function constructor Person cannot be invoked without "new"

// new 调用返回构造函数
console.log("new:", new Person("战场", "小包")); // Person {...}

方案三:使用 ES6 Class(最佳方案)

Class 语法天然限制必须使用 new 调用:

class Person {
    constructor(name) {
        this.name = name;
    }
}

// 普通调用直接报错
Person("小包"); 
// Uncaught TypeError: Class constructor Person cannot be invoked without 'new'

// 必须使用 new
new Person("小包"); // Person {name: "小包"}

扩展:使用 new.target 实现抽象类

在继承场景中,new.target 返回实际调用的构造函数(父类或子类),可以实现抽象类:

class Animal {
    constructor(type, name, age) {
        // 禁止直接实例化 Animal
        if (new.target === Animal) {
            throw new TypeError("Abstract class Animal cannot be instantiated");
        }
        this.type = type;
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animal {
    constructor(name, age) {
        super("dog", name, age);
    }
}

// 抽象类不能实例化
new Animal("dog", "baobao", 18); 
// Uncaught TypeError: Abstract class Animal cannot be instantiated

// 子类可以正常实例化
new Dog("baobao", 18); // Dog {type: "dog", name: "baobao", age: 18}

关键点

  • instanceof 方案利用 this 指向差异,但可被 call/apply 绕过,适用于低版本浏览器
  • new.target 是 ES6 专门为检测构造函数调用方式设计的属性,返回 new 作用的构造函数或 undefined
  • ES6 Class 天然限制必须使用 new 调用,是面向对象编程的最佳方案
  • new.target 在继承中返回子类构造函数,可用于实现抽象类(父类不可实例化,只能通过子类使用)