封装、继承与多态
面向对象编程(OOP)的三大特性——封装、继承、多态——在 JavaScript 中有独特的实现方式。本文将系统梳理 JS 中的 OOP 实践,从闭包封装到原型链继承,再到鸭子类型多态。
学习目标
读完本文,你将学会:
- 4 种 JavaScript 封装技术的实现与对比
- 6 种继承方式的原理、优缺点与适用场景
- 多态与鸭子类型在 JS 中的实践
- "组合优于继承"的设计思想
一、封装(Encapsulation)
1.1 什么是封装
封装是将数据(属性)和操作数据的方法绑定在一起,并隐藏内部实现细节,只暴露有限的公共接口。
封装的核心思想: ┌─────────────────────────────┐ │ 外部世界 │ │ 只能通过公开接口交互 │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 公开接口(public) │ │ getName()、setAge() │ ├─────────────────────────────┤ │ 内部实现(private) │ │ _name、_age、_validate() │ └─────────────────────────────┘1.2 四种封装方式对比
| 方式 | 语法 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|---|
| 命名约定 | _privateField | 低(外部仍可访问) | 高 | 团队协作约定 |
| 闭包 | 函数作用域 | 高 | 中 | 传统模块模式 |
| Symbol | Symbol('key') | 中(可被反射获取) | 高 | 半私有属性 |
私有字段# | #privateField | 极高(语法级私有) | 高 | ES2022+ 现代项目 |
1.3 闭包封装(传统方式)
functioncreatePerson(name,age){// _name 和 _age 是闭包中的私有变量let_name=name;let_age=age;return{getName(){return_name;},setName(newName){if(typeofnewName!=='string'){thrownewTypeError('名字必须是字符串');}_name=newName;},getAge(){return_age;},setAge(newAge){if(newAge<0||newAge>150){thrownewRangeError('年龄不合法');}_age=newAge;},// 只读属性:不暴露 settergetInfo(){return`${_name},${_age}岁`;}};}constperson=createPerson('张三',25);console.log(person.getName());// 张三person.setAge(26);console.log(person.getInfo());// 张三,26 岁// person._name → undefined,无法直接访问1.4 私有字段#(ES2022)
classPerson{// 私有字段:只能在类内部访问#name;#age;constructor(name,age){this.#name=name;this.#age=age;}getname(){returnthis.#name;}setname(value){if(typeofvalue!=='string'){thrownewTypeError('名字必须是字符串');}this.#name=value;}getInfo(){return`${this.#name},${this.#age}岁`;}}constperson=newPerson('李四',30);console.log(person.name);// 李四console.log(person.#name);// SyntaxError: 私有字段只能在类内部访问1.5 封装方式性能对比
// 闭包封装:每次创建实例都生成新的函数对象functionclosureEncapsulation(){letcount=0;return{increment(){count++;}};}// 类 + 私有字段:方法在原型上共享,内存更高效classClassEncapsulation{#count=0;increment(){this.#count++;}}// 内存对比:创建 10000 个实例constclosureInstances=Array.from({length:10000},closureEncapsulation);constclassInstances=Array.from({length:10000},()=>newClassEncapsulation());// closureInstances 占用更多内存(每个实例都有独立的 increment 函数)// classInstances 占用更少内存(increment 在原型上共享)二、继承(Inheritance)
2.1 继承方式演进图谱
JavaScript 继承方式演进: ┌─────────────────┐ │ 原型链继承 │ ← 最原始,存在引用共享问题 │ prototype │ └────────┬────────┘ │ ┌────────▼────────┐ │ 构造函数继承 │ ← 解决引用共享,但方法无法复用 │ call/apply │ └────────┬────────┘ │ ┌────────▼────────┐ │ 组合继承 │ ← 结合前两种,但调用两次父类构造函数 │ prototype + call│ └────────┬────────┘ │ ┌────────▼────────┐ │ 寄生组合继承 │ ← 最完善的 ES5 方案 │ Object.create │ └────────┬────────┘ │ ┌────────▼────────┐ │ Class extends │ ← ES6 语法糖,底层仍是寄生组合 │ super() │ └─────────────────┘2.2 六种继承方式详解
方式 1:原型链继承
functionAnimal(){this.colors=['黑色','白色'];}Animal.prototype.getColors=function(){returnthis.colors;};functionDog(){}// 子类原型指向父类实例Dog.prototype=newAnimal();constdog1=newDog();constdog2=newDog();dog1.colors.push('棕色');console.log(dog2.colors);// ['黑色', '白色', '棕色'] ← 共享了引用!问题:父类的引用类型属性被所有实例共享。
方式 2:构造函数继承
functionAnimal(name){this.name=name;this.colors=['黑色','白色'];}functionDog(name,breed){// 在子类中调用父类构造函数Animal.call(this,name);this.breed=breed;}constdog1=newDog('旺财','金毛');constdog2=newDog('来福','柯基');dog1.colors.push('棕色');console.log(dog1.colors);// ['黑色', '白色', '棕色']console.log(dog2.colors);// ['黑色', '白色'] ← 不共享了!// 但 dog1.getColors is undefined ← 父类原型方法无法继承问题:无法继承父类原型上的方法。
方式 3:组合继承(最常用 ES5 方案)
functionAnimal(name){this.name=name;this.colors=['黑色','白色'];}Animal.prototype.getColors=function(){returnthis.colors;};functionDog(name,breed){Animal.call(this,name);// 第二次调用父类构造函数this.breed=breed;}Dog.prototype=newAnimal();// 第一次调用父类构造函数Dog.prototype.constructor=Dog;constdog=newDog('旺财','金毛');console.log(dog.getColors());// ['黑色', '白色']问题:调用了两次父类构造函数,效率略低。
方式 4:寄生组合继承(最佳 ES5 方案)
functionAnimal(name){this.name=name;}Animal.prototype.say=function(){return`我是${this.name}`;};functionDog(name,breed){Animal.call(this,name);this.breed=breed;}// 核心:用 Object.create 创建中间对象,只继承原型Dog.prototype=Object.create(Animal.prototype);Dog.prototype.constructor=Dog;Dog.prototype.getBreed=function(){returnthis.breed;};constdog=newDog('旺财','金毛');console.log(dog.say());// 我是 旺财console.log(dog.getBreed());// 金毛优点:只调用一次父类构造函数,不共享引用,原型链清晰。
方式 5:Class extends(ES6+)
classAnimal{constructor(name){this.name=name;}say(){return`我是${this.name}`;}}classDogextendsAnimal{constructor(name,breed){super(name);// 调用父类构造函数this.breed=breed;}getBreed(){returnthis.breed;}}constdog=newDog('旺财','金毛');console.log(dog.say());// 我是 旺财console.log(dog.getBreed());// 金毛本质:class extends是寄生组合继承的语法糖。
2.3 继承方式对比表
| 继承方式 | 引用共享 | 方法复用 | 调用父构造函数次数 | 代码复杂度 | 推荐度 |
|---|---|---|---|---|---|
| 原型链继承 | ❌ 共享 | ✅ 复用 | 1 | 低 | ⭐ |
| 构造函数继承 | ✅ 不共享 | ❌ 不复用 | 1 | 低 | ⭐⭐ |
| 组合继承 | ✅ 不共享 | ✅ 复用 | 2 | 中 | ⭐⭐⭐ |
| 寄生组合继承 | ✅ 不共享 | ✅ 复用 | 1 | 中 | ⭐⭐⭐⭐ |
| Class extends | ✅ 不共享 | ✅ 复用 | 1 | 低 | ⭐⭐⭐⭐⭐ |
三、多态(Polymorphism)
3.1 鸭子类型(Duck Typing)
JavaScript 是动态类型语言,多态通过鸭子类型实现:
“如果它走起路来像鸭子,叫起来像鸭子,那它就是鸭子。”
// 不需要共同的父类,只需要有相同的方法classDuck{makeSound(){return'嘎嘎嘎';}}classDog{makeSound(){return'汪汪汪';}}classCat{makeSound(){return'喵喵喵';}}// 多态:统一接口,不同表现functionanimalConcert(animals){animals.forEach(animal=>{console.log(animal.makeSound());});}animalConcert([newDuck(),newDog(),newCat()]);// 输出:嘎嘎嘎、汪汪汪、喵喵喵3.2 运行时方法重写
classShape{getArea(){thrownewError('子类必须实现 getArea 方法');}}classRectangleextendsShape{constructor(width,height){super();this.width=width;this.height=height;}getArea(){returnthis.width*this.height;}}classCircleextendsShape{constructor(radius){super();this.radius=radius;}getArea(){returnMath.PI*this.radius**2;}}// 多态使用constshapes=[newRectangle(10,5),newCircle(3)];shapes.forEach(shape=>{console.log(`面积:${shape.getArea()}`);});// 面积: 50// 面积: 28.274333882308138四、组合优于继承
4.1 继承的局限性
继承的问题: Animal │ ┌─────────────┼─────────────┐ ▼ ▼ ▼ Bird Fish Mammal │ │ │ ▼ ▼ ▼ Penguin FlyingFish Bat 问题:Penguin 是 Bird 但不会飞,Bat 是 Mammal 却会飞 继承层次越深,"is-a" 关系越难维护4.2 组合的设计
// 将行为拆分为可复用的功能模块constcanFly={fly(){return`${this.name}在飞翔`;}};constcanSwim={swim(){return`${this.name}在游泳`;}};// 通过组合创建对象,而非继承functioncreateBird(name){return{name,...canFly};}functioncreatePenguin(name){return{name,...canSwim// 企鹅不会飞,但会游泳};}functioncreateDuck(name){return{name,...canFly,...canSwim// 鸭子既会飞也会游泳};}constduck=createDuck('唐老鸭');console.log(duck.fly());// 唐老鸭 在飞翔console.log(duck.swim());// 唐老鸭 在游泳五、常见误区与注意点
| 误区 | 正确做法 |
|---|---|
| 所有场景都用 Class extends | 简单组合场景用对象展开或工厂函数更灵活 |
| 深层继承链(3+ 层) | 继承不超过 2 层,优先用组合替代 |
| 父类构造函数有副作用 | 确保子类super()调用时机正确,且父类构造函数纯净 |
忽略constructor的修正 | 手动设置原型后,务必修正prototype.constructor |
用for...in遍历继承属性 | 使用hasOwnProperty过滤,或改用Object.keys |
六、动手练习
练习 1:实现寄生组合继承
// 请用寄生组合继承让 Square 继承 RectanglefunctionRectangle(width,height){this.width=width;this.height=height;}Rectangle.prototype.getArea=function(){returnthis.width*this.height;};// 你的代码:实现 Square 继承 RectanglefunctionSquare(side){// ...}参考答案:
functionSquare(side){Rectangle.call(this,side,side);}Square.prototype=Object.create(Rectangle.prototype);Square.prototype.constructor=Square;练习 2:组合实现日志功能
// 用组合方式给任何对象添加日志功能constwithLogging={log(message){console.log(`[${newDate().toISOString()}]${message}`);}};// 给 User 对象添加日志能力functioncreateUser(name){return{name,login(){this.log(`${this.name}登录了`);},...withLogging};}七、AI 辅助学习
7.1 本节知识点的 AI 提问模板
- “JavaScript 中寄生组合继承的完整实现是什么?”
- “私有字段
#和闭包封装有什么区别?” - “什么是鸭子类型?请用 JavaScript 举例”
- “什么情况下应该优先使用组合而不是继承?”
7.2 用 AI 验证你的理解
让 AI 生成一段包含多层继承的代码,分析其中存在的问题并提出改进方案。
7.3 警惕 AI 的常见错误
AI 有时会建议使用已废弃的__proto__进行继承,或在 Class 中忘记调用super()。
八、配套代码
本文示例代码位于:CODE-ADVANCED/05-封装继承与多态/
| 文件名 | 说明 |
|---|---|
encapsulation-patterns.js | 4 种封装模式对比(命名约定、闭包、Symbol、私有字段) |
inheritance-evolution.js | 6 种继承方式完整实现 |
polymorphism-demo.js | 多态与鸭子类型示例 |
composition-vs-inheritance.js | 组合优于继承的对比实现 |
九、本章小结
- 封装:4 种方式中,ES2022 的
#私有字段是最佳选择,闭包适合传统场景 - 继承:6 种方式中,Class
extends和寄生组合继承是最推荐的 - 多态:JS 通过鸭子类型实现多态,关注"能做什么"而非"是什么"
- 设计原则:组合优于继承,继承不超过 2 层
下篇预告:Class 高级特性 —— 深入 ES6+ Class 的私有字段、静态成员与 Mixin 混入。
如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!