JavaScript 中代理模式的威力

2025-05-25

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'
Enter fullscreen mode Exit fullscreen mode

此示例显示了一个简单的助手,它验证对象的字段,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'
Enter fullscreen mode Exit fullscreen mode

验证器运行完美:

TypeError: The id 3 is not a number. Received string instead
Enter fullscreen mode Exit fullscreen mode

剪贴板 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()
Enter fullscreen mode Exit fullscreen mode

你可能会问,在这种情况下使用代理有什么意义,而不是直接在实际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')
Enter fullscreen mode Exit fullscreen mode

缓存:

{
  "109105107101": "[hello] mike",
  "11597108108121": "[hello] sally",
  "109105107101321161041013210310597110116": "[hello] mike the giant",
  "115971081081213211610410132108105116116108101": "[hello] sally the little",
  "108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
Enter fullscreen mode Exit fullscreen mode

直接在类中实现这一点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]
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

班级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)
Enter fullscreen mode Exit fullscreen mode

如果我们尝试创建多个实例,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)
Enter fullscreen mode Exit fullscreen mode

结果:

Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Warrior {
  hp: 100,
  name: 'mike',
  equipments: { defense: {}, offense: {} }
}
Enter fullscreen mode Exit fullscreen mode

饼干窃贼

在本节中,我们将演示一个示例,使用 来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)
Enter fullscreen mode Exit fullscreen mode

乔治的食物:

 {
  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' }
  ]
}
Enter fullscreen mode Exit fullscreen mode

我们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)
Enter fullscreen mode Exit fullscreen mode

突然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' }
]
Enter fullscreen mode Exit fullscreen mode

乔治:

Human {
  foods: [
    Food { name: 'apple', points: 2 },
    Food { name: 'banana', points: 2 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

如果我们倒回去,让我们的救世主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')
Enter fullscreen mode Exit fullscreen mode

我们的朋友superman很幸运,他恰好拥有一个protectFromCookieStealers利用 的力量Proxy伪造饼干清单的人!他把包含乔治饼干的真正食物藏了起来,不让CookieStealerCookieStealer他继续实施他的邪恶计划,似乎被骗了,以为他已经拿到了饼干:

CookieStealer.stealCookies(slickGeorge)

console.log(CookieStealer.cookies)
Enter fullscreen mode Exit fullscreen mode

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' }
]
Enter fullscreen mode Exit fullscreen mode

他不知道自己被超人欺骗了,那些是假饼干george由于超人的力量将Proxy他从邪恶的黑暗中拯救出来,他的饼干仍然完好无损:

console.log(slickGeorge)
Enter fullscreen mode Exit fullscreen mode
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' }
  ]
}
Enter fullscreen mode Exit fullscreen mode

结论

我希望这有助于阐明代理模式以及如何使用ProxyJavaScript 中现在内置的类来利用这个概念。

这篇文章到此结束 :) 我希望你发现这篇文章对你有帮助,并确保在 Medium 上关注我,以便查看以后的文章!

在Medium上找到我

文章来源:https://dev.to/jsmanifest/the-power-of-proxy-pattern-in-javscript-6fp
PREV
JavaScript 中观察者模式的威力
NEXT
JavaScript 中高阶函数的威力(附示例和用例)