欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

掌握JavaScript的23种设计模式,让你的代码更高效!

最编程 2024-08-04 21:04:55
...

这是本人写的第二篇博客,在开始动笔前就有点后悔了,和我的第一篇博客选话题一样有点大,导致动辄上万字非常的耗时耗力,最后总算坚持下来了,下次吸取教训针对某个小知识深入研究的文章。望各位多多支持,有问题欢迎一起讨论,你们的支持是我最大的动力。

一、设计模式介绍

什么是设计模式

  • 设计模式是解决问题的一种思想,和语言无关。在面向对象软件设计的工程中,针对特定的问题简洁优雅的一种解决方案。通俗一点的说,设计模式就是符合某种场景下某个问题的解决方案,通过设计模式可以增加代码的可重用性,可扩展性,可维护性,最终使得我们的代码高内聚、低耦合。

设计模式的五大设计原则

  • 单一职责:一个程序只需要做好一件事。如果功能过于复杂就拆分开,保证每个部分的独立
  • 开放封闭原则:对扩展开放,对修改封闭。增加需求时,扩展新代码,而不是修改源代码。这是软件设计的终极目标。
  • 里氏置换原则:子类能覆盖父类,父类能出现的地方子类也能出现。
  • 接口独立原则:保持接口的单一独立,避免出现“胖接口”。这点目前在TS中运用到。
  • 依赖倒置原则:面向接口编程,依赖于抽象而不依赖于具体。使用方只专注接口而不用关注具体类的实现。俗称“鸭子类型”

设计模式的三大类

  • 创建型:工厂模式,抽象工厂模式,建造者模式,单例模式,原型模式
  • 结构型:适配器模式,装饰器模式,代理模式,外观模式,桥接模式,组合模式,享元模式
  • 行为型:策略模式,模板方法模式,发布订阅模式,迭代器模式,职责链模式,命令模式,备忘录模式,状态模式,访问者模式,中介者模式,解释器模式。

二、设计模式

1.工厂模式

  • 工厂模式是用来创建对象的常见设计模式,在不暴露创建对象的具体逻辑,而是将逻辑进行封装,那么它就可以被称为工厂。工厂模式又叫做静态工厂模式,由一个工厂对象决定创建某一个类的实例。

优点

  1. 调用者创建对象时只要知道其名称即可
  2. 扩展性高,如果要新增一个产品,直接扩展一个工厂类即可。
  3. 隐藏产品的具体实现,只关心产品的接口。

缺点

  • 每次增加一个产品时,都需要增加一个具体类,这无形增加了系统内存的压力和系统的复杂度,也增加了具体类的依赖

例子

  • 一个服装厂可以生产不同类型的衣服,我们通过一个工厂方法类来模拟产出

class DownJacket {
  production(){
    console.log('生产羽绒服')
  }
}
class Underwear{
  production(){
    console.log('生产内衣')
  }
}
class TShirt{
  production(){
    console.log('生产t恤')
  }
}
// 工厂类
class clothingFactory {
  constructor(){
    this.downJacket = DownJacket
    this.underwear = Underwear
    this.t_shirt = TShirt
  }
  getFactory(clothingType){
    const _production = new this[clothingType]
    return _production.production()
  }
}
const clothing = new clothingFactory()
clothing.getFactory('t_shirt')// 生产t恤

2.抽象工厂模式

  • 抽象工厂模式就是通过类的抽象使得业务适用于一个产品类簇的创建,而不负责某一个类产品的实例。抽象工厂可以看作普通工厂的升级版,普通工厂以生产实例为主,而抽象工厂的目就是生产工厂。

优点

  • 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。

缺点

  • 产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。

例子

  • 同样基于上面的例子,模拟出一个抽象类,同时约束继承子类的方法实现。最后再通过工厂函数返回指定的类簇
