【JS】设计模式,代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。

  • Post author:
  • Post category:其他




设计模式 (design pattern)

设计模式是不区分语言的,是一种编程逻辑。

在合适的场景使用合适的设计模式,写出来的代码比较稳定、比较高效、维护性比价高。

设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。



设计模式原则


  1. 单一职责原则

    :一个程序只做好一件事、如果功能过于复杂就拆分开,每个部分保持独立

  2. 开放/封闭原则

    :对扩展开放,对修改封闭;增加需求时,扩展新代码,而非修改已有代码
  3. 里氏替换原则:子类能覆盖父类、父类能出现的地方子类就能出现
  4. 接口隔离原则:保持接口的单一独立,类似单一职责原则,这里更关注接口
  5. 依赖倒转原则:面向接口编程,依赖于抽象而不依赖于具体, 使用方只关注接口而不关注具体类的实现



设计模式详解



创造型
  • 构造器模式
  • 原型模式
  • 工厂模式
  • 抽象工厂模式
  • 单例模式


构造器模式

在面向对象的编程语言中,构造器是一个类中用来

初始化新对象

的特殊方法,并且可以

接受参数

用来设定实例对象的属性和方法。

有一天接到个需求,我们需要将三年二班的教职工录入系统中,此时班里只有小明自己,定义学生时,三下五除二就写完了。

// 定义学生
let 小明 = {
  name: "小明",
  age: 12,
  gender: '男',
  identity: '学生'
}

又进行一天的招生后,来了小红和小强,于是CV后把他也加入了…

// 定义学生
let 小明 = {
  name: "小明",
  age: 12,
  gender: '男',
  identity: '学生'
}
let 小红 = {
  name: "小红",
  age: 13,
  gender: '女',
  identity: '学生'
}
let 小强 = {
  name: "小强",
  age: 13,
  gender: '男',
  identity: '学生'
}

又过了两天你老板过来了 说:“三年二班杀疯了,一天之间招进来了

80个学生

”。此时继续以上写法,代码肯定是

重复并且臃肿

此时构造器就派上了用场,在面向对象的编程语言中,构造器是一个类中用来

初始化新对象

的特殊方法,并且可以

接受参数

用来设定实例对象的属性和方法。

基本构造器 在 JS 中,ES6之前是没有类这个概念的,所以一般用函数来表示一个构造器,使用方法是在构造器函数前使用 new 关键字。

所以,基本的构造器模式看起来是这样的:

// 注意:Student 构造函数首字母一般为大写
function Student(name, gender, age) {
  this.name = name;
  this.gender = gender;
  this.age = age;
  this.sayName = function () {
    console.log('我是' + this.name)
  }
}
// 调用 Student 构造函数,传入参数,初始化一个新的对象
let xiaoming = new Student('小明', '男', 17)

console.log(xiaoming.name); // '小明'
console.log(xiaoming.sayName()); // 我是小明

在这里插入图片描述

代码的冗余程度直线减少了,但也有个不理想的地方,就是每次创建一个新对象,都需要重新定义 sayName 这个方法。

为了使 sayName 这个方法在实例之间共享,我们使用原型(prototype)来优化。



原型模式

原型模式,就是创建一个共享的原型,通过

拷贝

这个原型来

创建新的类

,用于创建重复的对象,带来性能上的提升。

ps: 此块使用到

原型链

知识

继续上面的例子:

function Student(name, gender, age) {
  this.name = name;
  this.gender = gender;
  this.age = age;
}
// 如果在构造函数的原型属性上添加 sayName 方法,那么所有实例化的对象都会共享这个方法。优化代码是这样的:
Student.prototype.sayName = function () {
  console.log('我是' + this.name)
}
let xiaoming = new Student('小明', '男', 17)

console.log(xiaoming) // Student { name: '小明', gender: '男', age: 12 }
console.log(xiaoming.sayName()) // 我是小明

扩展:ES6版本 ES6 支持了类的定义,所以写起来风格更加优雅。

class Student {
  constructor(name, gender, age) {
    this.name = name
    this.gender = gender;
    this.age = age;
    this.work = ['学习', '玩游戏']
  }
  sayName() {
    console.log('我是' + this.name)
  }
}
let xiaoming = new Student('小明', '男', 17)
xiaoming.sayName() // -> '我是小明'

