Proxy实际用法

我心飞翔 分类:javascript

翻译并带自己总结: A practical guide to Javascript Proxy

1. Default/ “Zero Values”

给对象(包括数组)提供zero values. zero values分别是Boolean(false), Number(0), String(''), Object({}), Array([])等等.

const withZeroValue = (target, zeroValue) => new Proxy(target, {
  get: (obj, prop) => (prop in obj) ? obj[prop] : zeroValue
})

let pos = {
  x: 4,
  y: 19
}

console.log(pos.x, pos.y, pos.z) // 4, 19, undefined

pos = withZeroValue(pos, 0)

console.log(pos.x, pos.y, pos.z) // 4, 19, 0
 

2. Negative Array Indices

获取数组的最后一个元素在JS里一般使用arr[arr.length - 1], 又重复又有可能出现差一位的错误.

const negativeArray = (els) => new Proxy(els, {
  get: (target, propKey, receiver) => Reflect.get(target,
    (+propKey < 0) ? String(target.length + +propKey) : propKey, receiver)
});

const unicorn = negativeArray(['🐴', '🎂', '🌈']);

unicorn[-1] // '🌈'
 

相关引用:

TC39Array.lastItem的提案: proposal-array-last.

Ruby的negative array indices: Ruby Quicktips.

npm包: negative-array

3. Hiding Properties

我们经常将私有属性前面带上_,hide函数可以让前缀带有_的属性不可迭代.

const hide = (target, prefix = '_') => new Proxy(target, {
  has: (obj, prop) => (!prop.startsWith(prefix) && prop in obj),
  ownKeys: (obj) => Reflect.ownKeys(obj)
    .filter(prop => (typeof prop !== "string" || !prop.startsWith(prefix))),
  get: (obj, prop, rec) => (prop in rec) ? obj[prop] : undefined
})

let userData = hide({
  firstName: 'Tom',
  mediumHandle: '@tbarrasso',
  _favoriteRapper: 'Drake'
})

userData._favoriteRapper        // undefined
('_favoriteRapper' in userData) // false
Object.keys(userData)           // ['firstName', 'mediumHandle']
 

可以使用Object.defineProperty设置属性或Symbol值设为属性使其不可迭代.

4. Caching

There are two hard problems in computer science: cache invalidation, naming things, and off-by-one errors.

ephemeral函数可以设置属性多久超时. 超时时获取不到该属性值(缓存失效).

const ephemeral = (target, ttl = 60) => {
  const CREATED_AT = Date.now()
  const isExpired = () => (Date.now() - CREATED_AT) > (ttl * 1000)
  
  return new Proxy(target, {
    get: (obj, prop) => isExpired() ? undefined : Reflect.get(obj, prop)
  })
}

let bankAccount = ephemeral({
  balance: 14.93
}, 10)

console.log(bankAccount.balance)    // 14.93

setTimeout(() => {
  console.log(bankAccount.balance)  // undefined
}, 10 * 1000)
 

5. Enums & Read-Only Views

枚举(enum)获取不存在的属性会报错, 他与第一个属性默认值类似, 返回的一个是默认值一个是报错, 但他的行为与枚举类型很相似所以单独列出来.

const createEnum = (target) => readOnlyView(new Proxy(target, {
  get: (obj, prop) => {
    if (prop in obj) {
      return Reflect.get(obj, prop)
    }
    throw new ReferenceError(`Unknown prop "${prop}"`)
  }
}))
 

只读对象

const NOPE = () => {
  throw new Error("Can't modify read-only view");
}

const NOPE_HANDLER = {
  set: NOPE,
  defineProperty: NOPE,
  deleteProperty: NOPE,
  preventExtensions: NOPE,
  setPrototypeOf: NOPE
}

const readOnlyView = target =>
  new Proxy(target, NOPE_HANDLER)

let SHIRT_SIZES = createEnum({
  S: 10,
  M: 15,
  L: 20
})

SHIRT_SIZES.S // 10
SHIRT_SIZES.S = 15

// Uncaught Error: Can't modify read-only view

SHIRT_SIZES.XL

// Uncaught ReferenceError: Unknown prop "XL"
 

相关链接

TS enum type

6. Operator Overload

重载in运算符. 虽然有点感觉增加了心智负担, 但或许增加了代码可读性.

const range = (min, max) => new Proxy(Object.create(null), {
  has: (_, prop) => (+prop >= min && +prop <= max)
})

const X = 10.5
const nums = [1, 5, X, 50, 100]

if (X in range(1, 100)) { // true
  // ...
}

nums.filter(n => n in range(1, 10)) // [1, 5]
 

不止可以计算数字范围,还可以排除范围数, 自然数, 有理数, 虚数, 无限数等等.

还可以重载delete, new.

7. Cookies Object

document.cookie获取的是一个字符串, 可以简单实现一个cookies的object方便操作.

_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1
 
const getCookieObject = () => {
    const cookies = document.cookie.split(';').reduce((cks, ck) => 
	({[ck.substr(0, ck.indexOf('=')).trim()]: ck.substr(ck.indexOf('=') + 1), ...cks}), {});
    const setCookie = (name, val) => document.cookie = `${name}=${val}`;
    const deleteCookie = (name) => document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`;

    return new Proxy(cookies, {
		set: (obj, prop, val) => (setCookie(prop, val), Reflect.set(obj, prop, val)),
        deleteProperty: (obj, prop) => (deleteCookie(prop), Reflect.deleteProperty(obj, prop))
     })
}


let docCookies = getCookieObject()

docCookies.has_recent_activity              // "1"
docCookies.has_recent_activity = "2"        // "2"
delete docCookies["has_recent_activity"]   // true

 

所以可以想到, 类似键值对的字符串都可以想是否需要用Proxy进行包装,来方便后续操作.

总结

为什么要用Proxy?

个人的理解为:

  1. 增强代码的语义化, 改变原来对象对属性的各方面的操作, 而不去用额外的函数等去包裹, 包裹后的对象天生就对设置好的属性有特殊的操作.

  2. 可以用简单的配置将对象增强, 比如我们可以让cookie只读,隐藏特殊属性, 带有ZeroValue的时候可以这样写:

    // document.cookie = "_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1"
    
    let docCookies = withZeroValue(hide(readOnlyView(getCookieObject())), "Cookie not found")
    
    docCookies.has_recent_activity  // "1"
    docCookies.nonExistentCookie    // "Cookie not found"
    docCookies._ga                  // "Cookie not found"
    docCookies.newCookie = "1"      // Uncaught Error: Can't modify read-only view
     

一句话总结Proxy的作用?

可能会总结的并不完整,但最重要的作用就是增强对象.

回复

我来回复
  • 暂无回复内容