/* 抽象类
js中abstract是个保留字,实现抽象类只能通过new.target进行验证,
防止抽象类被直接实例,另外如果子类没有覆盖指定方法,则抛出错误
*/
class ProductionFlow {
  constructor(){
    if(new.target === ProductionFlow){
      throw new Error('抽象类不能被实例')
    }
  }
  production(){
    throw new Error('production要被重写')
  }
  materials(){
    throw new Error('materials要被重写')
  }
}
class DownJacket extends ProductionFlow{
  production(){
    console.log(`材料:${this.materials()},生产羽绒服`)
  }
  materials(){
    return '鸭毛'
  }
}
class Underwear extends ProductionFlow{
  production(){
    console.log(`材料:${this.materials()},生产内衣`)
  }
  materials(){
    return '丝光棉'
  }
}
class TShirt extends ProductionFlow{
  production(){
    console.log(`材料:${this.materials()},生产t恤`)
  }
  materials(){
    return '纯棉'
  }
}

function getAbstractProductionFactory(clothingType){
  const clothingObj = {
    downJacket:DownJacket,
    underwear:Underwear,
    t_shirt:TShirt,
  }
  if(clothingObj[clothingType]){
    return clothingObj[clothingType]
  }
  throw new Error(`工厂暂时不支持生产这个${clothingType}类型的服装`)
}

const downJacketClass = getAbstractProductionFactory('downJacket')
const underwearClass = getAbstractProductionFactory('underwear')

const downJacket = new downJacketClass()
const underwear = new underwearClass()
downJacket.production() // 材料:鸭毛,生产羽绒服
underwear.production() // 材料:丝光棉,生产内衣

3.建造者模式

  • 建造者模式是一种比较复杂使用频率较低的创建型设计模式,建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。主要用于将一个复杂对象的构建与他的表现分离,使得同样的构建过程可以创建不同的表示。

优点

  1. 建造者独立易扩展
  2. 方便控制细节风险

缺点

  1. 产品必须有共同点,范围有限制
  2. 当内部有变化复杂时,会有很多建造类

例子

下面继续用服装厂的生产流程作为例子。

// 抽象类
class Clothing {
  constructor() {
    this.clothingType = ''
    this.price
  }
}

class Underwear extends Clothing {
  constructor() {
    super()
    this.clothingType = 'underwear'
    this.price = 10
  }
}

class TShirt extends Clothing {
  constructor() {
    super()
    this.clothingType = 't_shirt'
    this.price = 50
  }
}

class DownCoat extends Clothing {
  constructor() {
    super()
    this.clothingType = 'DownCoat'
    this.price = 500
  }
}


// 产品
class Purchase {
  constructor() {
    this.clothings = []
  }
  addClothing(clothing) {
    this.clothings.push(clothing)
  }
  countPrice() {
    return this.clothings.reduce((prev, cur)=>cur.price + prev,0)
  }
}

// 厂长
class FactoryManager {
  createUnderwear() {
    throw new Error(`子类必须重写 createUnderwear`)
  }
  createTShirt() {
    throw new Error(`子类必须重写 createTShirt`)
  }
  createDownCoat() {
    throw new Error(`子类必须重写 DownCoat`)
  }
}

// 工人
class Worker extends FactoryManager {
  constructor() {
    super()
    this.purchase = new Purchase()
  }
  createUnderwear(num) {
    for (let i = 0; i < num; i++) {
      this.purchase.addClothing(new Underwear())
    }
  }
  createTShirt(num) {
    for (let i = 0; i < num; i++) {
      this.purchase.addClothing(new TShirt())
    }
  }
  createDownCoat(num) {
    for (let i = 0; i < num; i++) {
      this.purchase.addClothing(new DownCoat())
    }
  }
}

// 销售
class Salesman {
  constructor() {
    this.worker = null
  }
  setWorker(worker) {
    this.worker = worker
  }
  reserve(clothing) {
    clothing.forEach((item) => {
      if (item.type === 'underwear') {
        this.worker.createUnderwear(item.num)
      } else if (item.type === 't_shirt') {
        this.worker.createTShirt(item.num)
      } else if (item.type === 'DownCoat') {
        this.worker.createDownCoat(item.num)
      } else {
        try {
          throw new Error('公司暂不生产或不存在该类型的商品')
        } catch (error) {
          console.log(error)
        }
      }
    });

    const purchase = this.worker.purchase
    return purchase.countPrice()
  }
}

