Skip to content
imshengli blog
Go back

ES6 Class:语法糖背后的构造函数与原型

ES6 Class 语法全面解析:constructor、静态方法、getter/setter、私有字段,以及与构造函数的对比。

· 6 min

简介

生成实例的传统方法:通过构造函数; ES6 引入了class,作为”对象的模板”;通过class定义类。 基本上,ES6 的class可以看作只是一个语法糖。

class 声明

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return `(${this.x}, ${this.y})`;
  }
}

typeof Point; // "function"
Point === Point.prototype.constructor; // true
Point.name; // "Point"

// 必须通过 new 调用,否则报错(与普通构造函数的区别)
const p = new Point(1, 2);

class 表达式

// 命名表达式:Me 只在 class 内部可用
const MyClass = class Me {
  getClassName() { return Me.name; }
};
new MyClass().getClassName(); // "Me"
// Me.name // ReferenceError

// 匿名表达式(更常用)
const MyClass2 = class { /* ... */ };

// 立即执行
const obj = new (class {
  constructor(name) { this.name = name; }
})('test');
obj.name; // "test"

class 不存在变量提升

new Foo(); // ReferenceError
class Foo {}

// 原因与继承有关,必须保证子类在父类之后定义
{
  let Foo = class {};
  class Bar extends Foo {}
}

constructor

构造方法,创建实例时自动调用。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
// 不写 constructor,默认生成空的 constructor() {}

// 返回其他对象的情况
class Odd {
  constructor() { return Object.create(null); }
}
new Odd() instanceof Odd; // false

实例方法

方法定义在 prototype 上,所有实例共享。

class Calculator {
  constructor(value = 0) { this.value = value; }
  add(n) { this.value += n; return this; }
  subtract(n) { this.value -= n; return this; }
  getResult() { return this.value; }
}

const calc = new Calculator(10);
calc.add(5).subtract(3).getResult(); // 12

// 方法在 prototype 上
calc.hasOwnProperty('add'); // false
calc.__proto__.hasOwnProperty('add'); // true

// 类内部定义的方法不可枚举(与 ES5 不同)
Object.keys(Calculator.prototype); // []
Object.getOwnPropertyNames(Calculator.prototype);
// ["constructor", "add", "subtract", "getResult"]

一次添加多个方法:Object.assign(Point.prototype, { method1() {}, method2() {} })

静态方法

static 定义的方法属于类本身,不被实例继承。

class MathUtil {
  static add(a, b) { return a + b; }
  static isPositive(n) { return n > 0; }
}
MathUtil.add(1, 2); // 3
// new MathUtil().add(1, 2); // TypeError

// 静态方法中 this 指向类本身
class Foo {
  static create() { return new this(); }
  static bar() { return this.baz(); }
  static baz() { return 'baz'; }
}

实例属性和静态属性

class Counter {
  // 实例属性(类体中直接声明)
  count = 0;
  // 静态属性
  static total = 0;

  constructor() { Counter.total++; }
  increment() { this.count++; }
}

const c1 = new Counter();
const c2 = new Counter();
Counter.total; // 2
c1.count; // 0(各实例独立)

getter 和 setter

使用 get/set 拦截属性的存取行为。

class Temperature {
  constructor(celsius) { this._celsius = celsius; }

  get fahrenheit() { return this._celsius * 1.8 + 32; }
  set fahrenheit(value) { this._celsius = (value - 32) / 1.8; }

  get celsius() { return this._celsius; }
  set celsius(value) {
    if (value < -273.15) throw new Error('Below absolute zero');
    this._celsius = value;
  }
}

const temp = new Temperature(100);
temp.fahrenheit; // 212
temp.fahrenheit = 32;
temp.celsius; // 0

getter/setter 定义在属性的 Descriptor 对象上:

var desc = Object.getOwnPropertyDescriptor(Temperature.prototype, 'fahrenheit');
'get' in desc; // true
'set' in desc; // true

私有字段和方法(# 语法)

ES2022 正式支持 # 前缀定义私有成员,只能在类内部访问。

class BankAccount {
  #balance = 0;

  constructor(owner, balance) {
    this.owner = owner;
    this.#balance = balance;
  }

  #validate(amount) {
    if (amount <= 0) throw new Error('Amount must be positive');
  }

  deposit(amount) { this.#validate(amount); this.#balance += amount; }
  withdraw(amount) {
    this.#validate(amount);
    if (amount > this.#balance) throw new Error('Insufficient funds');
    this.#balance -= amount;
  }
  get balance() { return this.#balance; }
}

const account = new BankAccount('Alice', 1000);
account.deposit(500);
account.balance; // 1500
// account.#balance; // SyntaxError

# 语法之前的私有化方案:

// 1. 命名约定(仅是约定,外部仍可访问)
class Foo { _private() {} }

// 2. Symbol 唯一性
const _method = Symbol('method');
class Baz { [_method]() { return 'private'; } }

this 指向问题

类方法中 this 默认指向实例,但单独提取方法时会丢失。

class Logger {
  constructor(prefix) { this.prefix = prefix; }
  log(msg) { console.log(`[${this.prefix}] ${msg}`); }
}
const { log } = new Logger('App');
// log('test'); // TypeError: Cannot read property 'prefix' of undefined

// 解决方案:constructor 中 bind,或使用箭头函数属性
// this.log = this.log.bind(this);
// log = (msg) => { console.log(`[${this.prefix}] ${msg}`); }

new.target

返回 new 命令作用于的构造函数,可实现抽象类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('Shape 不能直接实例化');
    }
  }
}

class Circle extends Shape {
  constructor(r) { super(); this.radius = r; }
}

// new Shape(); // Error
new Circle(5); // OK

与传统构造函数的对比

// 传统构造函数
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return this.name + ' speaks'; };
Animal.staticMethod = function() { return 'static'; };

// ES6 class
class Animal2 {
  constructor(name) { this.name = name; }
  speak() { return this.name + ' speaks'; }
  static staticMethod() { return 'static'; }
}
特性构造函数class
变量提升
必须 new 调用
方法可枚举
严格模式需手动声明自动
私有字段# 语法

Generator 方法

方法前加星号使其成为 Generator 函数。

class Range {
  constructor(start, end) { this.start = start; this.end = end; }
  * [Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) yield i;
  }
}

[...new Range(1, 5)]; // [1, 2, 3, 4, 5]

Share this post on:

Previous Post
ES6 Object:解构赋值、扩展运算符与可选链
Next Post
ES6 Class 继承:extends、super 与原型链对比