一文彻底搞懂 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 原型继承的问题:
- 写法繁琐,语义不清:需要手动处理原型链、修正 constructor 指向,代码冗长且不直观
- 继承逻辑分散:构造函数继承和原型链继承分开写,容易出错
- 没有统一的语法:实例方法、静态方法、继承的写法各不相同
- 没有私有属性:只能通过命名约定(下划线)模拟私有属性,没有真正的封装
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
class 用 extends 关键字实现继承,比 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 是继承中最核心也最容易混淆的关键字,它有两种用法:
- 作为函数调用:在子类构造函数中,
super()表示调用父类的构造函数- 必须在子类构造函数中使用
this之前调用 - 如果子类没有写构造函数,JS 会自动生成一个默认的构造函数,里面会自动调用
super()
- 必须在子类构造函数中使用
- 作为对象调用:在子类方法中,
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 的内置对象,比如 Array、Date、Error 等,这在 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
解决方法:
- 在构造函数中 bind this
class Person { constructor(name) { this.name = name; this.sayHello = this.sayHello.bind(this); } sayHello() { console.log(`我是${this.name}`); } } - 用箭头函数作为类方法(类字段语法)
class Person { constructor(name) { this.name = name; } // 箭头函数作为类方法,this 永远指向实例 sayHello = () => { console.log(`我是${this.name}`); }; } - 调用时用箭头函数包裹
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);
}
}
七、面试高频考点总结
-
class 的本质是什么? class 本质上是函数,是 ES5 原型继承的语法糖。
-
class 和 ES5 构造函数的区别是什么?
- class 必须用 new 调用,普通构造函数可以直接调用
- class 内部默认严格模式
- class 没有提升
- class 的实例方法是不可枚举的
- class 支持 extends 继承,语法更简洁
-
super 关键字的作用是什么?
- 作为函数:在子类构造函数中调用父类构造函数
- 作为对象:在子类方法中指向父类原型,调用父类方法
-
什么是私有属性?怎么实现? 私有属性是只能在类内部访问的属性,ES2022 用
#前缀实现真正的私有属性。 -
类方法的 this 为什么会丢失?怎么解决? 因为类方法作为回调函数被调用时,脱离了原来的上下文。解决方法:bind this、箭头函数类方法、调用时用箭头函数包裹。
-
ES5 原型继承和 class 继承的区别是什么? 本质上没有区别,class 只是语法糖,底层还是原型继承。但 class 语法更简洁、语义更清晰、更不容易出错。
写在最后
class 是现代 JavaScript 面向对象编程的标准写法,它没有改变 JavaScript 基于原型的本质,只是提供了一套更友好、更直观的语法。掌握 class 不仅能让你写出更优雅、更易维护的代码,更是面试中脱颖而出的必备技能。
希望这篇文章能帮你彻底搞懂 class,让你在实际开发和面试中都能游刃有余。
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、关注,有任何问题可以在评论区留言讨论!