ES6 —— class

5 阅读7分钟

一文彻底搞懂 JavaScript Class:从语法糖到底层原理,面试再也不怕

class 是 ES6 引入的最重要的特性之一,它为 JavaScript 提供了更接近传统面向对象语言的语法,彻底解决了 ES5 原型继承写法繁琐、语义不清的痛点。虽然 class 本质上只是原型继承的语法糖,但它已经成为现代前端开发的标准写法,也是面试的必考题

本文将从ES5 原型继承的痛点→class 基本语法→继承机制→底层原理→高级特性→常见坑点六个维度,带你彻底搞懂 class,让你不仅会用,更知道它的底层是怎么工作的。


一、为什么我们需要 Class?

在讲 class 之前,我们先回顾一下 ES5 时代是怎么实现面向对象和继承的。你会深刻体会到 class 到底解决了什么问题。

ES5 原型继承的噩梦

// ES5 实现一个 Person 类
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 实例方法:挂载到原型上
Person.prototype.sayHello = function() {
  console.log(`我是${this.name},今年${this.age}岁`);
};

// 静态方法:挂载到构造函数本身
Person.create = function(name, age) {
  return new Person(name, age);
};

// 实现继承:Student 继承 Person
function Student(name, age, grade) {
  // 调用父类构造函数
  Person.call(this, name, age);
  this.grade = grade;
}

// 原型链继承
Student.prototype = Object.create(Person.prototype);
// 修正 constructor 指向
Student.prototype.constructor = Student;

// 子类方法
Student.prototype.study = function() {
  console.log(`${this.name}正在学习`);
};

// 测试
const student = new Student('张三', 18, '高三');
student.sayHello(); // "我是张三,今年18岁"
student.study(); // "张三正在学习"

ES5 原型继承的问题:

  1. 写法繁琐,语义不清:需要手动处理原型链、修正 constructor 指向,代码冗长且不直观
  2. 继承逻辑分散:构造函数继承和原型链继承分开写,容易出错
  3. 没有统一的语法:实例方法、静态方法、继承的写法各不相同
  4. 没有私有属性:只能通过命名约定(下划线)模拟私有属性,没有真正的封装

class 的出现就是为了解决这些问题。它提供了一套统一、清晰、直观的语法,让面向对象编程变得更加简单。


二、Class 基本语法

1. 类的定义

// 类声明
class Person {
  // 构造函数:创建实例时自动调用
  constructor(name, age) {
    // 实例属性:挂载到 this 上
    this.name = name;
    this.age = age;
  }

  // 实例方法:自动挂载到原型上
  sayHello() {
    console.log(`我是${this.name},今年${this.age}岁`);
  }

  // 静态方法:挂载到类本身
  static create(name, age) {
    return new Person(name, age);
  }

  // 访问器属性:get/set
  get info() {
    return `${this.name} - ${this.age}岁`;
  }

  set info(value) {
    const [name, age] = value.split(' - ');
    this.name = name;
    this.age = Number(age);
  }
}

// 创建实例
const person = new Person('张三', 18);
person.sayHello(); // "我是张三,今年18岁"
console.log(person.info); // "张三 - 18岁"

// 调用静态方法
const person2 = Person.create('李四', 20);

对比 ES5 的写法,class 的语法清晰了太多:

  • 所有代码都在一个 class 块中,结构统一
  • 构造函数用 constructor 明确标识
  • 实例方法直接写在类中,自动挂载到原型
  • 静态方法用 static 关键字明确标识
  • 访问器属性用 get/set 关键字

2. 重要注意点

  • 类没有提升:必须先定义类,再创建实例,否则会报错
    const person = new Person(); // ❌ ReferenceError: Cannot access 'Person' before initialization
    class Person {}
    
  • 类内部默认严格模式:不需要手动写 'use strict'
  • 类必须用 new 调用:不能直接调用类,否则会报错
    Person(); // ❌ TypeError: Class constructor Person cannot be invoked without 'new'
    

三、Class 继承:extends 和 super

classextends 关键字实现继承,比 ES5 的原型链继承简单太多。

1. 基本继承

// 父类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`我是${this.name},今年${this.age}岁`);
  }
}

