【前端三剑客-30/Lesson50(2025-12-01)】JavaScript 面向对象编程深度全解:从原型到类的完整演化🧠

35 阅读6分钟

🧠 JavaScript 作为一门“基于对象”的语言,其面向对象编程(Object-Oriented Programming, OOP)模型与传统语言如 Java、C++ 等有着本质区别。它并非基于类(Class-based),而是基于原型(Prototype-based)。这一特性赋予了 JavaScript 极大的灵活性,也带来了理解上的挑战。本文将系统梳理 JavaScript 面向对象的核心概念——封装、继承、多态,并深入剖析从原始字面量到 ES6 Class 的演进路径,辅以大量代码示例和底层机制解析。


🔒 封装:隐藏复杂性,暴露接口

什么是封装?

在 OOP 中,封装(Encapsulation) 指的是将数据(属性)和操作数据的方法(行为)捆绑在一起,并对外部隐藏内部实现细节,只通过公开接口进行交互。传统语言通过 publicprivateprotected 等访问修饰符实现封装。

然而,JavaScript 在 ES2022 之前没有原生私有属性支持,但开发者早已发明多种模式来模拟封装。

方式一:闭包实现私有变量(经典构造函数)

function Person(name, age) {
  // 私有变量(闭包作用域)
  const _name = name;
  let _age = age;

  // 公共方法(可访问私有变量)
  this.getName = function() { return _name; };
  this.getAge = function() { return _age; };
  this.setAge = function(newAge) {
    if (typeof newAge === 'number' && newAge > 0) {
      _age = newAge;
    }
  };
}

✅ 优点:真正的私有性
❌ 缺点:每个实例都创建独立方法,浪费内存

方式二:模块模式(IIFE + 闭包)

const CounterModule = (function() {
  let count = 0; // 私有状态

  function validateNumber(n) {
    return typeof n === 'number' && !isNaN(n);
  }

  return {
    increment: () => ++count,
    decrement: () => --count,
    reset: () => count = 0,
    getCount: () => count
  };
})();

这是《JavaScript语言精粹》中推崇的模块模式,是现代模块系统(如 CommonJS、ESM)的前身。

方式三:ES2022 私有字段(标准语法)

class ModernPerson {
  #name;   // 私有字段(必须在类内声明)
  #age;

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

  getName() {
    return this.#name; // 只能在类内部访问
  }

  // 外部无法访问 this.#name
}

🔥 这是目前最推荐的方式:语法清晰、语义明确、引擎优化。


🧬 继承:原型链与委托机制

JavaScript 的继承不是“复制”,而是委托(Delegation) 。对象通过 [[Prototype]] 链向上查找属性,形成原型链(Prototype Chain)

对象的本质

在 JavaScript 中,一切皆对象(除原始类型外,但它们也有包装对象)。对象是“无序属性的集合”,属性可以是:

  • 数据属性(如 name: "张三"
  • 访问器属性(getter/setter)
  • 方法(本质是函数属性)

每个对象都有内部属性:

  • [[Prototype]]:指向其原型(可通过 __proto__Object.getPrototypeOf() 访问)
  • [[Extensible]]:是否可扩展
  • [[Class]]:类型标签(已废弃)

原型继承的基本形式

const animalProto = {
  eat() { return `${this.name} is eating`; }
};

const dog = Object.create(animalProto);
dog.name = 'Rex';
console.log(dog.eat()); // "Rex is eating"

这里 dog.__proto__ === animalProto,实现了对象间委托

构造函数与原型的关系

function Person(name) {
  this.name = name;
}

Person.prototype.sayHello = function() {
  return `Hello, I'm ${this.name}`;
};

const john = new Person('John');

关键关系:

  • john.__proto__ === Person.prototype
  • Person.prototype.constructor === Person
  • 所有实例共享 Person.prototype 上的方法 → 节省内存

继承的几种实现方式

1. 原型链继承(不推荐)

function SuperType() { this.property = true; }
SuperType.prototype.getSuperValue = function() { return this.property; };

function SubType() { this.subproperty = false; }
SubType.prototype = new SuperType(); // 继承
SubType.prototype.getSubValue = function() { return this.subproperty; };

❌ 问题:所有子实例共享父实例的引用属性(如数组),且无法向父构造函数传参。

2. 借用构造函数(经典继承)

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

function SubType(name, age) {
  SuperType.call(this, name); // 借用父构造函数
  this.age = age;
}

✅ 解决了引用属性共享问题
❌ 无法继承父类原型上的方法

3. 组合继承(最常用)

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() { return this.name; };

function SubType(name, age) {
  SuperType.call(this, name); // 继承属性
  this.age = age;
}
SubType.prototype = new SuperType(); // 继承方法
SubType.prototype.constructor = SubType; // 修复 constructor
SubType.prototype.sayAge = function() { return this.age; };

✅ 同时继承属性和方法,引用属性不共享
❌ 调用了两次父构造函数(一次在 new SuperType(),一次在 call

4. 寄生组合继承(最优解)

function inheritPrototype(SubType, SuperType) {
  const prototype = Object.create(SuperType.prototype);
  prototype.constructor = SubType;
  SubType.prototype = prototype;
}

function SuperType(name) { this.name = name; }
SuperType.prototype.sayName = function() { return this.name; };

function SubType(name, age) {
  SuperType.call(this, name);
  this.age = age;
}
inheritPrototype(SubType, SuperType);

✅ 只调用一次父构造函数,完美继承


🧩 ES6 Class:语法糖背后的真相

ES6 引入 class 关键字,使 JavaScript 看起来更像传统 OOP 语言:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return `Hello, I'm ${this.name}`;
  }
}

