JavaScript面向对象之七大基本原则实例详解

 更新时间:2020年5月7日 11:33  点击:2154

本文实例讲述了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类存在两个方法addGoodsgetGoodsList,分别是添加商品和获取商品列表。Settlement类中存在两个方法calculatePricegetAllPrice分别做的事情是计算价钱与获取总价钱。ShoppinCarSettlement都是在做自己的事情。添加商品与计算价格,虽然在业务上是相互依赖的,但是在代码中分别用两个类,然他们自己做自己的事情。其中任何一个类更改不会对另一个类进行更改。

开闭原则

在一个类中暴露出去的方法,若这个方法变更了,则会产生很大的后果,可能导致其他依赖于这个方法且有不需要变更的业务造成大面积瘫痪。为了解决这个问题,可以单独再写一个方法,若这个方法与这个类中的其他方法相互依赖。

解决办法:

  1. 把其中依赖的代码copy一份到新的类中。

  2. 在新类中引用旧类中的方法。

两种方法都不是最好的解决方案。

第一种方法会导致代码大量的重复,第二种方法会导致类与类之间互相依赖。

什么是开闭原则

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

开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。开闭原则是一个最基本的原则,另外六个原则都是开闭原则的具体形态,是指导设计的工具和方法,而开闭原则才是精神领袖.

开闭原则好处
  1. 开闭原则有利于进行单元测试

  2. 开闭原则可以提高复用性

  3. 开闭原则可以提高可维护性

  4. 面向对象开发的要求

实例

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

LimitDrag中重写了move方法,若修改了可以满足两种需求,一种是限制型拖拽,一种是不限制型拖拽,任何一个更改了另外一个还是可以正常运行。

里氏替换

每个开发人员在使用别人的组件时,只需知道组件的对外裸露的接口,那就是它全部行为的集合,至于内部到底是怎么实现的,无法知道,也无须知道。所以,对于使用者而言,它只能通过接口实现自己的预期,如果组件接口提供的行为与使用者的预期不符,错误便产生了。里氏替换原则就是在设计时避免出现派生类与基类不一致的行为。

什么是里氏替换