// 子类继承父类
class Student extends Person {
  constructor(name, age, grade) {
    // 调用父类构造函数,必须在使用 this 之前调用
    super(name, age);
    this.grade = grade;
  }

  // 子类自己的方法
  study() {
    console.log(`${this.name}正在${this.grade}学习`);
  }

  // 重写父类方法
  sayHello() {
    // 调用父类的 sayHello 方法
    super.sayHello();
    console.log(`我是一名${this.grade}的学生`);
  }
}

// 测试
const student = new Student('张三', 18, '高三');
student.sayHello();
// "我是张三,今年18岁"
// "我是一名高三的学生"
student.study(); // "张三正在高三学习"

2. super 关键字详解

super 是继承中最核心也最容易混淆的关键字,它有两种用法:

  1. 作为函数调用:在子类构造函数中,super() 表示调用父类的构造函数
    • 必须在子类构造函数中使用 this 之前调用
    • 如果子类没有写构造函数,JS 会自动生成一个默认的构造函数,里面会自动调用 super()
  2. 作为对象调用:在子类方法中,super 表示父类的原型对象
    • 可以通过 super.方法名() 调用父类的方法
    • 注意:通过 super 调用父类方法时,方法内部的 this 仍然指向子类实例
class Parent {
  constructor() {
    this.name = '父类';
  }

  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor() {
    super();
    this.name = '子类';
  }

  test() {
    super.sayName(); // 调用父类的 sayName 方法,但 this 指向子类实例
  }
}

const child = new Child();
child.test(); // "子类" ✅ 不是"父类"

3. 继承内置对象

class 可以直接继承 JavaScript 的内置对象,比如 ArrayDateError 等,这在 ES5 中是很难实现的。

// 继承 Array,实现一个带求和方法的数组
class MyArray extends Array {
  sum() {
    return this.reduce((a, b) => a + b, 0);
  }
}

const arr = new MyArray(1, 2, 3, 4);
console.log(arr.sum()); // 10
console.log(arr instanceof Array); // true

四、Class 的本质:语法糖

重要的事情说三遍:class 本质上就是函数,就是原型继承的语法糖!class 本质上就是函数,就是原型继承的语法糖!class 本质上就是函数,就是原型继承的语法糖!

我们用代码来验证这一点:

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

  sayHello() {}

  static create() {}
}

// 1. class 本质是函数
console.log(typeof Person); // "function" ✅

// 2. Person 就是构造函数本身
console.log(Person === Person.prototype.constructor); // true ✅

// 3. 实例方法挂载在原型上
console.log(Person.prototype.sayHello); // ƒ sayHello() {} ✅

// 4. 静态方法挂载在构造函数本身
console.log(Person.create); // ƒ create() {} ✅

// 5. 实例的 __proto__ 指向类的 prototype
const person = new Person('张三');
console.log(person.__proto__ === Person.prototype); // true ✅

你会发现,class 做的所有事情,ES5 都能做到。它只是给原型继承套了一层更漂亮、更直观的语法外壳。

ES5 与 Class 对应关系表

ES5 写法Class 写法
function Person() {}class Person {}
Person.prototype.sayHello = function() {}class Person { sayHello() {} }
Person.create = function() {}class Person { static create() {} }
Student.prototype = Object.create(Person.prototype)class Student extends Person {}
Person.call(this, name)super(name)

五、Class 高级特性

1. 私有属性(ES2022)

ES2022 正式引入了真正的私有属性,用 # 前缀表示。私有属性只能在类内部访问,外部无法访问和修改。

class Person {
  // 私有属性:用 # 前缀
  #name;
  #age;

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  sayHello() {
    // 类内部可以访问私有属性
    console.log(`我是${this.#name},今年${this.#age}岁`);
  }
}

const person = new Person('张三', 18);
person.sayHello(); // "我是张三,今年18岁"