在这里插入图片描述


特点:

构造函数内不定义属性和方法,把属性和方法都定义在构造函数的原型上。这样所有的对象实例都共享对象原型上的属性和方法


优点:

  1. 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过克隆一个已有实例可以提高新实例的创建效率。
  2. 多个实例可以共享原型上的属性和方法


缺点:

  1. 修改原型上的一些引用属性,所有实例对应的属性也将被改变,这样可能带来一些问题


工厂模式

由一个工厂对象决定创建某一种产品对象类的实例。主要

用来创建同一类对象

我们三年二班除了学生还有教师,或许还有专职的助教,此时我们的Student类并不能满足我们的需求,所以此时我们需要再创建“教师”与“助教”

// 教师
class Teacher {
  constructor(name,gender,age) {
    this.name = name
    this.gender = gender;
    this.age = age;
    this.work = ['教书','偷懒']
  }
}

// 助教
class AssistantTeacher {
  constructor(name,gender,age) {
    this.name = name
    this.gender =gender;
    this.age =age;
    this.work = ['协助教师','收钱']
  }
}

现在我们有三个类了(后面可能还会有更多的类),麻烦的事情来了:难道我每从数据库拿到一条数据,都要人工判断一下这个人的身份,然后手动给它分配构造器吗?可以实现,但不推荐,最好还是交给函数去处理:

// 学生
class Student {
  constructor(name, gender, age) {
    this.name = name
    this.gender = gender;
    this.age = age;
    this.work = ['学习', '玩游戏']
  }
}

// 教师
class Teacher {
  constructor(name, gender, age) {
    this.name = name
    this.gender = gender;
    this.age = age;
    this.work = ['教书', '偷懒']
  }
}

// 助教
class AssistantTeacher {
  constructor(name, gender, age) {
    this.name = name
    this.gender = gender;
    this.age = age;
    this.work = ['协助教师', '收钱']
  }
}

function Factory(name, age, gender, identity) {
  switch (identity) {
    case 'stucent':
      return new Student(name, age, gender)
    case 'teacher':
      return new Teacher(name, age, gender)
    case 'assistantTeacher':
      return new AssistantTeacher(name, age, gender)
  }
}

看起来是好一些了,至少我们不用操心构造函数的分配问题了。

但如果再来几个身份,例如学生家长,例如寝室阿姨,难道要手写十个类、数十行 switch 吗?

当然不!

我们仔细观察上面的代码,发现每个类都有用name、age、gender、work这四个属性,它们之间的区别,也只在于 work 字段需要随 identity 字段取值的不同而改变,而其他三个不变,这样以来,我们是不是对共性封装的不够彻底呢?

现在我们把相同的逻辑封装回User类里,然后把这个承载了共性的 User 类和个性化的逻辑判断写入同一个函数:

function User(name, age, gender, identity, work) {
  this.name = name
  this.age = age
  this.gender = gender
  this.identity = identity
  this.work = work
}

function Factory(name, age, gender, identity) {
  let work = []
  switch (identity) {
    case 'student':
      work = ['学习', '玩游戏']
      break
    case 'teacher':
      work = ['教书', '偷懒']
      break
    case 'assistantTeacher':
      work = ['协助教师', '收钱']
    // case 'xxx':
    // 其它身份
    // ...
  }
  return new User(name, age, gender, identity, work)
}

let xiaoming = Factory('小明', 17, '男', 'status');
console.log(xiaoming); // User { name: '小明', age: 17, gender: '男', identity: 'status', work: [] }

在这里插入图片描述

function User(name, age, gender, identity, work) {
  this.name = name
  this.age = age
  this.gender = gender
  this.identity = identity
  this.work = work
}

function Factory(name, age, gender, identity) {
  let work = []
  switch (identity) {
    case 'student':
      work = ['学习', '玩游戏']
      break
    case 'teacher':
      work = ['教书', '偷懒']
      break
    case 'assistantTeacher':
      work = ['协助教师', '收钱']
      // case 'xxx':
      // 其它身份
      // ...
  }
  return new User(name, age, gender, identity, work)
}

let xiaoming = Factory('小明', 17, '男', 'status');
console.log(xiaoming); // User { name: '小明', age: 17, gender: '男', identity: 'status', work: [] }

