JavaScript面向对象设计的七大基本原则详解

作者:木棉

时间:2026-10-23

浏览:194

本文深入解析JavaScript面向对象编程的七大核心原则,涵盖单一职责、开闭、里氏替换、依赖倒置、接口隔离、迪米特法则及组合聚合复用。通过具体代码实例,阐述如何降低耦合度、提升代码可维护性与扩展性,帮助开发者构建更健壮的系统架构。

文章目录

本文通过具体实例,详细解析JavaScript面向对象编程中的七大基本原则,旨在为读者提供清晰的参考与学习路径。

面向对象编程具备独特的特性与规范。若对面向对象已有基础认知,应掌握其三大核心特征:封装、继承与多态。若对这些概念尚不熟悉,建议先参阅《面向对象之三个基本特征(JavaScript)》一文以夯实基础。

单一职责

在编程实践中,若单个类或方法包含过多功能,将严重损害代码可读性,导致维护困难。为解决这一痛点,单一职责原则应运而生。

什么是单一职责

单一职责,亦称单一功能原则,是面向对象五大基本原则(SOLID)之一。其核心定义是:一个类应当仅有一个引起它变化的原因。

具体而言,一个类应只负责一项职责,仅执行与其相关的事务。在类设计阶段,需按职责进行划分,确保各部分正交且互不干扰。

单一职责的好处
  1. 降低类的复杂性,使实现的功能职责清晰明确。
  2. 提高代码可读性,随着复杂性降低,代码逻辑更易理解。
  3. 提升可维护性,高可读性自然带来更便捷的维护体验。
  4. 降低变更带来的风险。若接口职责单一,修改一个接口通常只影响对应的实现类,而不波及其它接口,这对系统的扩展性与长期维护具有重大帮助。
实例

class ShoppinCar {
  constructor(){
    this.goods = [];
  }
  addGoods(good){
    this.goods = [good];
  }
  getGoodsList(){
    return this.goods;
  }
}
class Settlement {
  constructor(){
    this.result = 0; 
  }
  calculatePrice(list,key){
    let allPrice = 0;
    list.forEach((el) => {
      allPrice += el[key];
    })
    this.result = allPrice;
  }
  getAllPrice(){
    return this.result;
  }
}

参考上述代码ShoppinCar,该类包含addGoods和getGoodsList两个方法,分别用于添加商品和获取商品列表。同时,Settlement类包含calculatePrice和getAllPrice两个方法,分别负责计算价格与获取总价。ShoppinCar与Settlement各自专注于自身业务。尽管添加商品与计算价格在业务逻辑上存在依赖,但在代码实现中通过两个独立的类来解耦,各自处理各自的事务。如此设计,任一类的修改均不会影响另一个类。

开闭原则

若类中暴露的方法发生变更,可能引发严重后果,导致其他依赖该方法且无需变更的业务模块大面积瘫痪。为解决此问题,可单独编写新方法,若该方法与原类中其他方法存在依赖关系,则需特殊处理。

解决办法:

  1. 将依赖的代码复制一份至新类中。
  2. 在新类中引用旧类的方法。

然而,上述两种方案均非最佳解决之道。

第一种方案会导致代码大量重复,第二种方案则会导致类与类之间产生紧密依赖。

什么是开闭原则

开闭原则指出:“软件中的对象(类、模块、函数等)应该对于扩展是开放的,但对于修改是封闭的。”这意味着允许在不更改源代码的前提下变更实体的行为。

开闭原则强调对扩展开放、对修改关闭,但这并不意味着完全禁止修改。底层模块的变更必然需要高层模块进行耦合,否则代码片段将孤立且无意义。开闭原则是最基础的原则,其余六个原则均为其具体形态,是指导设计的工具与方法,而开闭原则则是整个体系的精神领袖。

开闭原则好处
  1. 有利于开展单元测试。
  2. 提高代码的复用性。
  3. 提高系统的可维护性。
  4. 符合面向对象开发的要求。
实例

class Drag {
  down(){
    // ...
  }  
  move(){
    // ...
    // 对拖拽没有做任何限制可以随意拖拽
  }  
  up(){
    // ...
  } 
}
class LimitDrag extends Drag {
  move(){
    // ...
    // 重写该方法对拖拽进行限制处理
  }
}

在LimitDrag中重写了move方法。若进行修改以满足两种需求:限制型拖拽与不限制型拖拽,即使其中一个发生更改,另一个仍可正常运行。

里氏替换

开发人员在使用他人组件时,只需了解组件对外暴露的接口,即其全部行为的集合,无需知晓内部实现细节。因此,使用者仅能通过接口实现预期行为。若组件接口提供的行为与使用者预期不符,错误便会产生。里氏替换原则旨在设计时避免派生类与基类行为不一致的问题。

