构造函数

Baseline Widely available

This feature is well established and works across many devices and browser versions. It’s been available across browsers since March 2017.

constructor 是一种用于创建和初始化 class 对象实例的特殊方法。

备注: 本页介绍 constructor 语法。关于所有对象的 constructor 属性,请参见 Object.prototype.constructor

尝试一下

语法

js
constructor() { /* … */ }
constructor(argument0) { /* … */ }
constructor(argument0, argument1) { /* … */ }
constructor(argument0, argument1, /* …, */ argumentN) { /* … */ }

还有一些额外的语法限制:

  • 名为 constructor 的类方法不能是 gettersetterasyncgenerator
  • 一个类不能有一个以上的 constructor 方法。

描述

通过构造函数,你可以在调用实例化对象的其他方法之前,提供必须完成的自定义初始化。

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

  introduce() {
    console.log(`你好,我的名字是 ${this.name}`);
  }
}

const otto = new Person("Otto");

otto.introduce(); // 你好,我的名字是 Otto

如果不指定构造函数,则使用默认的构造函数。如果你的类是基类,默认构造函数会是空的:

js
constructor() {}

如果你的类是派生类,默认构造函数会调用父构造函数,并传递所提供的任何参数:

js
constructor(...args) {
  super(...args);
}

备注: 像上面这样的显式构造函数与默认构造函数的区别在于,后者实际上并不通过参数展开来调用数组迭代器

这样代码才能正常工作:

js
class ValidationError extends Error {
  printCustomerMessage() {
    return `验证失败 :-((详细信息:${this.message})`;
  }
}

try {
  throw new ValidationError("非有效电话号码");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.name); // 这是 Error,而不是 ValidationError!
    console.log(error.printCustomerMessage());
  } else {
    console.log("未知错误", error);
    throw error;
  }
}

ValidationError 类不需要显式构造函数,因为它不需要进行任何自定义初始化。默认构造函数会根据给定的参数初始化父类 Error

但是,如果你提供了自己的构造函数,而你的类派生自某个父类,那么你必须使用 super() 显式地调用父类的构造函数。例如:

js
class ValidationError extends Error {
  constructor(message) {
    super(message); // 调用父类构造函数
    this.name = "ValidationError";
    this.code = "42";
  }

  printCustomerMessage() {
    return `发生未知错误 :-((详细信息:${this.message},错误代码:${this.code})`;
  }
}

try {
  throw new ValidationError("非有效手机号码");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(error.name); // 现在这是 ValidationError!
    console.log(error.printCustomerMessage());
  } else {
    console.log("未知错误", error);
    throw error;
  }
}

在类中使用 new,需要经过以下步骤:

  1. (如果是派生类)super() 调用之前的 constructor 主体。这部分不应访问 this,因为它尚未初始化。
  2. (如果是派生类)执行 super() 调用,通过同样的过程初始化父类。
  3. 当前类的字段将被初始化。
  4. 执行调用 super() 后的 constructor 主体(如果是基类,则对整个主体)。

constructor 主体中,你可以通过 this 访问正在创建的对象,并通过 new.target 访问用 new 调用的类。请注意,在执行 constructor 之前,方法(包括 gettersetter)和原型链已在 this 上初始化,因此你甚至可以从父类的构造函数访问子类的方法。但是,如果这些方法使用了 this,这时 this 尚未完全初始化。这意味着读取派生类的公共字段的结果是 undefined,而读取私有字段将导致 TypeError

js
new (class C extends class B {
  constructor() {
    console.log(this.foo());
  }
} {
  #a = 1;
  foo() {
    return this.#a; //TypeError: Cannot read private member #a from an object whose class did not declare it
    // 这并不是因为 class 没有声明它,
    // 而是由于私有字段在父类构造函数运行的时候尚未初始化
  }
})();

constructor 方法可能有返回值。基类可以在构造函数中返回任何值,而派生类必须返回一个对象、 undefined 值,或抛出 TypeError

js
class ParentClass {
  constructor() {
    return 1;
  }
}

console.log(new ParentClass()); // ParentClass {}
// 因为返回值不是一个对象,所以它会被忽略
// 这与函数构造函数一致

class ChildClass extends ParentClass {
  constructor() {
    return 1;
  }
}

console.log(new ChildClass()); TypeError: Derived constructors may only return object or undefined

如果父类构造函数返回一个对象,则该对象将被用作 this 值,派生类的类字段将在该值上定义。这种技巧被称为“返回重载”,它允许在无关对象上定义派生类的字段(包括私有字段)。

constructor 遵循正常的方法语法,因此参数默认值剩余参数等都可以使用。

js
class Person {
  constructor(name = "Anonymous") {
    this.name = name;
  }
  introduce() {
    console.log(`你好,我的名字是 ${this.name}`);
  }
}

const person = new Person();
person.introduce(); // 你好,我的名字是 Anonymous

构造函数必须是一个明确的值。计算属性不能成为构造函数。

js
class Foo {
  // 这是一个计算属性。它不会作为构造函数被拾取。
  ["constructor"]() {
    console.log("被调用");
    this.a = 1;
  }
}

const foo = new Foo(); // 无日志
console.log(foo); // Foo {}
foo.constructor(); // 记录“被调用”
console.log(foo); // Foo { a: 1 }

禁止将异步方法、生成器方法、访问器和类字段称为 constructor。私有名称不能被命名为 #constructor。任何名为 constructor 的成员都必须是普通方法。

示例

使用constructor方法

以下代码片段来自 类的实例在线 demo)。

js
class Square extends Polygon {
  constructor(length) {
    // 在这里,它调用了父类的构造函数,并将 lengths 提供给 Polygon 的"width"和"height"
    super(length, length);
    // 注意:在派生类中,必须先调用 super() 才能使用 "this"。
    // 忽略这个,将会导致一个引用错误。
    this.name = "Square";
  }
  get area() {
    return this.height * this.width;
  }
  set area(value) {
    this.height = value ** 0.5;
    this.width = value ** 0.5;
  }
}

在绑定了不同原型的构造函数中调用 super

super() 调用当前类原型的构造函数。如果更改了当前类的原型,super() 将调用新原型的构造函数。更改当前类的 prototype 属性的原型不会影响 super() 调用哪个构造函数。

js
class Polygon {
  constructor() {
    this.name = "Polygon";
  }
}

class Rectangle {
  constructor() {
    this.name = "Rectangle";
  }
}

class Square extends Polygon {
  constructor() {
    super();
  }
}

// 让 Square 扩展 Rectangle(这是一个基类),而不是 Polygon
Object.setPrototypeOf(Square, Rectangle);

const newInstance = new Square();

// newInstance 仍然是 Polygon 的实例,因为我们没有
// 没有改变 Square.prototype 的原型,所以 newInstance 的
// 原型链仍然是
//   newInstance --> Square.prototype --> Polygon.prototype
console.log(newInstance instanceof Polygon); // true
console.log(newInstance instanceof Rectangle); // false

// 然而,由于 super() 调用 Rectangle 作为构造函数,
// newInstance 的 name 属性将按照 Rectangle 中的逻辑进行初始化
console.log(newInstance.name); // Rectangle

规范

Specification
ECMAScript Language Specification
# sec-static-semantics-constructormethod

浏览器兼容性

BCD tables only load in the browser

参阅