这仅仅是语法糖!上述代码等价于:

function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() { return `Hello, I'm ${this.name}`; };

Class 的继承:extendssuper

class Employee extends Person {
  constructor(name, position) {
    super(name); // 调用父类构造函数
    this.position = position;
  }
  introduce() {
    return `${super.sayHello()}, I'm a ${this.position}`;
  }
}

⚠️ 注意:super 在构造函数中必须先调用,才能使用 this

为什么大厂倾向使用 Class?

根据《JavaScript语言精粹》和《你不知道的JS》总结:

  1. 可读性强:对有 Java/C# 背景的开发者友好
  2. 避免 new 陷阱:直接调用 class 会报错(而函数构造器不会)
  3. 静态方法支持static method() {} 直接挂载在类上
  4. 私有字段支持#field 语法标准化
  5. 继承表达清晰extends 比手动设置原型链更直观

🦆 多态:鸭子类型与动态分派

JavaScript 没有类型系统,因此多态不是靠接口或抽象类,而是靠鸭子类型(Duck Typing)

“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。”

多态示例

function calculateArea(shape) {
  return shape.getArea(); // 动态分派:运行时决定调用哪个 getArea
}

const circle = {
  radius: 5,
  getArea() { return Math.PI * this.radius ** 2; }
};

const rectangle = {
  width: 10,
  height: 5,
  getArea() { return this.width * this.height; }
};

console.log(calculateArea(circle));     // ~78.54
console.log(calculateArea(rectangle));  // 50

接口约定(非强制)

虽然 JS 没有 interface,但可通过文档或运行时检查模拟:

function processDrawable(drawable) {
  if (typeof drawable.draw !== 'function') {
    throw new Error('Object must implement draw() method');
  }
  drawable.draw();
}

TypeScript 则提供了正式的 interface 支持:

interface Drawable {
  draw(): void;
}
class Circle implements Drawable {
  draw() { console.log("Drawing circle"); }
}

🏗️ 从对象字面量到构造函数:演进之路

阶段一:对象字面量(模板?)

// 1.js 中的例子
var Cat = { name: "", color: "" };
var cat1 = {};
cat1.name = '加菲猫';
cat1.color = '橘色';

❌ 问题:

  • 代码重复(违反 DRY)
  • 实例之间无关联(无原型链)
  • 无法复用方法

阶段二:构造函数(new + this

function Cat(name, color) {
  this.name = name;
  this.color = color;
}
Cat.prototype.meow = function() { console.log('Meow!'); };

const cat1 = new Cat('加菲猫', '橘色');

new 调用时发生四步(《你不知道的JS》):

  1. 创建新空对象 {}
  2. 链接 [[Prototype]]Cat.prototype
  3. 绑定 this 到新对象
  4. 返回新对象(除非构造函数显式返回对象)

⚠️ 若忘记 newthis 指向全局对象(浏览器中为 window),造成污染!

阶段三:ES6 Class(现代化)

class Cat {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }
  meow() { console.log('Meow!'); }
}

更安全、更清晰、支持私有字段、静态方法等。


🛠️ 高级设计模式与最佳实践

工厂模式

const CarFactory = {
  createCar(type) {
    switch(type) {
      case 'sedan': return { type: 'sedan', seats: 5 };
      case 'suv': return { type: 'suv', seats: 7 };
      default: throw new Error('Unknown type');
    }
  }
};

单例模式

const Singleton = (function() {
  let instance;
  function create() { return { data: 'Only one!' }; }
  return {
    getInstance() {
      if (!instance) instance = create();
      return instance;
    }
  };
})();

组合优于继承(Composition over Inheritance)

const canFly = { fly() { console.log('Flying...'); } };
const canSwim = { swim() { console.log('Swimming...'); } };

const duck = Object.assign({}, canFly, canSwim); // 组合能力

✅ 更灵活,避免继承层级过深

委托模式(原型精髓)

const Task = {
  setID(id) { this.id = id; },
  outputID() { console.log(this.id); }
};

const XYZ = Object.create(Task);
XYZ.prepareTask = function(ID, label) {
  this.setID(ID);
  this.label = label;
};

这正是 JavaScript 原型设计的哲学:对象委托,而非类继承


🌐 现代 JavaScript 与未来展望

模块化封装(ESM)

// person.js
const _privateHelper = () => { /* ... */ };

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

函数式 + OOP 结合

class Counter {
  constructor(count = 0) { this.count = count; }
  increment() {
    return new Counter(this.count + 1); // 不变性
  }
}

TypeScript:强类型 OOP

abstract class Animal {
  abstract makeSound(): void;
}

class Dog extends Animal {
  makeSound() { console.log('Woof!'); }
}

📚 总结:掌握 JavaScript OOP 的核心

  • 封装:从闭包到 #private,控制访问边界
  • 继承:理解原型链本质,优先使用 class + extends
  • 多态:依靠鸭子类型和动态分派,无需接口也能灵活
  • 设计哲学委托优于继承,组合优于类层次

正如道格拉斯·克罗克福德所言:“JavaScript 是基于原型的面向对象语言。” 而 Kyle Simpson 在《你不知道的JS》中强调:“理解 this 和原型链,是掌握 JavaScript 的钥匙。”

无论你使用函数构造器还是 ES6 Class,深入理解其底层机制,才能写出健壮、可维护、高性能的代码。在 AI 与全栈开发的时代,这种底层认知力,正是区分普通开发者与专家的关键。

🌟 记住:Class 是糖,原型是根。知其然,更要知其所以然。