05-封装、继承与多态
2026/6/22 23:39:26 网站建设 项目流程

封装、继承与多态

面向对象编程(OOP)的三大特性——封装、继承、多态——在 JavaScript 中有独特的实现方式。本文将系统梳理 JS 中的 OOP 实践,从闭包封装到原型链继承,再到鸭子类型多态。


学习目标

读完本文,你将学会:

  • 4 种 JavaScript 封装技术的实现与对比
  • 6 种继承方式的原理、优缺点与适用场景
  • 多态与鸭子类型在 JS 中的实践
  • "组合优于继承"的设计思想

一、封装(Encapsulation)

1.1 什么是封装

封装是将数据(属性)和操作数据的方法绑定在一起,并隐藏内部实现细节,只暴露有限的公共接口。

封装的核心思想: ┌─────────────────────────────┐ │ 外部世界 │ │ 只能通过公开接口交互 │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 公开接口(public) │ │ getName()、setAge() │ ├─────────────────────────────┤ │ 内部实现(private) │ │ _name、_age、_validate() │ └─────────────────────────────┘

1.2 四种封装方式对比

方式语法安全性性能适用场景
命名约定_privateField低(外部仍可访问)团队协作约定
闭包函数作用域传统模块模式
SymbolSymbol('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.js4 种封装模式对比(命名约定、闭包、Symbol、私有字段)
inheritance-evolution.js6 种继承方式完整实现
polymorphism-demo.js多态与鸭子类型示例
composition-vs-inheritance.js组合优于继承的对比实现

九、本章小结

  • 封装:4 种方式中,ES2022 的#私有字段是最佳选择,闭包适合传统场景
  • 继承:6 种方式中,Classextends和寄生组合继承是最推荐的
  • 多态:JS 通过鸭子类型实现多态,关注"能做什么"而非"是什么"
  • 设计原则:组合优于继承,继承不超过 2 层

下篇预告:Class 高级特性 —— 深入 ES6+ Class 的私有字段、静态成员与 Mixin 混入。


如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询