面向对象编程

张玥 2026年9月16日 阅读时间 45分钟
JavaScript 面向对象 原型链 ES6 Class 设计模式
JavaScript基础之面向对象编程

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
构造函数
方法挂在 prototype,实例共享
Object.create
精确控制原型,最底层的写法

new 运算符到底做了什么

new 是 JavaScript 中最容易被"会用但不懂"的运算符。它背后其实只有四步操作,理解这四步,你就理解了实例化的本质。

/* new 的四步操作: */
/* 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
1 创建空对象 {}
2 链接原型 obj.__proto__ = Fn.prototype
3 绑定 this 并执行构造函数体
4 返回对象(引用类型才生效)

若构造函数返回基本类型,会被忽略

原型与原型链

原型是 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(含原型链)
cat 实例 { name: '喵喵' }
Animal.prototype { speak }
Object.prototype { toString, hasOwnProperty... }
null(原型链终点)

属性查找:自身 → 原型 → 原型的原型 → null

prototype 与 __proto__ 的区别

这是面试中的高频问题,也是初学者最容易混淆的两个概念。记住一句话:prototype 是函数才有的属性,__proto__ 是所有对象都有的属性。

function Foo() {}
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);
prototype
函数属性
Foo.prototype
__proto__
对象属性
f.__proto__
f.__proto__ === Foo.prototype
实例的原型 === 构造函数的 prototype

ES6 class:更优雅的语法糖

ES6 引入了 class 关键字,让面向对象的写法更接近传统语言。但要注意,它本质上仍是基于原型的语法糖,并没有引入新的继承模型。

class Person {
  // 实例字段(可在构造器中初始化)
  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 内部结构
constructor() → 初始化实例
greet() → 挂在 Person.prototype
static create() → 挂在 Person 本身
#secret → 真正的私有字段
get / set → 访问器属性

class 必须使用 new 调用,否则抛错

继承的多种实现方式

JavaScript 的继承经历了多个阶段:原型链继承、构造函数继承、组合继承、寄生组合继承,最终由 class extends 统一。理解这些方案的演进,能让你真正明白 extends 背后的逻辑。

/* 1. 原型链继承:子类原型指向父类实例 */
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() + ' 扩展';
  }
}
原型链继承
共享引用属性 ❌
构造函数继承
拿不到原型方法 ❌
组合继承
功能完整,但调用了两次父类 ⚠️
class extends
语法清晰,语义明确 ✅

继承方案的演进:从手动到语义化

super 关键字的两种用法

super 在类中有两种完全不同的用法:作为函数调用(在构造器中)和作为对象访问(在方法中)。理解它们的差异可以避免很多坑。

class Animal {
  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(); // "动物(犬科)"
super(...args)
作为函数调用,等价于 Parent.call(this, ...args),只能在构造器中使用,且必须在使用 this 之前调用。
super.method()
作为对象访问,指向父类的原型(普通方法)或父类本身(静态方法),可用于复用父类逻辑。

继承中不调用 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:外部不可访问
BankAccount 类
#balance —— 私有字段,外部不可见
deposit() —— 公开方法,可校验入参
get balance —— 只读访问器

封装 = 数据校验 + 接口收敛 + 实现隐藏

多态与鸭子类型

多态意味着"同一个接口,不同的实现"。JavaScript 作为动态类型语言,天然支持鸭子类型(Duck Typing)——只要对象有对应的方法,就可以被当作某种类型使用。

class Shape {
  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() 时:
Circle.area() → 28.27
Rect.area() → 20.00
Triangle.area() → 12.50

"走起来像鸭子,叫起来像鸭子,那就是鸭子"

this 指向完全指南

this 是 JavaScript 中最令人困惑的关键字之一。它的值在函数被调用时确定,遵循四条绑定规则:默认绑定、隐式绑定、显式绑定、new 绑定。

/* 1. 默认绑定:独立调用 → window/undefined */
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);
  }
};
默认 window / undefined
隐式 obj.fn() → obj
显式 call / apply / bind → 指定对象
new new Fn() → 新实例
箭头 继承外层词法作用域

优先级:new > 显式 > 隐式 > 默认

静态成员、访问器与迭代器

除了普通方法,类还支持静态成员、getter/setter、生成器和迭代器协议,这些特性让类的表达能力大幅增强。

class Temperature {
  // 静态属性
  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]
static
属于类,不属于实例
get / set
计算属性,读写拦截
Symbol.iterator
支持 for...of 与展开
#private
真正的外部不可见

这些特性组合起来,让类具备完整的抽象能力

混入(Mixin)与"组合优于继承"

JavaScript 只支持单继承,当一个类需要复用多个来源的能力时,混入(Mixin)是常见的解决方案。但在现代前端工程中,"组合优于继承"已成为更被推崇的设计原则。

/* 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());
继承:is-a 关系
Dog 是一个 Animal —— 层级深、耦合强
混入:能力复制
User 拥有 serialize / log 能力
组合:has-a 关系
OrderService 持有一个 Logger —— 灵活、可替换

面向对象常用设计模式

设计模式是面向对象经验的结晶。在 JavaScript 中,单例、工厂、观察者、策略这四种模式出现频率最高,也最实用。

/* 1. 单例模式 */
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 文档');
类之间的关系
Task(抽象基类)
#done · complete() · describe()
SimpleTask
describe → 📌
DeadlineTask
describe → ⏰
TaskManager(组合 + 观察者)
#tasks · #listeners · onChange()

封装 + 继承 + 多态 + 组合 + 观察者,一次到位

常见陷阱与避坑指南

面向对象编程中有一些高频错误,提前了解可以省下大量调试时间。

/* 陷阱 1:方法中 this 丢失 */
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() {};
this 丢失
用箭头函数或 bind 固定
内存浪费
方法挂原型,别挂实例
循环闭包
用 let 或 IIFE
原型污染
别改内建对象原型

面向对象最佳实践与总结

  • 优先使用 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);
  }
}