JavaScript 虽然常常被贴上"函数式语言"的标签,但它从诞生之初就具备完整的面向对象能力。从早期的构造函数 + 原型链,到 ES6 的 class 语法糖,再到私有字段、静态块、装饰器提案,JavaScript 的面向对象体系一直在演进。很多人写了几年 JS,却依然说不清 new 到底做了什么、prototype 和 __proto__ 有什么区别、this 为什么会丢失。本文将从零开始,系统梳理 JavaScript 面向对象编程的完整知识体系——对象创建、原型机制、类与继承、封装与多态、this 绑定、混入与组合、常用设计模式,最后给出一个完整的实战案例。45分钟沉浸式学习,帮你彻底打通 JS 面向对象的任督二脉。
什么是面向对象编程
面向对象编程(Object-Oriented Programming,OOP)是一种以"对象"为核心的编程范式。它把数据(属性)和操作数据的逻辑(方法)封装在一起,通过对象之间的协作来完成程序功能。OOP 的四大核心支柱是:封装、继承、多态、抽象。
const userName = '张玥';
const userAge = 28;
function greet(name, age) {
return `我是${name},今年${age}岁`;
}
/* 面向对象:数据与行为封装在对象中 */
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `我是${this.name},今年${this.age}岁`;
}
}
const me = new User('张玥', 28);
me.greet(); // "我是张玥,今年28岁"
对象 = 属性(状态) + 方法(行为)
创建对象的多种方式
JavaScript 提供了多种创建对象的手段,理解它们的差异是掌握 OOP 的第一步。从最简单的字面量,到工厂函数,再到构造函数,每一种都有其适用场景。
const user1 = {
name: '张玥',
age: 28,
greet() { return `Hi, ${this.name}`; }
};
/* 方式二:工厂函数(返回对象) */
function createUser(name, age) {
return {
name,
age,
greet() { return `Hi, ${name}`; }
};
}
/* 方式三:构造函数(配合 new) */
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.greet = function() {
return `Hi, ${this.name}`;
};
/* 方式四:Object.create(原型继承) */
const proto = { greet() { return 'Hi'; } };
const user2 = Object.create(proto);
user2.name = '张玥';
new 运算符到底做了什么
new 是 JavaScript 中最容易被"会用但不懂"的运算符。它背后其实只有四步操作,理解这四步,你就理解了实例化的本质。
/* 1. 创建一个新的空对象 */
/* 2. 把该对象的原型指向构造函数的 prototype */
/* 3. 将构造函数内的 this 绑定到该对象并执行 */
/* 4. 若构造函数返回对象则返回它,否则返回新对象 */
/* 手动实现一个 myNew */
function myNew(Constructor, ...args) {
// 第 1、2 步
const obj = Object.create(Constructor.prototype);
// 第 3 步
const result = Constructor.apply(obj, args);
// 第 4 步
return (result !== null && typeof result === 'object') ? result : obj;
}
/* 验证 */
function Person(name) { this.name = name; }
const p = myNew(Person, '张玥');
p.name; // "张玥"
p instanceof Person; // true
{}
obj.__proto__ = Fn.prototype
若构造函数返回基本类型,会被忽略
原型与原型链
原型是 JavaScript 实现继承的底层机制。每个函数都有一个 prototype 属性,每个对象都有一个内部属性 [[Prototype]](可通过 __proto__ 或 Object.getPrototypeOf 访问)。属性查找会沿着这条链一路向上,直到 null。
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} 发出了声音`;
};
const cat = new Animal('喵喵');
/* 原型链验证 */
cat.__proto__ === Animal.prototype; // true
Animal.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
/* 属性查找顺序 */
cat.speak(); // 自身没有 → 原型上有 → 调用
cat.toString(); // 一路查到 Object.prototype
/* 判断属性来源 */
cat.hasOwnProperty('name'); // true(自有属性)
cat.hasOwnProperty('speak'); // false(继承属性)
'speak' in cat; // true(含原型链)
属性查找:自身 → 原型 → 原型的原型 → null
prototype 与 __proto__ 的区别
这是面试中的高频问题,也是初学者最容易混淆的两个概念。记住一句话:prototype 是函数才有的属性,__proto__ 是所有对象都有的属性。
const f = new Foo();
/* prototype:函数专有,指向"实例的原型" */
typeof Foo.prototype; // "object"
Foo.prototype.constructor === Foo; // true
/* __proto__:所有对象都有,指向自己的原型 */
f.__proto__ === Foo.prototype; // true
Foo.__proto__ === Function.prototype; // true
/* 推荐使用标准 API(__proto__ 已不推荐) */
Object.getPrototypeOf(f) === Foo.prototype; // true
Object.setPrototypeOf(f, null); // 改变原型
/* 创建一个"干净"的对象,不带 Object.prototype */
const dict = Object.create(null);
f.__proto__ === Foo.prototype
ES6 class:更优雅的语法糖
ES6 引入了 class 关键字,让面向对象的写法更接近传统语言。但要注意,它本质上仍是基于原型的语法糖,并没有引入新的继承模型。
// 实例字段(可在构造器中初始化)
species = 'Homo sapiens';
// 私有字段
#secret = '私密数据';
// 构造函数
constructor(name, age) {
this.name = name;
this.age = age;
}
// 原型方法(不可枚举)
greet() {
return `你好,我是 ${this.name}`;
}
// 静态方法(挂在类本身,而非原型)
static create(name, age) {
return new Person(name, age);
}
// 访问器
get info() { return `${this.name} / ${this.age}`; }
set info(v) { [this.name, this.age] = v.split('/'); }
}
const p = Person.create('张玥', 28);
p.greet(); // "你好,我是 张玥"
p.info; // "张玥 / 28"
class 必须使用 new 调用,否则抛错
继承的多种实现方式
JavaScript 的继承经历了多个阶段:原型链继承、构造函数继承、组合继承、寄生组合继承,最终由 class extends 统一。理解这些方案的演进,能让你真正明白 extends 背后的逻辑。
function Child() {}
Child.prototype = new Parent();
// 缺点:父类引用属性被所有实例共享
/* 2. 构造函数继承:在子类中调用父类 */
function Child(name) {
Parent.call(this, name);
}
// 缺点:无法继承父类原型上的方法
/* 3. 组合继承:两者结合(最常用) */
function Child(name) {
Parent.call(this, name);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
/* 4. ES6 class extends(最终方案) */
class Child extends Parent {
constructor(name, extra) {
super(name); // 必须先调用 super
this.extra = extra;
}
method() {
return super.method() + ' 扩展';
}
}
继承方案的演进:从手动到语义化
super 关键字的两种用法
super 在类中有两种完全不同的用法:作为函数调用(在构造器中)和作为对象访问(在方法中)。理解它们的差异可以避免很多坑。
constructor(name) {
this.name = name;
}
speak() { return `${this.name} 发出声音`; }
static type() { return '动物'; }
}
class Dog extends Animal {
constructor(name, breed) {
// 用法一:super() 调用父类构造函数
super(name);
this.breed = breed;
}
speak() {
// 用法二:super.method() 调用父类方法
return super.speak() + ',汪汪!';
}
static type() {
return super.type() + '(犬科)';
}
}
const d = new Dog('旺财', '柴犬');
d.speak(); // "旺财 发出声音,汪汪!"
Dog.type(); // "动物(犬科)"
Parent.call(this, ...args),只能在构造器中使用,且必须在使用 this 之前调用。
继承中不调用 super() 会抛 ReferenceError
封装:从闭包到私有字段
封装是 OOP 的第一原则——隐藏内部实现,只暴露必要接口。JavaScript 实现封装的方案经历了闭包、WeakMap、Symbol,最终在 ES2022 迎来了真正的私有字段 #。
function Counter() {
let count = 0; // 外部无法访问
this.increment = () => ++count;
this.getCount = () => count;
}
/* 方案二:Symbol 作为"伪私有"键 */
const _balance = Symbol('balance');
class Account {
constructor(v) { this[_balance] = v; }
getBalance() { return this[_balance]; }
}
/* 方案三:ES2022 私有字段(推荐) */
class BankAccount {
#balance = 0;
constructor(initial) {
if (initial < 0) throw new Error('初始金额不能为负');
this.#balance = initial;
}
deposit(amount) {
if (amount <= 0) throw new Error('金额必须大于 0');
this.#balance += amount;
return this;
}
get balance() { return this.#balance; }
}
const acc = new BankAccount(100);
acc.deposit(50).deposit(30);
acc.balance; // 180
acc.#balance; // SyntaxError:外部不可访问
封装 = 数据校验 + 接口收敛 + 实现隐藏
多态与鸭子类型
多态意味着"同一个接口,不同的实现"。JavaScript 作为动态类型语言,天然支持鸭子类型(Duck Typing)——只要对象有对应的方法,就可以被当作某种类型使用。
area() {
throw new Error('子类必须实现 area()');
}
}
class Circle extends Shape {
constructor(r) { super(); this.r = r; }
area() { return (Math.PI * this.r ** 2).toFixed(2); }
}
class Rect extends Shape {
constructor(w, h) { super(); this.w = w; this.h = h; }
area() { return (this.w * this.h).toFixed(2); }
}
/* 统一接口,不同实现 */
const shapes = [new Circle(3), new Rect(4, 5)];
shapes.forEach(s => console.log(s.area()));
/* 鸭子类型:不关心类型,只关心能力 */
function printArea(shape) {
if (typeof shape.area !== 'function') {
throw new TypeError('需要 area() 方法');
}
return shape.area();
}
shape.area() 时:"走起来像鸭子,叫起来像鸭子,那就是鸭子"
this 指向完全指南
this 是 JavaScript 中最令人困惑的关键字之一。它的值在函数被调用时确定,遵循四条绑定规则:默认绑定、隐式绑定、显式绑定、new 绑定。
function fn() { console.log(this); }
fn(); // 严格模式下是 undefined
/* 2. 隐式绑定:谁调用,this 就指向谁 */
const obj = { name: '张玥', say() { console.log(this.name); } };
obj.say(); // "张玥"
/* 隐式丢失:赋值后变成默认绑定 */
const fn2 = obj.say;
fn2(); // undefined(this 丢失)
/* 3. 显式绑定:call / apply / bind */
fn.call({ name: '李四' }); // 立即执行
const bound = fn.bind({ name: '王五' }); // 永久绑定
/* 4. new 绑定:this 指向新创建的实例 */
function Foo() { this.x = 1; }
new Foo().x; // 1
/* 箭头函数:没有自己的 this,继承外层 */
const counter = {
count: 0,
start() {
setInterval(() => {
this.count++; // this 指向 counter
}, 1000);
}
};
优先级:new > 显式 > 隐式 > 默认
静态成员、访问器与迭代器
除了普通方法,类还支持静态成员、getter/setter、生成器和迭代器协议,这些特性让类的表达能力大幅增强。
// 静态属性
static unit = '℃';
static #instances = 0;
constructor(celsius) {
this.#celsius = celsius;
Temperature.#instances++;
}
// 访问器:像属性一样读写
get fahrenheit() {
return this.#celsius * 1.8 + 32;
}
set fahrenheit(f) {
this.#celsius = (f - 32) / 1.8;
}
// 静态方法
static count() { return Temperature.#instances; }
// 迭代器:让实例可被 for...of 遍历
*[Symbol.iterator]() {
yield this.#celsius;
yield this.fahrenheit;
}
}
const t = new Temperature(25);
t.fahrenheit; // 77
t.fahrenheit = 86; // 反向设置
[...t]; // [30, 86]
这些特性组合起来,让类具备完整的抽象能力
混入(Mixin)与"组合优于继承"
JavaScript 只支持单继承,当一个类需要复用多个来源的能力时,混入(Mixin)是常见的解决方案。但在现代前端工程中,"组合优于继承"已成为更被推崇的设计原则。
const Serializable = {
serialize() { return JSON.stringify(this); }
};
const Loggable = {
log() { console.log(`[LOG] ${this.constructor.name}`); }
};
class User {}
Object.assign(User.prototype, Serializable, Loggable);
const u = new User();
u.log(); // "[LOG] User"
/* 组合:把能力作为属性注入 */
class Logger {
log(msg) { console.log(`[${new Date().toISOString()}] ${msg}`); }
}
class OrderService {
constructor(logger) {
this.logger = logger; // 依赖注入
}
create(order) {
this.logger.log('创建订单');
return { ...order, id: Date.now() };
}
}
const service = new OrderService(new Logger());
面向对象常用设计模式
设计模式是面向对象经验的结晶。在 JavaScript 中,单例、工厂、观察者、策略这四种模式出现频率最高,也最实用。
class Config {
static #instance;
constructor() {
if (Config.#instance) return Config.#instance;
Config.#instance = this;
this.env = 'production';
}
}
/* 2. 工厂模式 */
class ButtonFactory {
static create(type) {
const map = {
primary: PrimaryButton,
ghost: GhostButton,
};
const Cls = map[type] || PrimaryButton;
return new Cls();
}
}
/* 3. 观察者模式(事件总线) */
class EventBus {
#listeners = new Map();
on(event, handler) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
return () => this.off(event, handler);
}
off(event, handler) {
this.#listeners.get(event)?.delete(handler);
}
emit(event, ...args) {
this.#listeners.get(event)?.forEach(h => h(...args));
}
}
/* 4. 策略模式 */
class PriceContext {
constructor(strategy) { this.strategy = strategy; }
calc(price) { return this.strategy.calc(price); }
}
const vip = { calc: p => p * 0.8 };
new PriceContext(vip).calc(100); // 80
模式不是目的,解决耦合才是
完整实战:任务管理器
下面把前面学到的知识整合起来,实现一个具备封装、继承、多态、观察者模式的任务管理器。
class Task {
#done = false;
constructor(title) {
if (new.target === Task) {
throw new Error('Task 是抽象类,不能直接实例化');
}
this.title = title;
this.createdAt = new Date();
}
get done() { return this.#done; }
complete() {
this.#done = true;
return this;
}
// 抽象方法:子类必须实现
describe() {
throw new Error('子类必须实现 describe()');
}
}
/* 具体任务:普通任务 */
class SimpleTask extends Task {
describe() {
return `📌 ${this.title}`;
}
}
/* 具体任务:带截止日期 */
class DeadlineTask extends Task {
constructor(title, deadline) {
super(title);
this.deadline = new Date(deadline);
}
describe() {
const days = Math.ceil(
(this.deadline - Date.now()) / 86400000
);
return `⏰ ${this.title}(剩余 ${days} 天)`;
}
}
/* 管理器:组合 + 观察者 */
class TaskManager {
#tasks = [];
#listeners = new Set();
add(task) {
if (!(task instanceof Task)) {
throw new TypeError('只能添加 Task 实例');
}
this.#tasks.push(task);
this.#notify();
return this;
}
complete(title) {
const t = this.#tasks.find(x => x.title === title);
t?.complete();
this.#notify();
return this;
}
onChange(fn) {
this.#listeners.add(fn);
return () => this.#listeners.delete(fn);
}
#notify() {
this.#listeners.forEach(fn => fn(this.report()));
}
// 多态:不同任务输出不同描述
report() {
return this.#tasks.map(t => ({
text: t.describe(),
done: t.done,
}));
}
}
/* 使用 */
const manager = new TaskManager();
manager.onChange(list => console.table(list));
manager
.add(new SimpleTask('阅读 MDN 文档'))
.add(new DeadlineTask('完成项目评审', '2026-10-01'))
.complete('阅读 MDN 文档');
#done · complete() · describe()
describe → 📌
describe → ⏰
#tasks · #listeners · onChange()
封装 + 继承 + 多态 + 组合 + 观察者,一次到位
常见陷阱与避坑指南
面向对象编程中有一些高频错误,提前了解可以省下大量调试时间。
class Timer {
constructor() { this.n = 0; }
start() {
// ❌ this 指向 setInterval 的调用者
setInterval(function() { this.n++; }, 1000);
// ✅ 箭头函数继承外层 this
setInterval(() => { this.n++; }, 1000);
}
}
/* 陷阱 2:类字段箭头函数 vs 原型方法 */
class A {
// 每个实例都会创建一份,占用更多内存
handle = () => { console.log(this); };
// 挂在原型上,所有实例共享
normal() { console.log(this); }
}
/* 陷阱 3:循环中创建函数引用同一个变量 */
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3 3 3
}
// 用 let 或闭包解决
/* 陷阱 4:直接修改原型带来的全局污染 */
// ❌ 永远不要修改内建对象的原型
// Array.prototype.myMethod = function() {};
面向对象最佳实践与总结
- 优先使用 class 语法:语义清晰,工具链支持更好,底层仍是原型
- 组合优于继承:继承层级不要超过三层,能用组合就用组合
- 优先使用私有字段:用
#保护内部状态,只暴露必要接口 - 方法定义在原型上:避免每个实例都创建一份函数,节省内存
- 明确 this 的绑定:回调中优先使用箭头函数,必要时用 bind
- 不要修改内建原型:会造成全局污染,破坏可维护性
- 善用静态成员:与实例无关的工具方法、工厂方法应设为 static
- 单一职责:一个类只做一件事,类名应当准确描述它的职责
- 面向接口编程:调用方依赖能力(方法),而非具体类型
- 写单元测试:面向对象的可测试性正是它的优势之一
/* 1. 这个类有明确的单一职责吗? */
/* 2. 内部状态是否被妥善封装? */
/* 3. 继承关系是 is-a 还是只是为了复用代码? */
/* 4. 是否可以通过组合降低耦合? */
/* 5. 子类是否真正实现了父类的契约? */
/* 一个符合最佳实践的类模板 */
class WellDesignedClass {
// 1. 私有状态
#state = new Map();
// 2. 明确的构造入口
constructor(options = {}) {
this.#init(options);
}
// 3. 私有方法
#init(options) {
Object.entries(options).forEach(([k, v]) => {
this.#state.set(k, v);
});
}
// 4. 公开只读访问
get size() { return this.#state.size; }
// 5. 静态工厂
static from(obj) {
return new WellDesignedClass(obj);
}
}