这样一来,是不是爽多了?我们要做的事情可以简单太多,不用时刻想着拿到的这组数据是什么工种,不用想着给他分配什么构造函数,更不用手写无数个构造函数!!Factory函数 已经帮我们做完了一切,而我们只需要像以前一样

无脑传参

就可以了,舒服了!


简单总结一下

,工厂模式其实就是

将创建对象的过程单独封装

。就像去小卖铺买东西,你不必关心这个东西的制作过程,只用告诉老板你想要的,老板就会把物品 return 给你。

工厂模式很爽,因为他实现了

无脑传参



抽象工厂模式


前言

:在实际的业务中,我们往往面对的复杂度并非数个类、一个工厂可以解决,而是需要动用多个工厂。

我们继续看上个小节举出的例子,简单工厂函数最后长这样:

function Factory(name, age, gender, identity) {
  let work = []
  switch (identity) {
    case 'student':
      work = ['学习', '玩游戏']
      break
    case 'teacher':
      work = ['教书', '偷懒']
      break
    case 'assistantTeacher':
      work = ['协助教师', '收钱']
      // case 'xxx':
      // 教导主任
      // ...

      return new User(name, age, gender, identity, work)
  }
}

首先映入眼帘的是我们把所有身份塞进了同一个工厂,例如老师和学生,又例如之后可能会添加进来的教导主任,他们每种身份的权限都会存在着很大的差别,有些操作老师可以执行,又有些操作只有学校的管理层可以执行,因此我们需要对这个群体的对象进行单独的逻辑处理。

怎么办?去修改 Factory 的函数体,增加老师、教导主任相关的判断和处理逻辑吗?单从功能实现上来说,可以。但这么做会让代码变成💩山,因为学校还有校长、外包的食堂阿姨等等,每考虑到一个新的员工群体,就得去修改一次 Factory 的函数体。

这样做的后果是:

  1. 坑自己 ——

    Factory函数体会变得非常庞大

    ,导致每次添加角色的时候都不敢下手,因为一旦写出Bug,就会导致整个Factory函数的崩坏,进而摧毁整个系统;
  2. 坑队友 —— Factory 的逻辑过于繁杂和混乱,没人想维护它;
  3. 坑测试 —— 每新加一个工种,他都需要整个Factory 的逻辑进行回归,因为改变是在 Factory 内部发生的

因为没有遵守开放封闭原则:

对拓展开放,对修改封闭

楼上这波操作错就错在我们不是在拓展,而是在疯狂地修改。


详解:


抽象工厂模式

(Abstract Factory Pattern)是

围绕一个超级工厂创建其他工厂

。该超级工厂又称为其他工厂的工厂。

这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

作为上帝,我们想要创建一个动物,基本组成是躯体(Body)与灵魂(Soul)组成,我们准备开一个工厂来量产,但是我们又不知道具体生产的是什么类型的动物,只知道由这两部分组成,所以我先来一个抽象类来约定住动物的基本组成:

class AnimalFactory {
  // 创造躯体
  createBody() {
    throw new Error('抽象工厂方法不允许直接调用,你需要将我重写!')
  }
  // 创建灵魂
  createSoul() {
    throw new Error('抽象工厂方法不允许直接调用,你需要将我重写!')
  }
}

楼上这个类除了约定动物的基本构成外,啥也不干,如果你尝试new一个

AnimalFactory

实力并调用里面的方法,它都会给你报错。在抽象工厂模式里,楼上这个类就是我们食物链顶端最大的

Boss——AbstractFactory

(抽象工厂);


抽象工厂不干活,具体工厂(ConcreteFactory)干活

!当我们明确了生产方案以后就可以化抽象为具体,比如现在需要生产哺乳动物,那我就可以定制一个

具体工厂

class AnimalFactory {
  // 创造躯体
  createBody() {
    throw new Error('抽象工厂方法不允许直接调用,你需要将我重写!');
  }
  // 创建灵魂
  createSoul() {
    throw new Error('抽象工厂方法不允许直接调用,你需要将我重写!');
  }
}


//具体工厂继承自抽象工厂
class Mammals extends AnimalFactory {
  createBody() {
    // 提供哺乳动物的躯体
    return new MammalsBody();
  }
  createSoul() {
    // 提供哺乳动物的灵魂
    return new MammalsSoul()
  }
}

