05-封装、继承与多态

发布时间:2026/6/22 23:39:57
05-封装、继承与多态 封装、继承与多态面向对象编程OOP的三大特性——封装、继承、多态——在 JavaScript 中有独特的实现方式。本文将系统梳理 JS 中的 OOP 实践从闭包封装到原型链继承再到鸭子类型多态。学习目标读完本文你将学会4 种 JavaScript 封装技术的实现与对比6 种继承方式的原理、优缺点与适用场景多态与鸭子类型在 JS 中的实践组合优于继承的设计思想一、封装Encapsulation1.1 什么是封装封装是将数据属性和操作数据的方法绑定在一起并隐藏内部实现细节只暴露有限的公共接口。封装的核心思想 ┌─────────────────────────────┐ │ 外部世界 │ │ 只能通过公开接口交互 │ └─────────────┬───────────────┘ │ ┌─────────────▼───────────────┐ │ 公开接口public │ │ getName()、setAge() │ ├─────────────────────────────┤ │ 内部实现private │ │ _name、_age、_validate() │ └─────────────────────────────┘1.2 四种封装方式对比方式语法安全性性能适用场景命名约定_privateField低外部仍可访问高团队协作约定闭包函数作用域高中传统模块模式SymbolSymbol(key)中可被反射获取高半私有属性私有字段##privateField极高语法级私有高ES2022 现代项目1.3 闭包封装传统方式functioncreatePerson(name,age){// _name 和 _age 是闭包中的私有变量let_namename;let_ageage;return{getName(){return_name;},setName(newName){if(typeofnewName!string){thrownewTypeError(名字必须是字符串);}_namenewName;},getAge(){return_age;},setAge(newAge){if(newAge0||newAge150){thrownewRangeError(年龄不合法);}_agenewAge;},// 只读属性不暴露 settergetInfo(){return${_name}${_age}岁;}};}constpersoncreatePerson(张三,25);console.log(person.getName());// 张三person.setAge(26);console.log(person.getInfo());// 张三26 岁// person._name → undefined无法直接访问1.4 私有字段#ES2022classPerson{// 私有字段只能在类内部访问#name;#age;constructor(name,age){this.#namename;this.#ageage;}getname(){returnthis.#name;}setname(value){if(typeofvalue!string){thrownewTypeError(名字必须是字符串);}this.#namevalue;}getInfo(){return${this.#name}${this.#age}岁;}}constpersonnewPerson(李四,30);console.log(person.name);// 李四console.log(person.#name);// SyntaxError: 私有字段只能在类内部访问1.5 封装方式性能对比// 闭包封装每次创建实例都生成新的函数对象functionclosureEncapsulation(){letcount0;return{increment(){count;}};}// 类 私有字段方法在原型上共享内存更高效classClassEncapsulation{#count0;increment(){this.#count;}}// 内存对比创建 10000 个实例constclosureInstancesArray.from({length:10000},closureEncapsulation);constclassInstancesArray.from({length:10000},()newClassEncapsulation());// closureInstances 占用更多内存每个实例都有独立的 increment 函数// classInstances 占用更少内存increment 在原型上共享二、继承Inheritance2.1 继承方式演进图谱JavaScript 继承方式演进 ┌─────────────────┐ │ 原型链继承 │ ← 最原始存在引用共享问题 │ prototype │ └────────┬────────┘ │ ┌────────▼────────┐ │ 构造函数继承 │ ← 解决引用共享但方法无法复用 │ call/apply │ └────────┬────────┘ │ ┌────────▼────────┐ │ 组合继承 │ ← 结合前两种但调用两次父类构造函数 │ prototype call│ └────────┬────────┘ │ ┌────────▼────────┐ │ 寄生组合继承 │ ← 最完善的 ES5 方案 │ Object.create │ └────────┬────────┘ │ ┌────────▼────────┐ │ Class extends │ ← ES6 语法糖底层仍是寄生组合 │ super() │ └─────────────────┘2.2 六种继承方式详解方式 1原型链继承functionAnimal(){this.colors[黑色,白色];}Animal.prototype.getColorsfunction(){returnthis.colors;};functionDog(){}// 子类原型指向父类实例Dog.prototypenewAnimal();constdog1newDog();constdog2newDog();dog1.colors.push(棕色);console.log(dog2.colors);// [黑色, 白色, 棕色] ← 共享了引用问题父类的引用类型属性被所有实例共享。方式 2构造函数继承functionAnimal(name){this.namename;this.colors[黑色,白色];}functionDog(name,breed){// 在子类中调用父类构造函数Animal.call(this,name);this.breedbreed;}constdog1newDog(旺财,金毛);constdog2newDog(来福,柯基);dog1.colors.push(棕色);console.log(dog1.colors);// [黑色, 白色, 棕色]console.log(dog2.colors);// [黑色, 白色] ← 不共享了// 但 dog1.getColors is undefined ← 父类原型方法无法继承问题无法继承父类原型上的方法。方式 3组合继承最常用 ES5 方案functionAnimal(name){this.namename;this.colors[黑色,白色];}Animal.prototype.getColorsfunction(){returnthis.colors;};functionDog(name,breed){Animal.call(this,name);// 第二次调用父类构造函数this.breedbreed;}Dog.prototypenewAnimal();// 第一次调用父类构造函数Dog.prototype.constructorDog;constdognewDog(旺财,金毛);console.log(dog.getColors());// [黑色, 白色]问题调用了两次父类构造函数效率略低。方式 4寄生组合继承最佳 ES5 方案functionAnimal(name){this.namename;}Animal.prototype.sayfunction(){return我是${this.name};};functionDog(name,breed){Animal.call(this,name);this.breedbreed;}// 核心用 Object.create 创建中间对象只继承原型Dog.prototypeObject.create(Animal.prototype);Dog.prototype.constructorDog;Dog.prototype.getBreedfunction(){returnthis.breed;};constdognewDog(旺财,金毛);console.log(dog.say());// 我是 旺财console.log(dog.getBreed());// 金毛优点只调用一次父类构造函数不共享引用原型链清晰。方式 5Class extendsES6classAnimal{constructor(name){this.namename;}say(){return我是${this.name};}}classDogextendsAnimal{constructor(name,breed){super(name);// 调用父类构造函数this.breedbreed;}getBreed(){returnthis.breed;}}constdognewDog(旺财,金毛);console.log(dog.say());// 我是 旺财console.log(dog.getBreed());// 金毛本质class extends是寄生组合继承的语法糖。2.3 继承方式对比表继承方式引用共享方法复用调用父构造函数次数代码复杂度推荐度原型链继承❌ 共享✅ 复用1低⭐构造函数继承✅ 不共享❌ 不复用1低⭐⭐组合继承✅ 不共享✅ 复用2中⭐⭐⭐寄生组合继承✅ 不共享✅ 复用1中⭐⭐⭐⭐Class extends✅ 不共享✅ 复用1低⭐⭐⭐⭐⭐三、多态Polymorphism3.1 鸭子类型Duck TypingJavaScript 是动态类型语言多态通过鸭子类型实现“如果它走起路来像鸭子叫起来像鸭子那它就是鸭子。”// 不需要共同的父类只需要有相同的方法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.widthwidth;this.heightheight;}getArea(){returnthis.width*this.height;}}classCircleextendsShape{constructor(radius){super();this.radiusradius;}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// 鸭子既会飞也会游泳};}constduckcreateDuck(唐老鸭);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.widthwidth;this.heightheight;}Rectangle.prototype.getAreafunction(){returnthis.width*this.height;};// 你的代码实现 Square 继承 RectanglefunctionSquare(side){// ...}参考答案functionSquare(side){Rectangle.call(this,side,side);}Square.prototypeObject.create(Rectangle.prototype);Square.prototype.constructorSquare;练习 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 混入。如果本文对你有帮助欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流