JavaScript 中代理模式的威力
我在职业生涯后期学到的一个更有趣的模式是Proxy。
在查找代理模式的示例时,你可能会经常看到不同的实现变体。这是因为代理并不局限于一种用例。一个代理可能充当验证器,而另一个代理可能更注重提升性能等等。
其理念是,通过利用代理,我们包装现有对象,使其功能与原始对象相同,其方法(甚至属性)完全相同,直到我们在调用包装函数之前在包装方法内部添加其他逻辑。这是一个完全隐藏于外部世界的过程,并且此调用对调用者而言始终相同。
换句话说,代理位于对象的客户端和实际对象之间。它可以选择充当“保护者”,也可以添加自定义逻辑(例如缓存),而调用者对此毫不知情。因此,它有时也被称为中介者 (Mediator)。有些人也可能将其归类为装饰者模式的另一种形式,但两者之间存在一些区别。
在这篇文章中,我们将介绍 JavaScript 中代理设计模式的强大功能,并通过几个示例来说明它对您的下一个应用程序有何益处。
由于 JavaScript 本身添加了一个实现该模式的类,因此在几个原始实现之后,Proxy我们将直接使用该类来演示该模式。Proxy
装饰器与代理的区别
在装饰器模式中,装饰器的主要职责是增强它所包装(或“装饰”)的对象,而代理具有更多的可访问性并控制对象。
代理可以选择增强它所包装的对象或以其他方式控制它,例如限制外界的访问,但装饰器则会通知并应用增强功能。
责任方面的区别显而易见。工程师通常使用装饰器来添加新行为,或者将其作为旧类或遗留类的适配器,返回一个客户端可能知道但并不关心的增强接口。代理通常旨在返回相同的接口,客户端可以假设它正在处理相同的对象,并且没有修改过。
验证器/助手
我将在这里展示的代理模式的第一个实现是一个验证器。
此示例展示了该模式的实现,它有助于验证输入并防止属性被设置为错误的数据类型。请记住,调用者必须始终假设它正在处理原始对象,因此代理不得更改其包装对象的签名或接口:
class Pop {
  constructor(...items) {
    this.id = 1
  }
}
const withValidator = (obj, field, validate) => {
  let value = obj[field]
  Object.defineProperty(obj, field, {
    get() {
      return value
    },
    set(newValue) {
      const errMsg = validate(newValue)
      if (errMsg) throw new Error(errMsg)
      value = newValue
    },
  })
  return obj
}
let mello = new Pop(1, 2, 3)
mello = withValidator(mello, 'id', (newId) => {
  if (typeof newId !== 'number') {
    return `The id ${newId} is not a number. Received ${typeof newId} instead`
  }
})
mello.id = '3'
此示例显示了一个简单的助手,它验证对象的字段,TypeError当验证失败时抛出异常。
代理拥有属性的所有权,getter并选择允许或拒绝尝试设置的值。setterid
在Proxy课堂上可以用类似这样的方式实现:
const withValidator = (obj, field, validate) => {
  return new Proxy(obj, {
    set(target, prop, newValue) {
      if (prop === field) {
        const errMsg = validate(newValue)
        if (errMsg) throw new TypeError(errMsg)
        target[prop] = newValue
      }
    },
  })
}
let mello = new Pop(1, 2, 3)
mello = withValidator(mello, 'id', (newId) => {
  if (typeof newId !== 'number') {
    return `The id ${newId} is not a number. Received ${typeof newId} instead`
  }
})
mello.id = '3'
验证器运行完美:
TypeError: The id 3 is not a number. Received string instead
剪贴板 Polyfill
本节将介绍如何使用代理服务器支持旧版浏览器,将文本复制到用户剪贴板,前提是浏览器必须支持该Navigator.clipboardAPI。如果浏览器不支持,则将回退到使用代理服务器execCommand复制选定内容。
同样,客户端将始终假定它调用方法的对象是原始对象,并且只知道它正在调用所述方法:
const withClipboardPolyfill = (obj, prop, cond, copyFnIfCond) => {
  const copyToClipboard = (str) => {
    if (cond()) {
      copyFnIfCond()
    } else {
      const textarea = document.createElement('textarea')
      textarea.value = str
      textarea.style.visibility = 'hidden'
      document.body.appendChild(textarea)
      textarea.select()
      document.execCommand('copy')
      document.body.removeChild(textarea)
    }
  }
  obj[prop] = copyToClipboard
  return obj
}
const api = (function () {
  const o = {
    copyToClipboard(str) {
      return navigator.clipboard.writeText(str)
    },
  }
  return o
})()
let copyBtn = document.createElement('button')
copyBtn.id = 'copy-to-clipboard'
document.body.appendChild(copyBtn)
copyBtn.onclick = api.copyToClipboard
copyBtn = withClipboardPolyfill(
  copyBtn,
  'onclick',
  () => 'clipboard' in navigator,
  api.copyToClipboard,
)
copyBtn.click()
你可能会问,在这种情况下使用代理有什么意义,而不是直接在实际copyToClipboard函数内部硬编码实现。如果我们使用代理,我们可以将其作为独立函数重用,并通过控制反转自由地更改实现。
使用此策略的另一个好处是我们不会修改原始函数。
缓存器(增强性能)
缓存可以在许多不同的场景中采用多种形式。例如,http 请求的“重新验证期间过期”机制、 nginx 内容缓存、CPU 缓存、延迟加载缓存、memoization 缓存等等。
在 JavaScript 中我们还可以借助代理实现缓存。
为了在不直接使用类的情况下实现代理模式,Proxy我们可以执行以下操作:
const simpleHash = (str) =>
  str.split('').reduce((acc, str) => (acc += str.charCodeAt(0)), '')