里氏替换原则:OCP作为OO的高层原则,主张使用“抽象(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();

大家一定要好好分析一下上面的代码,共有两个抽象类,分别对应不同的行为,CatDog类拥有共同的行为,但是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();

[!--infotagslink--]

相关文章

  • 使用PHP+JavaScript将HTML页面转换为图片的实例分享

    这篇文章主要介绍了使用PHP+JavaScript将HTML元素转换为图片的实例分享,文后结果的截图只能体现出替换的字体,也不能说将静态页面转为图片可以加快加载,只是这种做法比较interesting XD需要的朋友可以参考下...2016-04-19
  • 关于JavaScript中name的意义冲突示例介绍

    在昨天的《Javascript权威指南》学习笔记之十:ECMAScript 5 增强的对象模型一文中,对于一段代码的调试出现了一个奇怪现象,现将源代码贴在下面: 复制代码 代码如下: <script type="text/javascript"> function Person(){}...2014-05-31
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • javascript自定义的addClass()方法

    复制代码 代码如下: //element:需要添加新样式的元素,value:新的样式 function addClass(element, value ){ if (!element.className){ element.className = value; }else { newClassName = element.className; newClas...2014-05-31
  • JavaScript中的this关键字使用方法总结

    在javascritp中,不一定只有对象方法的上下文中才有this, 全局函数调用和其他的几种不同的上下文中也有this指代。 它可以是全局对象、当前对象或者任意对象,这完全取决于函数的调用方式。JavaScript 中函数的调用有以下...2015-03-15
  • JavaScript中逗号运算符介绍及使用示例

    有一道js面试题,题目是这样的:下列代码的执行结果是什么,为什么? 复制代码 代码如下: var i, j, k; for (i=0, j=0; i<10, j<6; i++, j++) { k = i+j; } document.write(k); 答案是显示10,这道题主要考察JavaScript的逗...2015-03-15
  • javascript的事件触发器介绍的实现

    事件触发器从字面意思上可以很好的理解,就是用来触发事件的,但是有些没有用过的朋友可能就会迷惑了,事件不是通常都由用户在页面上的实际操作来触发的吗?这个观点不完全正确,因为有些事件必须由程序来实现,如自定义事件,jQue...2014-06-07
  • 详解javascript数组去重问题

    首先,我想到的是另建一个结果数组,用来存储原始数组中不重复的数据。遍历原始数组依次跟结果数组中的元素进行比较,检测是否重复。于是乎,我写出了如下代码A: Array.prototype.clearRepetitionA = function(){ var resul...2015-11-08
  • ActiveX控件与Javascript之间的交互示例

    1、ActiveX向Javascript传参 复制代码 代码如下: <script language="javascript" for="objectname" event="fun1(arg)"> fun2(arg); </script> objectname为ActiveX控件名,通过<object>标签里的id属性设定,如下; 复制...2014-06-07
  • Javascript类型转换的规则实例解析

    这篇文章主要介绍了Javascript类型转换的规则实例解析,涉及到javascript类型转换相关知识,对本文感兴趣的朋友一起学习吧...2016-02-27
  • 详解JavaScript操作HTML DOM的基本方式

    通过 HTML DOM,可访问 JavaScript HTML 文档的所有元素。 HTML DOM (文档对象模型) 当网页被加载时,浏览器会创建页面的文档对象模型(Document Object Model)。 HTML DOM 模型被构造为对象的树: 通过可编程的对象模型,Java...2015-10-23
  • JavaScript获取浏览器信息的方法

    Window有navigator对象让我们得知浏览器的全部信息.我们可以利用一系列的API函数得知浏览器的信息.JavaScript代码如下:function message(){ txt = "<p>浏览器代码名: " + navigator.appCodeName + "</p>";txt+= "<p>...2015-11-24
  • 跟我学习javascript的最新标准ES6

    虽然ES6都还没真正发布,但已经有用ES6重写的程序了,各种关于ES789的提议已经开始了,这你敢信。潮流不是我等大众所能追赶的。潮流虽然太快,但我们不停下学习的步伐,就不会被潮流丢下的,下面来领略下ES6中新特性,一堵新生代JS...2015-11-24
  • javascript设计模式之解释器模式详解

    神马是“解释器模式”?先翻开《GOF》看看Definition:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。在开篇之前还是要科普几个概念: 抽象语法树: 解释器模式并未解释如...2014-06-07
  • JavaScript操作URL的相关内容集锦

    ---恢复内容开始---1.location.href.....(1)self.loction.href="http://www.cnblogs.com/url" window.location.href="http://www.cnblogs.com/url" 以上两个用法相同均为在当前页面打开URL页面 (2)this.locati...2015-10-30
  • javascript实现tab切换的四种方法

    tab切换在网页中很常见,故最近总结了4种实现方法。 首先,写出tab的框架,加上最简单的样式,代码如下: <!DOCTYPE html> <html> <head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><style> *{ pa...2015-11-08
  • 基于JavaScript如何实现私有成员的语法特征及私有成员的实现方式

    前言在面向对象的编程范式中,封装都是必不可少的一个概念,而在诸如 Java,C++等传统的面向对象的语言中, 私有成员是实现封装的一个重要途径。但在 JavaScript 中,确没有在语法特性上对私有成员提供支持, 这也使得开发人员使...2015-10-30
  • JavaScript预解析,对象详解

    这篇文章主要介绍了JavaScript预解析,对象的的相关资料,小编觉得这篇文章写的还不错,需要的朋友可以参考下,希望能够给你带来帮助...2021-11-10
  • 学习JavaScript设计模式之装饰者模式

    这篇文章主要为大家介绍了JavaScript设计模式中的装饰者模式,对JavaScript设计模式感兴趣的小伙伴们可以参考一下...2016-01-21
  • javascript实现数独解法

    生生把写过的java版改成javascript版,第一次写,很不专业,见谅。唉,我是有多闲。复制代码 代码如下: var Sudoku = { init: function (str) { this.blank = []; this.fixed = []; this.cell =...2015-03-15