JS面向对象之从prototype到class再到私有属性详解
作者:止观止
前言
这一篇将探讨 JavaScript 中的面向对象编程(OOP)。对于从 Java、C# 转过来的开发者来说,以前 JS 的原型继承机制往往让人摸不着头脑。ES6 引入的 class 关键字极大地降低了心智负担,虽然它本质上是“语法糖”,但它让代码结构更加清晰、标准。
1. 引言
在 ES6(ES2015)之前,JavaScript 想要实现“类”和“继承”是非常痛苦的。因为 JS 本质是基于原型的,而不是基于类的。如果你在网上搜“JS 继承五种写法”,你会发现组合继承、寄生组合继承等让人头秃的代码:
// ES5 之前的“恐怖”继承写法
function Parent(name) { this.name = name; }
Parent.prototype.say = function() { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name); // 借用构造函数
this.age = age;
}
Child.prototype = new Parent(); // 原型链继承
Child.prototype.constructor = Child; // 修正 constructor 指向
这不怪开发者,怪语言设计太底层。
ES6 引入了 class 关键字,终于给了我们一个**“看起来像样”的面向对象语法。虽然它底层依然是原型继承,但它把复杂的细节封装了起来。到了 ES2022,甚至支持了真正的私有属性**。
本文将带你掌握:
- 为什么
class只是语法糖,但必须得用? - 如何优雅地使用
extends和super实现继承? - 如何使用
#定义真正的私有变量。
2. 正文
2.1 ES5 的“原型”写法(快速回顾)
在深入 class 之前,只需记住一点:ES5 的类本质是一个构造函数。
function Animal(name) {
this.name = name;
}
// 方法挂载在原型上,所有实例共享
Animal.prototype.speak = function() {
console.log(this.name + " makes a noise.");
};
这种写法把属性(构造函数内)和方法(原型上)分开了,对于 Java/C++ 程序员来说非常不直观。
2.2 ES6class—— 更加优雅的语法糖
ES6 的 class 可以看作是一个语法糖,它让对象原型的写法更加清晰、更像面向对象编程的语法。
1. 定义类与构造函数
class Animal {
// 构造方法
constructor(name) {
this.name = name;
}
// 实例方法(等同于定义在 prototype 上)
speak() {
console.log(`${this.name} makes a noise.`);
}
}
const a = new Animal("Dog");
a.speak(); // "Dog makes a noise."
注意:类的方法之间不需要逗号分隔,加了逗号会报错!
2. 静态方法
使用 static 关键字定义的方法属于类本身,而不是实例。这非常适合工具类方法。
class Animal {
constructor(name) { this.name = name; }
// 静态方法
static compare(a, b) {
return a.name === b.name;
}
}
const dog1 = new Animal("Dog");
const dog2 = new Animal("Dog");
// 正确:通过类调用
console.log(Animal.compare(dog1, dog2)); // true
// 错误:实例无法调用
// dog1.compare(); // TypeError

2.3 继承与super的正确姿势
ES6 使用 extends 关键字实现继承,比 ES5 的原型链篡改要简单得多。
class Dog extends Animal {
constructor(name, breed) {
// 必须先调用 super(),否则会报错!
// super 代表父类的构造函数
super(name);
this.breed = breed;
}
speak() {
// 调用父类的方法
super.speak();
console.log(`${this.name} barks.`);
}
}
const d = new Dog("Rex", "Bulldog");
d.speak();
// 输出:
// "Rex makes a noise."
// "Rex barks."
核心坑点:
在子类的 constructor 中,必须在使用 this 之前调用 super()。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象。
2.4 私有属性与新特性
在很长一段时间里,JS 没有真正的私有属性(private)。大家只能靠命名约定(比如 _name)来表示“请勿访问”,但这是防君子不防小人的。
1. ES2022:私有字段
现在,我们可以在属性名前加 #(井号)来定义真正的私有属性。带 # 的属性是类作用域私有的,外部无法访问。
class BankAccount {
#balance = 0; // 私有属性
constructor(initial) {
this.#balance = initial;
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const myAccount = new BankAccount(100);
myAccount.deposit(50);
console.log(myAccount.getBalance()); // 150
// console.log(myAccount.#balance); // 报错!SyntaxError: Private field '#balance' must be declared in an enclosing class

2. new.target
用于检测函数或构造函数是否是通过 new 关键字调用的。它可以用来限制类必须被实例化,或者防止直接调用抽象类。
class Parent {
constructor() {
if (new.target === Parent) {
throw new Error("本类不能直接实例化");
}
}
}
class Child extends Parent {
constructor() {
super(); // 正常
}
}
new Child(); // ✅
// new Parent(); // ❌ 抛出错误

3. 常见问题 (FAQ)
Q1:class 是一个新的数据类型吗?
A: 不是。typeof Dog 返回的依然是 "function"。class 本质上就是把函数包装了一下,让你觉得它是一个“类”。你可以把 class 看作是 function 和 prototype 的语法糖。
Q2:为什么在子类 constructor 里必须先写 super()?
A: 这是 JS 的继承机制决定的。子类实例的构建是基于父类实例加工的,不先调用父类的构造函数(生成父类 this),子类就没有 this 可用。
Q3:什么时候该用继承?
A: 虽然语法好看了,但设计原则依然建议 “多用组合,少用继承”。如果你只是想复用一些工具方法,用普通对象或 Mixin 可能比继承更灵活。只有在明确的“is-a”关系(如“狗是动物”)时才使用继承。
4. 总结
JavaScript 的面向对象语法终于进化到了主流水平:
class:提供了标准的类定义写法,虽然底层是原型,但代码可读性大幅提升。extends&super:让继承变得简单直观,不再需要手动操作prototype链。#私有属性:ES2022 提供了真正的访问控制,封装性不再是靠自觉。
最佳实践建议:
在开发 React 组件、Vue 类组件(Vue 2/3)或 Node.js 服务端类时,优先使用
class语法。对于敏感数据(如内部状态、API 密钥),务必使用#私有属性进行保护。
到此这篇关于JS面向对象之从prototype到class再到私有属性的文章就介绍到这了,更多相关JS prototype到class再到私有属性内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