// ❌ 外部无法访问私有属性
console.log(person.#name); // SyntaxError: Private field '#name' must be declared in an enclosing class

私有属性的特点

  • 必须在类的顶部声明,不能在构造函数中动态添加
  • 只能在类内部访问,外部和子类都无法访问
  • 真正的私有,无法通过任何方式绕过(比如 Object.keys()for...in 都遍历不到)

2. 静态私有属性

和私有属性类似,静态私有属性用 static # 表示,只能在类内部通过类名访问。

class Person {
  static #count = 0;

  constructor() {
    Person.#count++;
  }

  static getCount() {
    return Person.#count;
  }
}

const p1 = new Person();
const p2 = new Person();
console.log(Person.getCount()); // 2

3. 类字段(Class Fields)

类字段允许我们直接在类中定义实例属性,不需要写在构造函数里。

class Person {
  // 类字段:直接定义实例属性
  name = '张三';
  age = 18;

  // 也可以定义私有类字段
  #gender = '男';

  sayHello() {
    console.log(`我是${this.name},今年${this.age}岁`);
  }
}

const person = new Person();
console.log(person.name); // "张三"

4. 类表达式

和函数一样,类也可以写成表达式的形式。

// 匿名类表达式
const Person = class {
  constructor(name) {
    this.name = name;
  }
};

// 命名类表达式
const Person = class PersonClass {
  constructor(name) {
    this.name = name;
  }
};

六、Class 常见坑点与最佳实践

1. 类方法的 this 指向问题

这是最常见的坑:当类方法作为回调函数被调用时,this 会丢失。

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

  sayHello() {
    console.log(`我是${this.name}`);
  }
}

const person = new Person('张三');
const sayHello = person.sayHello;
sayHello(); // ❌ TypeError: Cannot read property 'name' of undefined

解决方法

  1. 在构造函数中 bind this
    class Person {
      constructor(name) {
        this.name = name;
        this.sayHello = this.sayHello.bind(this);
      }
    
      sayHello() {
        console.log(`我是${this.name}`);
      }
    }
    
  2. 用箭头函数作为类方法(类字段语法)
    class Person {
      constructor(name) {
        this.name = name;
      }
    
      // 箭头函数作为类方法,this 永远指向实例
      sayHello = () => {
        console.log(`我是${this.name}`);
      };
    }
    
  3. 调用时用箭头函数包裹
    setTimeout(() => person.sayHello(), 1000);
    

2. 不要在构造函数中定义方法

// ❌ 错误:每个实例都会创建一个新的方法,浪费内存
class Person {
  constructor(name) {
    this.name = name;
    this.sayHello = function() {
      console.log(`我是${this.name}`);
    };
  }
}

// ✅ 正确:方法定义在原型上,所有实例共享
class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log(`我是${this.name}`);
  }
}

3. 继承时不要忘记调用 super()

在子类构造函数中,必须在使用 this 之前调用 super(),否则会报错。

class Student extends Person {
  constructor(name, age, grade) {
    this.grade = grade; // ❌ ReferenceError: Must call super constructor in derived class before accessing 'this'
    super(name, age);
  }
}

七、面试高频考点总结

  1. class 的本质是什么? class 本质上是函数,是 ES5 原型继承的语法糖。

  2. class 和 ES5 构造函数的区别是什么?

    • class 必须用 new 调用,普通构造函数可以直接调用
    • class 内部默认严格模式
    • class 没有提升
    • class 的实例方法是不可枚举的
    • class 支持 extends 继承,语法更简洁
  3. super 关键字的作用是什么?

    • 作为函数:在子类构造函数中调用父类构造函数
    • 作为对象:在子类方法中指向父类原型,调用父类方法
  4. 什么是私有属性?怎么实现? 私有属性是只能在类内部访问的属性,ES2022 用 # 前缀实现真正的私有属性。

  5. 类方法的 this 为什么会丢失?怎么解决? 因为类方法作为回调函数被调用时,脱离了原来的上下文。解决方法:bind this、箭头函数类方法、调用时用箭头函数包裹。

  6. ES5 原型继承和 class 继承的区别是什么? 本质上没有区别,class 只是语法糖,底层还是原型继承。但 class 语法更简洁、语义更清晰、更不容易出错。


写在最后

class 是现代 JavaScript 面向对象编程的标准写法,它没有改变 JavaScript 基于原型的本质,只是提供了一套更友好、更直观的语法。掌握 class 不仅能让你写出更优雅、更易维护的代码,更是面试中脱颖而出的必备技能。

希望这篇文章能帮你彻底搞懂 class,让你在实际开发和面试中都能游刃有余。

如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!