const salesman = new Salesman()
const worker = new Worker()
salesman.setWorker(worker)
const order = [
  {
    type: 'underwear',
    num: 10
  },
  {
    type: 't_shirt',
    num: 4
  },
  {
    type: 'DownCoat',
    num: 1
  }
]
console.log(`本次订单所需金额:${salesman.reserve(order)}`)

4.单例模式

  • 单例模式的思路是:保证一个类只能被实例一次,每次获取的时候,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回。
  • 单例模式的核心就是创建一个唯一的对象,而在javascript中创建一个唯一的对象太简单了,为了获取一个对象而去创建一个类有点多此一举。如const obj = {}obj就是独一无二的一个对象,在全局作用域的声明下,可以在任何地方对它访问,这就满足了单例模式的条件。

优点

  1. 内存中只有一个实例,减少了内存的开销。
  2. 避免了对资源多重的占用。

缺点

  • 违反了单一职责,一个类应该只关心内部逻辑,而不用去关心外部的实现

例子

  • 我们常见到的登录弹窗,要么显示要么隐藏,不可能同时出现两个弹窗,下面我们通过一个类来模拟弹窗。
class LoginFrame {
    static instance = null
    constructor(state){
        this.state = state
    }
    show(){
        if(this.state === 'show'){
            console.log('登录框已显示')
            return
        }
        this.state = 'show'
        console.log('登录框展示成功')
    }
    hide(){
        if(this.state === 'hide'){
            console.log('登录框已隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
    // 通过静态方法获取静态属性instance上是否存在实例,如果没有创建一个并返回,反之直接返回已有的实例
    static getInstance(state){
        if(!this.instance){
            this.instance = new LoginFrame(state)
        }
        return this.instance
    }
}
const p1 = LoginFrame.getInstance('show')
const p2 = LoginFrame.getInstance('hide')
console.log(p1 === p2) // true

5.适配器模式

  • 适配器模式的目的是为了解决对象之间的接口不兼容的问题,通过适配器模式可以不更改源代码的情况下,让两个原本不兼容的对象在调用时正常工作。

优点

  • 让任何两个没有关联的类可以同时有效运行,并且提高了复用性、透明度、以及灵活性

缺点

  • 过多的使用适配器模式,会让系统变得零乱,不易整体把控。建议在无法重构的情况下使用适配器。

例子

  • 拿一个现实中的例子来说,杰克只会英语,小明只会中文,它们在交流上出现了障碍,小红同时会中英双语,通过小红将杰克的英语翻译成中文,让小明和杰克进行无障碍的沟通,这里小红就起到了适配器的角色。
class Jack {
  english() {
    return 'I speak English'
  }
}
class Xiaoming {
  chinese() {
    return '我只会中文'
  }
}
// 适配器
class XiaoHong {
  constructor(person) {
    this.person = person
  }
  chinese() {
    return `${this.person.english()} 翻译: "我会说英语"`
  }
}
class Communication {
  speak(language) {
    console.log(language.chinese())
  }
}

const xiaoming = new Xiaoming()
const xiaoHong = new XiaoHong(new Jack())
const communication = new Communication()
communication.speak(xiaoming)
communication.speak(xiaoHong)

6.装饰器模式

  • 装饰者模式能够在不更改源代码自身的情况下,对其进行职责添加。相比于继承装饰器的做法更轻巧。通俗的讲我们给心爱的手机上贴膜,带手机壳,贴纸,这些就是对手机的装饰。

优点

  • 装饰类和被装饰类它们之间可以相互独立发展,不会相互耦合,装饰器模式是继承的一个替代模式,它可以动态的扩展一个实现类的功能。

缺点

  • 多层的装饰会增加复杂度

例子

  • 在编写飞机大战的游戏中,飞机对象的攻击方式只有普通子弹攻击,如何在不更改原代码的情况下,为它其他的攻击方式,如激光武器,导弹武器?
class Aircraft {
    ordinary(){
        console.log('发射普通子弹')
    }
}
class AircraftDecorator {
    constructor(aircraft){
        this.aircraft = aircraft
    }
    laser(){
        console.log('发射激光')
    }
    guidedMissile(){
        console.log('发射导弹')
    }
    ordinary(){
        this.aircraft.ordinary()
    }
}
const aircraft = new Aircraft()
const aircraftDecorator = new AircraftDecorator(aircraft)
aircraftDecorator.ordinary() // 发射普通子弹
aircraftDecorator.laser() // 发射激光
aircraftDecorator.guidedMissile() // 发射导弹
// 可以看到在不更改源代码的情况下对它进行了装饰扩展

7.代理模式

  • 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际*问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
  • 代理和本体接口需要一致性,代理和本体之间可以说是鸭子类型的关系,不在乎他怎么实现的,只要它们之间暴露的方法一致既可。

优点

  • 职责清晰,高扩展性,智能化

缺点

  • 当对象和对象之间增加了代理可能会影响到处理的速度。
  • 实现代理需要额外的工作,有些代理会非常的复杂。

例子

  • 我们都知道,领导拥有公司的最高权限,假设公司有员工100个,如果每个人都去找领导去处理事务,那领导肯定会崩溃,因此领导招聘了一个秘书帮他收集整理事务,秘书会在合适时间一次性将需要处理的业务交给老板处理,在这里秘书就是领导的一个代理角色。
// 员工
class Staff {
  constructor(affairType){
    this.affairType = affairType
  }
  applyFor(target){
    target.receiveApplyFor(this.affairType)
  }
}
// 秘书
class Secretary {
  constructor(){
    this.leader = new Leader()
  }
  receiveApplyFor(affair){
    this.leader.receiveApplyFor(affair)
  }
}
//领导
class Leader {
  receiveApplyFor(affair){
    console.log(`批准:${affair}`)
  }
}

const staff = new Staff('升职加薪')
staff.applyFor(new Secretary()) // 批准:升职加薪

8.外观模式

  • 外观模式本质就是封装交互,隐藏系统的复杂性,提供一个可以访问的接口。由一个将子系统一组的接口集成在一起的高层接口,以提供一个一致的外观,减少外界与多个子系统之间的直接交互,从而更方便的使用子系统。

优点

  • 减少系统的相互依赖,以及安全性和灵活性

缺点

  • 违反开放封闭原则,有变动的时候更改会非常麻烦,即使继承重构都不可行。

例子

  • 外观模式经常被用于处理高级游览器的和低版本游览器的一些接口的兼容处理
function addEvent(el,type,fn){
    if(el.addEventlistener){// 高级游览器添加事件DOM API
        el.addEventlistener(type,fn,false)
    }else if(el.attachEvent){// 低版本游览器的添加事件API
        el.attachEvent(`on${type}`,fn)
    }else {//其他
        el[type] = fn
    }
}
  • 另一种场景,在某个函数中的某个参数可传可不传的情况下,通过函数重载的方式,让传参更灵活。
function bindEvent(el,type,selector,fn){
    if(!fn){
        fn = selector
    }
    // 其他代码
    console.log(el,type,fn)
}
bindEvent(document.body,'click','#root',()=>{})
bindEvent(document.body,'click',()=>{})

9.发布订阅模式

  • 发布订阅又称观察者模式,它定义对象之间的1对N的依赖关系,当其中一个对象发生变化时,所有依赖于它的对象都会得到通知。
  • 发布订阅模式经常出现在我们的工作场景中,如:当你给DOM绑定一个事件就已经使用了发布订阅模式,通过订阅DOM上的click事件,当被点击时会向订阅者发布消息。

优点

  • 观察者和被观察者它们之间是抽象耦合的。并且建立了触发机制。

缺点

  • 当订阅者比较多的时候,同时通知所有的订阅者可能会造成性能问题。
  • 在订阅者和订阅目标之间如果循环引用执行,会导致崩溃。
  • 发布订阅模式没有办法提供给订阅者所订阅的目标它是怎么变化的,仅仅只知道它变化了。

例子

  • 比喻前段时间的冬奥会,项目还没有开始的时候可以提前预定,等到项目快开始的时,APP会提前给我们发送通知即将开始的项目,而没到时间的不通知,另外在项目还没有开始的时候,可以取消订阅避免接受到通知。根据这个需求我们来写一个例子吧
class Subject {
  constructor(){
    this.observers = {}
    this.key = ''
  }
  add(observer){
    const key = observer.project
    if (!this.observers[key]) {
      this.observers[key] = []
    }
    this.observers[key].push(observer)
  }
  remove(observer){
    const _observers = this.observers[observer.project]
    console.log(_observers,11)
    if(_observers.length){
      _observers.forEach((item,index)=>{
        if(item === observer){
          _observers.splice(index,1)
        }
      })
    }
  }
  setObserver(subject){
    this.key = subject
    this.notifyAllObservers()
  }
  notifyAllObservers(){
    this.observers[this.key].forEach((item,index)=>{
      item.update()
    })
  }
}

class Observer {
  constructor(project,name) {
    this.project = project
    this.name = name
  }
  update() {
    console.log(`尊敬的:${this.name} 你预约的项目:【${this.project}】 马上开始了`)
  }
}

const subject = new Subject()
const xiaoming = new Observer('滑雪','xiaoming')
const A = new Observer('大跳台','A')
const B = new Observer('大跳台','B')
const C = new Observer('大跳台','C')
subject.add(xiaoming)
subject.add(A)
subject.add(B)
subject.add(C)
subject.remove(B) // 取消订阅
subject.setObserver('大跳台')
/** 执行结果
 * 尊敬的:A 你预约的项目:【大跳台】 马上开始了
 * 尊敬的:C 你预约的项目:【大跳台】 马上开始了
 */

10.迭代器模式

  • 迭代器模式是指提供一种方法顺序访问一个聚合对象中的每个元素,并且不需要暴露该对象的内部。

优点

  • 它支持以不同的方式遍历一个聚合对象。
  • 迭代器简化了聚合类。在同一个聚合上可以有多个遍历。
  • 在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。

缺点

  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

例子

  • 迭代器分为内部迭代器和外部迭代器,它们有各自的适用场景。
  1. 内部迭代器
// 内部迭代器表示内部已经定义好了迭代规则,它完全接受整个迭代过程,外部只需一次初始调用。
Array.prototype.MyEach = function(fn){
    for(let i = 0;i<this.length;i++){
        fn(this[i],i,this)
    }
}
Array.prototype.MyEach = function(fn){
    for(let i = 0;i<this.length;i++){
        fn(this[i],i,this)
    }
}
[1,2,3,4].MyEach((item,index)=>{
    console.log(item,index)
})
  1. 外部迭代器
// 外部迭代器必须显示的迭代下一个元素。它增加了调用的复杂度,但也增加了迭代器的灵活性,可以手动控制迭代的过程。
class Iterator{
    constructor(arr){
        this.current = 0
        this.length = arr.length
        this.arr = arr
    }
    next(){
        return this.getCurrItem()
    }
    isDone(){
        return this.current>=this.length
    }
    getCurrItem(){
        return {
            done:this.isDone(),
            value:this.arr[this.current++]
        }
    }
}
let iterator =new Iterator([1,2,3])
while(!(item=iterator.next()).done) { 
    console.log(item) 
}
iterator.next()
/* 下面的数据格式是不是有点熟悉
{done: false, value: 1}
{done: false, value: 2}
{done: false, value: 3}
{done: true, value: undefined}
*/

11.状态模式

  • 允许一个对象在其内部状态改变的时候改变其行为,对象看起来似乎修改了它的类,通俗一点的将就是记录一组状态,每个状态对应一个实现,实现的时候根据状态去运行实现。

优点

  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。

缺点

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对"开闭原则"的支持并不太好,对切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。

例子

  • lol中的瑞文的Q有三段攻击,同一个按键,在不同的状态下,攻击的行为不同。通常情况下,我们通过if...else也可以实现,但是这样明显不利于扩展,违反了开放封闭原则。接下来用代码来描述这种场景。
class State {
  constructor(attack){
    this.attack = attack
  }
  handle(context){
    console.log(this.attack)
    context.setState(this)
  }
}

class Context {
  constructor(){
    this.state = null
  }
  getState(){
    return this.state
  }
  setState(state){
    this.state = state
  }
}
const q1 = new State('q1 第1击'),
      q2 = new State('q2 第2击'),
      q3 = new State('q3 第3击'),
      context = new Context()
      
q1.handle(context)//q1 第1击
q2.handle(context)//q2 第2击
q3.handle(context)//q3 第3击

12.策略模式

  • 策略模式指的是定义一系列算法,把他们一个个封装起来,目的就是将算法的使用和算法的实现分离开来。同时它还可以用来封装一系列的规则,比如常见的表单验证规则,只要这些规则指向的目标一致,并且可以被替换使用,那么就可以用策略模式来封装它们。

优点

  • 算法可以*切换,避免了使用多层条件判断,增加了扩展性

缺点

  • 策略类增多,所有策略类都需要对外暴露。

例子

  • 刚入这个行业的时候,写表单验证经常无止境的if...else写法,意识到这种写法不靠谱,于是我把检验规则放在一个对象中,在函数中对它进行控制,把规则与实现进行了分离,每次只需要在封装的规则中去修改配置。在后面的多种场景都用这种方法,解决了频繁使用if...else的问题,当第一次接触倒策略模式才知道这种写法也算策略模式。
const rules = {
    cover_img: {
        must: false,
        msg: '请上传封面图片',
        val: ''
    },
    name: {
        must: true,
        msg: '姓名不能为空',
        val: ''
    },
    sex: {
        must: true,
        msg: '请填写性别',
        val: ''
    },
    birthday: {
        must: false,
        msg: '请选择生日',
        val: ''
    },
}
function verify(){
    for(const key in rules){
        if(rules[key].must&&!rules[key].val){
            console.log(rules[key].msg)
        }
    }
}
verify()
// 姓名不能为空
// 请填写性别
  • 上面的例子是以js方式写的,在javascript将函数作为一等公民的语言里,策略模式就是隐形的,它已经融入到了javascript的语言中,所以以javascript方式的策略模式会显得简单直接。不过我们依然要了解传统的策略模式,下面来看看传统的策略模式的例子。
//html-----------------
<form action="http:// xxx.com/register" id="registerForm" method="post">
    请输入用户名:<input type="text" name="userName" />
    请输入密码:<input type="text" name="password" />
    请输入手机号码:<input type="text" name="phoneNumber" />
    <button>提交</button>
</form>
// js------------------
class Strategies {
  constructor() {
    this.rules = {}
  }
  add(key, rule) {
    this.rules[key] = rule
    return this
  }
}

class Validator {
  constructor(strategies) {
    this.cache = [] // 保存检验规则
    this.strategies = strategies
  }
  add(dom, rules) {
    rules.forEach((rule) => {
      const strategyAry = rule.strategy.split(':')
      this.cache.push(() => {
        const strategy = strategyAry.shift()
        strategyAry.unshift(dom.value)
        strategyAry.push(rule.errorMsg)
        console.log(this.strategies[strategy])
        return this.strategies[strategy].apply(dom, strategyAry)
      })
    });
  }
  start() {
    for (let i = 0,validatorFunc; validatorFunc =this.cache[i++]; ) {
      const msg = validatorFunc()
      if (msg) {
        return msg
      }
    }
  }
}
const registerForm = document.getElementById('registerForm') // 获取formDom节点
const strategies = new Strategies()
strategies.add('isNonEmpty', function(value, errorMsg) {
  if (!value) {
    return errorMsg
  }
}).add('minLength', function(
						

上一篇: 探索无限可能:开源神经网络架构与引擎

下一篇: 超强AI性能!骁龙8让你轻松应对各种任务

推荐阅读