这里我们在提供哺乳动物的时候,调用了两个构造函数:MammalsBody和MammalsSoul,它们分别用于生成哺乳动物的躯体与灵魂。像这种被我们拿来用于 new 出具体对象的类,叫做具体产品类(ConcreteProduct)。具体产品类往往不会孤立存在,不同的具体产品类往往有着共同的功能,比如哺乳动物的躯体和爬行动物的躯体,虽身体中有着不同的构造,带起码都有个壳。因此我们可以用一个

抽象产品(AbstractProduct)类

来声明这一类产品应该具有的基本功能。

// 定义操作系统这类产品的抽象产品类
class Body {
  walking() {
    throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
  }
}

// 定义具体操作系统的具体产品类
class MammalsBody extends Body {
  walking() {
    console.log('我会用哺乳动物的方式行走')
  }
}

class reptilesBody extends Body {
  walking() {
    console.log('我会用爬行动物的方式行走')
  }
}

生产’灵魂’也是同理,这里就不重复了。

// 定义灵魂的抽象类
class Soul {
  // 灵性
  spiritual() {
    throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
  }
}

// 定义具体操作系统的具体产品类
class MammalsSoul extends Soul {
  spiritual() {
    console.log('我具有哺乳动物的灵性')
  }
}

class reptilesSoul extends Soul {
  spiritual() {
    console.log('我具有爬行动物的灵性')
  }
}

如此一来,当我们需要生产一个哺乳动物时,我们只需要:

// 哺乳动物
const Mammals = new Mammals()

const myMammals = {}
// 让它拥有躯体
myMammals.body = Mammals.createBody()
// 让它拥有灵魂
myMammals.soul = Mammals.createSoul()

当之后需要写一个新的物种,则不需要对动物工厂AnimalFactory做任何修改,只需要拓展它的种类:

class 火星某动物 extends AnimalFactory {
  createBody() {
    // 此种动物躯体
  }
  createSoul() {
    // 此种动物灵魂
  }
}

这么个操作,对

原有的系统不会造成任何潜在影响

所谓的“

对拓展开放,对修改封闭

”就这么圆满实现了。

抽象工厂和简单工厂有哪些异同?

共同点:在于都

尝试去分离一个系统中变与不变的部分

不同点:

场景的复杂度

抽象工厂模式的定义,是

围绕一个超级工厂创建其他工厂

,对一些工作经验少的同学来说可能较难理解,但目前来说在JS世界里也应用得并不广泛,所以大家不必拘泥于细节,只需对“开放封闭原则”形成自己的理解,知道它好在哪,知道执行它的必要性。



单例模式

保证

一个类仅有一个实例

,并提供一个访问它的全局访问点,这样的模式就叫做单例模式。


意图:

保证一个类仅有一个实例,并提供一个访问它的全局访问点。


主要解决:

一个全局使用的类频繁地创建与销毁。


如何解决:

判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

单例模式是设计模式中相对较为容易理解、容易上手的一种模式,同时因为其具有广泛的应用场景,也是

面试题里的常客

一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象。像这样:

class SingleDog {
  show() {
    console.log('俺是一个单例对象')
  }
}
const s1 = new SingleDog()
const s2 = new SingleDog()

s1 === s2 // false

在这里插入图片描述

class SingleDog {
  show() {
    console.log('俺是一个单例对象')
  }
}
const s1 = new SingleDog()
const s2 = new SingleDog()

s1 === s2 // false

楼上我们先 new 创建了一个 s1,又 new 创建了一个 s2, s1与s2显然是没有任何联系的,两者各占一块内存空间,单例模式想要做到的是,无论创建多少次,它都只返回第一次所创建的那个实例。

要做到这一点,就需要构造函数

具备判断自己是否已经创建过一个实例

的能力。我们现在把这段判断逻辑写成一个静态方法(其实也可以直接写入构造函数的函数体里):

class SingleDog {
  show() {
    console.log('俺是一个单例对象')
  }
  static getInstance() {
    // 判断是否已经new过1个实例
    if (!SingleDog.instance) {
      // 若这个唯一的实例不存在,那么先创建它
      SingleDog.instance = new SingleDog()
    }
    // 如果这个唯一的实例已经存在,则直接返回
    return SingleDog.instance
  }
}
const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