const withMemoization = (obj, prop) => {
  const origFn = obj[prop]
  const cache = {}
  const fn = (...args) => {
    const hash = simpleHash(args.map((arg) => String(arg)).join(''))
    if (!cache[hash]) cache[hash] = origFn(...args)
    return cache[hash]
  }
  Object.defineProperty(obj, prop, {
    get() {
      return fn
    },
  })
  return obj
}
const sayHelloFns = {
  prefixWithHello(str) {
    return `[hello] ${str}`
  },
}
const enhancedApi = withMemoization(sayHelloFns, 'prefixWithHello')
enhancedApi.prefixWithHello('mike')
enhancedApi.prefixWithHello('sally')
enhancedApi.prefixWithHello('mike the giant')
enhancedApi.prefixWithHello('sally the little')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
缓存:
{
  "109105107101": "[hello] mike",
  "11597108108121": "[hello] sally",
  "109105107101321161041013210310597110116": "[hello] mike the giant",
  "115971081081213211610410132108105116116108101": "[hello] sally the little",
  "108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
直接在类中实现这一点Proxy很简单:
const withMemoization = (obj, prop) => {
  const origFn = obj[prop]
  const cache = {}
  const fn = (...args) => {
    const hash = simpleHash(args.map((arg) => String(arg)).join(''))
    if (!cache[hash]) cache[hash] = origFn(...args)
    return cache[hash]
  }
  return new Proxy(obj, {
    get(target, key) {
      if (key === prop) {
        return fn
      }
      return target[key]
    },
  })
}
 班级Proxy
我们已经在几个基本的代理模式实现中看到了持久化模式,而不是直接使用Proxy类。由于 JavaScript 直接Proxy以对象的形式提供,因此本文的其余部分将以此为方便。
所有剩余的示例都可以在没有的情况下实现Proxy,但我们将重点关注类语法,因为它更简洁,更容易使用,特别是为了这篇文章。
代理到单例
如果你从未听说过单例模式 (Singleton),它是另一种设计模式,它确保在应用程序的整个生命周期内,如果某个对象已经实例化,则该对象将被返回并重用。在实践中,你很可能会看到它被用作全局变量。
例如,如果我们正在编写一个 MMORPG 游戏,并且我们有三个类Equipment,,Person并且Warrior只能存在一个,那么我们可以在 实例化类时Warrior使用construct第二个参数内的处理程序方法:ProxyWarrior 
class Equipment {
  constructor(equipmentName, type, props) {
    this.id = `_${Math.random().toString(36).substring(2, 16)}`
    this.name = equipmentName
    this.type = type
    this.props = props
  }
}
class Person {
  constructor(name) {
    this.hp = 100
    this.name = name
    this.equipments = {
      defense: {},
      offense: {},
    }
  }
  attack(target) {
    target.hp -= 5
    const weapons = Object.values(this.equipments.offense)
    if (weapons.length) {
      for (const weapon of weapons) {
        console.log({ weapon })
        target.hp -= weapon.props.damage
      }
    }
  }
  equip(equipment) {
    this.equipments[equipment.type][equipment.id] = equipment
  }
}
class Warrior extends Person {
  constructor() {
    super(...arguments)
  }
  bash(target) {
    target.hp -= 15
  }
}
function useSingleton(_Constructor) {
  let _warrior
  return new Proxy(_Constructor, {
    construct(target, args, newTarget) {
      if (!_warrior) _warrior = new Warrior(...args)
      return _warrior
    },
  })
}
const WarriorSingleton = useSingleton(Warrior)
如果我们尝试创建多个实例,Warrior我们可以确保每次只使用创建的第一个实例:
const mike = new WarriorSingleton('mike')
const bob = new WarriorSingleton('bob')
const sally = new WarriorSingleton('sally')
console.log(mike)
console.log(bob)
console.log(sally)
结果:
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
饼干窃贼
在本节中,我们将演示一个示例,使用 来Proxy阻止 cookie 列表的更改。这将阻止原始对象被更改,并且更改器(CookieStealer)将假定他们的恶意操作成功了。
让我们看一下这个例子:
class Food {
  constructor(name, points) {
    this.name = name
    this.points = points
  }
}
class Cookie extends Food {
  constructor() {
    super(...arguments)
  }
  setFlavor(flavor) {
    this.flavor = flavor
  }
}
class Human {
  constructor() {
    this.foods = []
  }
  saveFood(food) {
    this.foods.push(food)
  }
  eat(food) {
    if (this.foods.includes(food)) {
      const foodToEat = this.foods.splice(this.foods.indexOf(food), 1)[0]
      this.hp += foodToEat.points
    }
  }
}
const apple = new Food('apple', 2)
const banana = new Food('banana', 2)
const chocolateChipCookie = new Cookie('cookie', 2)
const sugarCookie = new Cookie('cookie', 2)
const butterCookie = new Cookie('cookie', 3)
const bakingSodaCookie = new Cookie('cookie', 3)
const fruityCookie = new Cookie('cookie', 5)
chocolateChipCookie.setFlavor('chocolateChip')
sugarCookie.setFlavor('sugar')
butterCookie.setFlavor('butter')
bakingSodaCookie.setFlavor('bakingSoda')
fruityCookie.setFlavor('fruity')
const george = new Human()
george.saveFood(apple)
george.saveFood(banana)
george.saveFood(chocolateChipCookie)
george.saveFood(sugarCookie)
george.saveFood(butterCookie)
george.saveFood(bakingSodaCookie)
george.saveFood(fruityCookie)
console.log(george)
乔治的食物:
 {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 },
    Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
    Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
    Cookie { name: 'cookie', points: 3, flavor: 'butter' },
    Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
    Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
  ]
}
我们george使用该类进行了实例化Human,并在其存储中添加了 7 种食物。乔治很高兴他即将吃水果和饼干。他对饼干尤其兴奋,因为他同时获得了他最喜欢的口味,很快就会狼吞虎咽地吃掉它们,满足他对饼干的渴望。
但是,有一个问题:
const CookieStealer = (function () {
  const myCookiesMuahahaha = []
  return {
    get cookies() {
      return myCookiesMuahahaha
    },
    isCookie(obj) {
      return obj instanceof Cookie
    },
    stealCookies(person) {
      let indexOfCookie = person.foods.findIndex(this.isCookie)
      while (indexOfCookie !== -1) {
        const food = person.foods[indexOfCookie]
        if (this.isCookie(food)) {
          const stolenCookie = person.foods.splice(indexOfCookie, 1)[0]
          myCookiesMuahahaha.push(stolenCookie)
        }
        indexOfCookie = person.foods.findIndex(this.isCookie)
      }
    },
  }
})()
CookieStealer.stealCookies(george)
突然CookieStealer出现偷走了他的饼干。CookieStealer现在他的存储里有5块饼干:
[
  Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
  Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
  Cookie { name: 'cookie', points: 3, flavor: 'butter' },
  Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
  Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
乔治:
Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 }
  ]
}
如果我们倒回去,让我们的救世主Superman应用他的一种方法来实现这个Proxy模式来阻止CookieStealer他的邪恶行为,那么我们的问题就解决了:
class Superman {
  protectFromCookieStealers(obj, key) {
    let realFoods = obj[key]
    let fakeFoods = [...realFoods]
    return new Proxy(obj, {
      get(target, prop) {
        if (key === prop) {
          fakeFoods = [...fakeFoods]
          Object.defineProperty(fakeFoods, 'splice', {
            get() {
              return function fakeSplice(...[index, removeCount]) {
                fakeFoods = [...fakeFoods]
                return fakeFoods.splice(index, removeCount)
              }
            },
          })
          return fakeFoods
        }
        return target[prop]
      },
    })
  }
}
const superman = new Superman()
const slickGeorge = superman.protectFromCookieStealers(george, 'foods')
我们的朋友superman很幸运,他恰好拥有一个protectFromCookieStealers利用 的力量Proxy伪造饼干清单的人!他把包含乔治饼干的真正食物藏了起来,不让CookieStealer。CookieStealer他继续实施他的邪恶计划,似乎被骗了,以为他已经拿到了饼干:
CookieStealer.stealCookies(slickGeorge)
console.log(CookieStealer.cookies)
他CookieStealer带着储藏室里的饼干离开了,并认为他已经逃脱了惩罚:
[
  Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
  Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
  Cookie { name: 'cookie', points: 3, flavor: 'butter' },
  Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
  Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
他不知道自己被超人欺骗了,那些是假饼干!george由于超人的力量将Proxy他从邪恶的黑暗中拯救出来,他的饼干仍然完好无损:
console.log(slickGeorge)
Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 },
    Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
    Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
    Cookie { name: 'cookie', points: 3, flavor: 'butter' },
    Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
    Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
  ]
}
结论
我希望这有助于阐明代理模式以及如何使用ProxyJavaScript 中现在内置的类来利用这个概念。
这篇文章到此结束 :) 我希望你发现这篇文章对你有帮助,并确保在 Medium 上关注我,以便查看以后的文章!
在Medium上找到我
文章来源:https://dev.to/jsmanifest/the-power-of-proxy-pattern-in-javscript-6fp 后端开发教程 - Java、Spring Boot 实战 - msg200.com
            后端开发教程 - Java、Spring Boot 实战 - msg200.com
          