🧠 JavaScript 作为一门“基于对象”的语言,其面向对象编程(Object-Oriented Programming, OOP)模型与传统语言如 Java、C++ 等有着本质区别。它并非基于类(Class-based),而是基于原型(Prototype-based)。这一特性赋予了 JavaScript 极大的灵活性,也带来了理解上的挑战。本文将系统梳理 JavaScript 面向对象的核心概念——封装、继承、多态,并深入剖析从原始字面量到 ES6 Class 的演进路径,辅以大量代码示例和底层机制解析。
🔒 封装:隐藏复杂性,暴露接口
什么是封装?
在 OOP 中,封装(Encapsulation) 指的是将数据(属性)和操作数据的方法(行为)捆绑在一起,并对外部隐藏内部实现细节,只通过公开接口进行交互。传统语言通过 public、private、protected 等访问修饰符实现封装。
然而,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.prototypePerson.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 的继承:extends 与 super
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》总结:
- 可读性强:对有 Java/C# 背景的开发者友好
- 避免
new陷阱:直接调用class会报错(而函数构造器不会) - 静态方法支持:
static method() {}直接挂载在类上 - 私有字段支持:
#field语法标准化 - 继承表达清晰:
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》):
- 创建新空对象
{} - 链接
[[Prototype]]到Cat.prototype - 绑定
this到新对象 - 返回新对象(除非构造函数显式返回对象)
⚠️ 若忘记
new,this指向全局对象(浏览器中为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 是糖,原型是根。知其然,更要知其所以然。