s1 === s2 // true

在这里插入图片描述

除了楼上这种实现方式之外,getInstance的逻辑还可以用

闭包

来实现:

SingleDog.getInstance = (function () {
  // 定义自由变量instance,模拟私有变量
  let instance = null
  return function () {
    // 判断自由变量是否为null
    if (!instance) {
      // 如果为null则new出唯一实例
      instance = new SingleDog()
    }
    return instance
  }
})()

const s1 = SingleDog.getInstance()
const s2 = SingleDog.getInstance()

s1 === s2 // true

在这里插入图片描述

可以看出,在getInstance方法的判断和拦截下,我们不管调用多少次,SingleDog都只会给我们返回一个实例,s1和s2现在都指向这个唯一的实例。



实现一个简易Storage


生产实践

:redux、vuex中的Store,或者我们经常使用的Storage都是单例模式。

来实现一下简易Storage:

class Storage {
  static getInstance() {
    if (!Storage.instance) {
      Storage.instance = new Storage();
    }
    return Storage.instance;
  }
  getItem(key) {
    return localStorage.getItem(key);
  }
  setItem(key, value) {
    return localStorage.setItem(key, value);
  }
}

const storage1 = Storage.getInstance()
const storage2 = Storage.getInstance()
storage1.setItem('name', '小明')
storage1.getItem('name') // 小明
storage2.getItem('name') // 小明
storage1 === storage2 // true

在这里插入图片描述


优点

  • 划分命名空间,减少全局变量
  • 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
  • 且只会实例化一次。简化了代码的调试和维护


缺点

  • 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。


场景例子

  • 定义命名空间和实现分支型方法
  • 登录框
  • vuex 和 redux中的store


结构型


装饰器模式

在我们的开发过程中我们会为了一些通用功能在多个不同的组件、接口或者类中使用,这个时候我们这些功能写到每个组件、接口或者类中,但是这样非常不利于维护。

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。

理解了装饰器的能解决了什么问题,那我们在什么情况下考虑使用装饰器模式呢?我的理解是:

  • 需要扩展一个类,为这个类附加一个方法或者属性的时候;
  • 需要修改一个类的功能,或者重构这个类中的某个方法;


如何定义装饰器

装饰器本质是一个函数,可以分为带参数和不带参数(也叫装饰器工厂),装饰器使用 @expression这种形式,expression 求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

@Test()
class Hello { }

function Test(target) {
  console.log("I am decorator.")
}


装饰器类型


类装饰器

一般主要应用于类构造函数,可以监视、修改、替换类的定义,装饰器用来装饰类的时候。装饰器函数的第一个参数,就是所要装饰的目标类本身。

a、添加静态属性或方法

@Test()
class Hello { }

function Test(target) {
  target.a = 1;
}

let o = new Hello();

console.log(o.a) ==> 1

b、添加实例属性或方法

@Test()
class Hello { }

function Test(target) {
  target.prototype.a = 1;
  target.prototype.f = function () {
    console.log("新增加方法")
  };

}

let o = new Hello();
o.f() ==> "新增加方法"
console.log(o.a) ==> 1

c、装饰器工厂(函数柯里化)

@Test('hello')
class Hello { }

function Test(str) {
  return function () {
    target.prototype.a = str;
    target.prototype.f = function () {
      console.log(str)
    };
  }

}

let o = new Hello();
o.f() ==> "hello"
console.log(o.a) ==> "hello"

d、重载构造函数

@Test('hello')
class Hello {
  constructor() {
    this.a = 1
  }
  f() {
    console.log('我是原始方法', this.a)
  }

}

function Test(target) {
  return class extends target {
    f() {
      console.log('我是装饰器方法', this.a)
    }
  }

}

let o = new Hello();
o.f() ==> "我是装饰器方法", 1



适配器模式

适配器模式通过

把一个类的接口变换成客户端所期待的另一种接口

,可以帮我们解决

不兼容

的问题。

最简单的适配器:

适配器模式没有想象中的那么复杂,举个最简单的例子。 客户端调用一个方法进行加法计算:

const result = add(1,2);

但是我们没有提供add这个方法,提供了同样类似功能的sum方法:

function sum (v1,v2){
  return v1 + v2;
}

为了避免修改客户端和服务端,我们增加一个包装函数:

function add(v1, v2) {
  return sum(v1, v2)
}

这就是一个最简单的适配器模式,

我们在两个不兼容的接口之间添加一个包装方法,用这个方法来连接二者使其共同工作。


总结:

适配器模式的原理很简单,就是新增一个包装类,对新的接口进行包装以适应旧代码的调用,避免修改接口和调用代码。



代理模式

代理模式:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

在生活中,代理模式的场景是十分常见的,例如我们现在如果有租房、买房的需求,更多的是去找链家等房屋中介机构,而不是直接寻找想卖房或出租房的人谈。此时,链家起到的作用就是代理的作用。链家和他所代理的客户在租房、售房上提供的方法可能都是一致的(收钱,签合同),可是链家作为代理却提供了访问限制,让我们不能直接访问被代理的客户。

事件代理,可能是代理模式最常见的一种应用方式,也是一道实打实的高频面试题。它的场景是一个父元素下有多个子元素,像这样:

<body>
  <div id="father">
    <a href="#">链接1号</a>
    <a href="#">链接2号</a>
    <a href="#">链接3号</a>
    <a href="#">链接4号</a>
    <a href="#">链接5号</a>
    <a href="#">链接6号</a>
  </div>
</body>

我们现在的需求是,希望鼠标点击每个 a 标签,都可以弹出“我是xxx”这样的提示。比如点击第一个 a 标签,弹出“我是链接1号”这样的提示。这意味着我们至少要安装 6 个监听函数给 6 个不同的的元素(一般我们会用循环,代码如下所示),如果我们的 a 标签进一步增多,那么性能的开销会更大。

// 假如不用代理模式,我们将循环安装监听函数
const aNodes = document.getElementById('father').getElementsByTagName('a')

const aLength = aNodes.length

for (let i = 0; i < aLength; i++) {
  aNodes[i].addEventListener('click', function (e) {
    e.preventDefault()
    alert(`我是${aNodes[i].innerText}`)
  })
}

考虑到事件本身具有“冒泡”的特性,当我们点击 a 元素时,点击事件会“冒泡”到父元素 div 上,从而被监听到。如此一来,点击事件的监听函数只需要在 div 元素上被绑定一次即可,而不需要在子元素上被绑定 N 次——这种做法就是事件代理,它可以很大程度上提高我们代码的性能。


事件代理的实现

用代理模式实现多个子元素的事件监听,代码会简单很多:

// 获取父元素
const father = document.getElementById('father')

// 给父元素安装一次监听函数
father.addEventListener('click', function (e) {
  // 识别是否是目标子元素
  if (e.target.tagName === 'A') {
    // 以下是监听函数的函数体
    e.preventDefault()
    alert(`我是${e.target.innerText}`)
  }
})

在这种做法下,我们的点击操作并不会直接触及目标子元素,而是由父元素对事件进行处理和分发、间接地将其作用于子元素,因此这种操作从模式上划分属于代理模式。


Proxy

Vue2升级到Vue3的核心

es6增建了

MDN Proxy

对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等),


总结

  • 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
  • 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
  • 无论是出于什么目的,这种模式的套路就只有一个—— A 不能直接访问 B,A 需要借助一个帮手来访问 B,这个帮手就是代理器。需要代理器出面解决的问题,就是代理模式发光发热的应用场景。


行为型


观察者模式

当对象间存在一对多关系时,则使用观察者模式。让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。


生活中的观察者模式

例子1:过年期间,老板说好在大年30当晚发红包,到了当天晚上,大家都已经做好了

抢红包

的准备,时刻等待着红包的降临。这个观察红包的过程,就是一个典型的

观察者模式

例子2:女神朋友圈官宣新男友。各位潜藏备胎纷纷失恋。


模式特征

  1. 一个目标者对象

    Subject

    ,拥有方法:添加 / 删除 / 通知

    Observer

  2. 多个观察者对象

    Observer

    ,拥有方法:接收

    Subject

    状态变更通知并处理;
  3. 目标对象

    Subject

    状态变更时,通知所有

    Observer


Subject

添加一系列

Observer



Subject

负责维护与这些

Observer

之间的联系,“你对我有兴趣,我更新就会通知你”。