什么是里氏替换

里氏替换原则:作为面向对象高层原则的OCP,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,以维持设计的封闭性。“抽象”是语言提供的功能,“多态”由继承语义实现。

里氏替换好处
  1. 实现代码共享,减少创建类的工作量,每个子类均拥有父类的方法和属性。
  2. 提高代码的重用性。
  3. 子类可形似父类,但又异于父类。
  4. 提高代码的可扩展性,只需实现父类的方法即可。许多开源框架的扩展接口均通过继承父类来完成。
  5. 提高产品或项目的开放性。
实例

// 抽象枪类
class AbstractGun {
  shoot(){
    throw "Abstract methods cannot be called";
  }
}
// 步枪
class Rifle extends AbstractGun {
  shoot(){
    console.log("步枪射击...");
  }
}
// 狙击枪
class AUG extends Rifle {
  zoomOut(){
    console.log("通过放大镜观察");
  }
  shoot(){
    console.log("AUG射击...");
  }
}
// 士兵
class Soldier {
  constructor(){
    this.gun = null;
  }
  setGun(gun){
    this.gun = gun;
  }
  killEnemy(){
    if(!this.gun){
      throw "需要给我一把枪";
      return;
    }
    console.log("士兵开始射击...");
    this.gun.shoot();
  }
}
// 狙击手
class Snipper extends Soldier {
  killEnemy(aug){
    if(!this.gun){
      throw "需要给我一把枪";
      return;
    }
    this.gun.zoomOut();
    this.gun.shoot();
  }
}
let soldier = new Soldier();
soldier.setGun(new Rifle());
soldier.killEnemy();

let snipper = new Snipper();
// 分配狙击枪
snipper.setGun(new AUG());
snipper.killEnemy();

snipper.setGun(new Rifle());
// snipper.killEnemy(); // this.gun.zoomOut is not a function

从上述代码可以看出,子类和父类之间的关系表现为:子类方法的功能范围一定等于或大于父类方法。子类可能出现的情况,父类不一定能出现;但父类出现的地方,子类一定能够出现。

依赖倒置

若方法与方法之间或类与类之间存在过多依赖关系,将导致代码可读性与可维护性极差。依赖倒置原则能有效解决这些问题。

什么是依赖倒置

依赖倒置原则:程序应依赖于抽象接口,而非具体实现。简而言之,即要求对抽象进行编程,不要对实现进行编程,从而降低客户与实现模块间的耦合度。

  1. 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
  2. 抽象不应该依赖细节。
  3. 细节应该依赖抽象。
依赖倒置好处
  1. 通过依赖于接口,隔离了具体实现类。
  2. 低一层的变动并不会导致高一层的变动。
  3. 提高了代码的容错性、扩展性和易于维护。
实例

// 抽象枪类
class AbstractGun {
  shoot(){
    throw "Abstract methods cannot be called";
  }
}
// 步枪
class Rifle extends AbstractGun {
  shoot(){
    console.log("步枪射击...");
  }
}
// 狙击枪
class AUG extends AbstractGun {
  shoot(){
    console.log("AUG射击...");
  }
}

从上面的代码可以看出,步枪与狙击枪的shoot全部都是依赖于AbstractGun抽象的枪类,上述编程满足了依赖倒置原则。

接口隔离

什么是接口隔离

接口隔离:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

接口隔离原则与单一职责原则的审视角度不相同。单一职责原则要求是类和接口的职责单一,注重的职责,这是业务逻辑上的划分。接口隔离原则要求接口的方法尽量少。

接口隔离好处
  1. 避免接口污染。
  2. 提高灵活性。
  3. 提供定制服务。
  4. 实现高内聚。
实例

function mix(...mixins) {
 class Mix {}
 for (let mixin of mixins) {
  copyProperties(Mix, mixin);
  copyProperties(Mix.prototype, mixin.prototype);
 }
 return Mix;
}
function copyProperties(target, source) {
 for (let key of Reflect.ownKeys(source)) {
  if ( key !== "constructor"&& key !== "prototype"&& key !== "name") {
   let desc = Object.getOwnPropertyDescriptor(source, key);
   Object.defineProperty(target, key, desc);
  }
 }
}
class Behavior {
  eat(){
    throw "Abstract methods cannot be used";
  }  
  call(){
    throw "Abstract methods cannot be used";
  }
}
class Action {
  climbTree(){
    throw "Abstract methods cannot be used";
  }
}
class Dog extends Behavior{
  eat(food){
    console.log(`狗正在吃${food}`);
  }
  hungry(){
    console.log("汪汪汪,我饿了")
  }
}
const CatMin = mix(Behavior,Action);
class Cat extends CatMin{
  eat(food){
    console.log(`猫正在吃${food}`);
  }
  hungry(){
    console.log("喵喵喵,我饿了")
  }
  climbTree(){
    console.log("爬树很开心哦~")
  }
}
let dog = new Dog();
dog.eat("骨头");
dog.hungry();
let cat = new Cat();
cat.eat("鱼");
cat.hungry();
cat.climbTree();