// 目标者类
class Subject {
  constructor() {
    this.observers = [];  // 观察者列表
  }
  // 添加
  add(observer) {
    this.observers.push(observer);
  }
  // 删除
  remove(observer) {
    let idx = this.observers.findIndex(item => item === observer);
    idx > -1 && this.observers.splice(idx, 1);
  }
  // 通知
  notify() {
    for (let observer of this.observers) {
      observer.update();
    }
  }
}

// 观察者类
class Observer {
  constructor(name) {
    this.name = name;
  }
  // 目标对象更新时触发的回调
  update() {
    console.log(`她发消息了,我是:${this.name}`);
  }
}

// 实例化目标者
let subject = new Subject();

// 实例化两个观察者
let obs1 = new Observer('男生A');
let obs2 = new Observer('男生B');

// 向目标者添加观察者
subject.add(obs1);
subject.add(obs2);

// 目标者通知更新
subject.notify();  
// 输出:
// 她发消息了,我是男生A
// 她发消息了,我是男生B

在这里插入图片描述


优点

  • 目标者与观察者,功能耦合度降低,专注自身功能逻辑;
  • 观察者被动接收更新,时间上解耦,实时接收目标者更新状态。


缺点

  • 过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解


迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。 ——《设计模式:可复用面向对象软件的基础》


迭代器模式其实就是为了让一切皆可遍历。


特点

  • 为遍历不同数据结构的 “集合” 提供统一的接口;
  • 能遍历访问 “集合” 数据中的项,不关心项的数据结构


实现

遍历作为一种合理、高频的使用需求,几乎没有语言会要求它的开发者手动去实现。在JS中,本身也内置了一个比较简陋的数组迭代器的实现——Array.prototype.forEach,我们这里来实现一个简易的forEach

// 统一遍历接口实现
var each = function (arr, callBack) {
  for (let i = 0, len = arr.length; i < len; i++) {
    // 将值,索引返回给回调函数callBack处理
    if (callBack(i, arr[i]) === false) {
      break;  // 中止迭代器,跳出循环
    }
  }
}

// 外部调用
each([1, 2, 3, 4, 5], function (index, value) {
  if (value > 3) {
    return false; // 返回false中止each
  }
  console.log([index, value]);
})

// 输出:[0, 1]  [1, 2]  [2, 3]

在这里插入图片描述

“迭代器模式的核心,就是实现统一遍历接口。”


模式细分

  1. 内部迭代器 (jQuery 的 $.each / for…of)
  2. 外部迭代器 (ES6 的 yield)


1. 内部迭代器

内部迭代器: 内部定义迭代规则,控制整个迭代过程,外部只需一次初始调用

// jQuery 的 $.each(跟上文each函数实现原理类似)
$.each(['小明', '小红', '小蓝'], function(index, value) {
    console.log([index, value]);
});

// 输出:[0, 小明]  [1, 小红]  [2, 小蓝]


优点

:调用方式简单,外部仅需一次调用


缺点

:迭代规则预先设置,欠缺灵活性。无法实现复杂遍历需求(如: 同时迭代比对两个数组)


2. 外部迭代器

外部迭代器: 外部显示(手动)地控制迭代下一个数据项

借助 ES6 新增的

Generator

函数中的

yield*

表达式来实现外部迭代器。

// ES6 的 yield 实现外部迭代器
function* generatorEach(arr) {
  for (let [index, value] of arr.entries()) {
    yield console.log([index, value]);
  }
}

let each = generatorEach(['Angular', 'React', 'Vue']);
each.next();
each.next();
each.next();

// 输出:[0, 'Angular']  [1, 'React']  [2, 'Vue']

在这里插入图片描述


优点

:灵活性更佳,适用面广,能应对更加复杂的迭代需求


缺点

:需显示调用迭代进行(手动控制迭代过程),外部调用方式较复杂


适用场景

不同数据结构类型的 “数据集合”,需要对外提供统一的遍历接口,而又不暴露或修改内部结构时,可应用迭代器模式实现。


特点

  • 访问一个聚合对象的内容而无需暴露它的内部表示。
  • 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作


总结

对于集合内部结果常常变化各异,不想暴露其内部结构的话,但又想让客户代码透明的访问其中的元素,可以使用迭代器模式