大家一定要好好分析一下上面的代码,共有两个抽象类,分别对应不同的行为,Cat与Dog类拥有共同的行为,但是Cat又拥有其自己单独的行为,使用抽象(即接口)继承其方法,使用接口隔离使其完成各自的工作,各司其职。

迪米特法则

迪米特法则:最少知识原则(Least Knowledge Principle 简写LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。英文简写为: LoD.

迪米特法则的做法观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。一个类应该对其他对象保持最少的了解。通俗来讲,就是一个类对自己依赖的类知道的越少越好。因为类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

迪米特法则好处
  1. 减少对象之间的耦合性。
实例

class ISystem {
  close(){
    throw "Abstract methods cannot be used";
  }
}
class System extends ISystem{
  saveCurrentTask(){
    console.log("saveCurrentTask")
  }
  closeService(){
    console.log("closeService")
  }
  closeScreen(){
    console.log("closeScreen")
  }
  closePower(){
    console.log("closePower")
  }
  close(){
    this.saveCurrentTask();
    this.closeService();
    this.closeScreen();
    this.closePower();
  }
}
class IContainer{
  sendCloseCommand(){
    throw "Abstract methods cannot be used";
  }
}
class Container extends IContainer{
  constructor(){
    super()
    this.system = new System();
  }
  sendCloseCommand(){
    this.system.close();
  }
}
class Person extends IContainer{
  constructor(){
    super();
    this.container = new Container();
  }
  clickCloseButton(){
    this.container.sendCloseCommand();
  }
}
let person = new Person();
person.clickCloseButton();

上面代码中Container作为媒介,其调用类不知道其内部是如何实现,用户去触发按钮,Container把消息通知给计算机,计算机去执行相对应的命令。

组合/聚合复用原则

聚合(Aggregation)表示一种弱的‘拥有'关系,体现的是A对象可以包含B对象但B对象不是A对象的一部分。

合成(Composition)则是一种强的'拥有'关系,体现了严格的部分和整体关系,部分和整体的生命周期一样。

组合/聚合:是通过获得其他对象的引用,在运行时刻动态定义的,也就是在一个对象中保存其他对象的属性,这种方式要求对象有良好定义的接口,并且这个接口也不经常发生改变,而且对象只能通过接口来访问,这样我们并不破坏封装性,所以只要类型一致,运行时还可以通过一个对象替换另外一个对象。

优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上,这样类和类继承层次会保持较小规模,而且不太可能增长为不可控制的庞然大物。

组合/聚合复用原则好处
  1. 新的实现较为容易,因为超类的大部分功能可通过继承关系自动进入子类;
  2. 修改或扩展继承而来的实现较为容易。
实例

function mix(...mixins) {
 class Mix {}
 for (let mixin of mixins) {
  copyProperties(Mix, mixin);
  copyProperties(Mix.prototype, mixin.prototype);
 }
 return Mix;
}
function copyProperties(target, source) {
 for (let key of Reflect.ownKeys(source)) {
  if ( key !== "constructor"&& key !== "prototype"&& key !== "name") {
   let desc = Object.getOwnPropertyDescriptor(source, key);
   Object.defineProperty(target, key, desc);
  }
 }
}
class Savings {
  saveMoney(){
    console.log("存钱");
  }
  withdrawMoney(){
    console.log("取钱");
  }
}
class Credit {
  overdraft(){
    console.log("透支")
  }
}
const CarMin = mix(Savings,Credit);
class UserCar extends CarMin {
  constructor(num,carUserName){
    super();
    console.log()
    this.carNum = num;
    this.carUserName = carUserName;
  }
  getCarNum(){
    return this.carNum;
  }
  getCarUserName(){
    return this.carUserName;
  }
}
let myCar = new UserCar(123456798,"Aaron");
console.log(myCar.getCarNum());
console.log(myCar.getCarUserName());
myCar.saveMoney();
myCar.withdrawMoney();
myCar.overdraft();

总结

这些原则在设计模式中体现的淋淋尽致,设计模式就是实现了这些原则,从而达到了代码复用、增强了系统的扩展性。所以设计模式被很多人奉为经典。我们可以通过好好的研究设计模式,来慢慢的体会这些设计原则。

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章