JavaScript: Proxy 與 localStorage

YY
9 min readMar 13, 2021

--

Photo by MIKHAIL VASILYEV on Unsplash

是說幾年前就聽過 Proxy 這傢伙,但一直覺得很複雜而沒搞懂,實務上也沒使用到,直到前陣子看到強者我同事 side project 用上了這酷東西,才發現不難理解又滿有趣的,難道這就跟長大後覺得從小吃到大的水餃變小了是一樣的道理嗎

又剛好之後寫了個小玩具用到 localStorage,覺得拿 Proxy 來包裝還不錯用,就來紀錄一下 Proxy 跟 localStorage 的糾纏以及如何監聽不同分頁的 localStorage change event

Proxy

ES6 的新產物,人如其名,功用就是 proxy,是個 object 代理人,可以使用者要操作這個 object 時都要先經過這個代理人的手

const target = {
foo: 'bar'
}
const handler = {
get(target, prop, receiver) {
return target[prop] + '!'
},
set(target, prop, val, receiver) {
return target[prop] = val + '?'
}
}
const p = new Proxy(target, handler)console.log(p.foo) // 'bar!'
p.foo = 'bbb'
console.log(p.foo) // 'bbb?!'

如上,透過 p 這個 Proxy 依照 handler 的邏輯來操作 target 物件,除了 getset 外還有其他操作可以被代理:

handler.apply()A trap for a function call.
handler.construct()A trap for the new operator.
handler.defineProperty()A trap for Object.defineProperty.
handler.deleteProperty()A trap for the delete operator.
handler.get()A trap for getting property values.
handler.getOwnPropertyDescriptor()A trap for Object.getOwnPropertyDescriptor.
handler.getPrototypeOf()A trap for Object.getPrototypeOf.
handler.has()A trap for the in operator.
handler.isExtensible()A trap for Object.isExtensible.
handler.ownKeys()A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols.
handler.preventExtensions()A trap for Object.preventExtensions.
handler.set()A trap for setting property values.
handler.setPrototypeOf()A trap for Object.setPrototypeOf.

更多細節請參考最下方文件連結

Reflect

講到 Proxy 就要知道他有個好朋友 Reflect,跟 Proxy 有著一樣的 handlers 以及相同的參數

Reflect.set

例如 set,會去幫你執行 set 這件事並回傳 true/false 表示是否成功

new Proxy(
{
foo: 'bar',
_foo: 'secret bar'
}, {
set(target, prop, val) {
if (prop.startsWith('_')) {
throw Error('fail to set item prefixed with _')
}
return Reflect.set(...arguments)
}
}
)

但這不是本次重點就不多贅述,更多細節請參考最下方文件連結

localStorage

這是應該不用多談,就是個每次要讀寫 Object 都要先 JSON.stringifyJSON.parse 的麻煩傢伙

w/ Proxy

比起另外寫 function 去做轉換

const KEY = '__myStorage'const getValFromLocalStorage = () => {
return JSON.parse(localStorage[KEY] || '{}')
}
const setValToLocalStorage = (val) => {
localStorage[KEY] = JSON.stringify(val)
}

也許可以換個想法,把 data 都存在 local object,也都針對這個 object 去操作,但在操作這個 object 的同時他會順便把資料存到 localStorage 中,也就變成:

const KEY = '__myStorage'
const initData = JSON.parse(localStorage[KEY] || '{}')
const setDataToStorage = (data) => {
localStorage[KEY] = JSON.stringify(data)
}
const createProxy = (initData) => new Proxy(initData, {
set(target, prop, val) {
if (Reflect.set(...arguments)) {
setDataToStorage(target)
return true
}
},
deleteProperty(target, prop) {
if (Reflect.deleteProperty(...arguments)) {
setDataToStorage(target)
return true
}
}
})
let data = createProxy(initData)

接著就可以直接操作 data 物件,並同時會把 data 的值存到 localStorage 中

on localStorage change

但這邊會遇到一個問題,當使用者在多個分頁中操作時會發生 race condition,所以需要監聽 localStorage change event 來更新 data 這個 Proxy object,這邊可以透過 window.onstorage 來監聽

// 承上
window.addEventListener('storage', (e) => {
const { key, newValue } = e
if (key === KEY) {
data = createProxy(JSON.parse(newValue))
}
})

經過測試發現 storage event 觸發的時機為

  1. 用 browser devtool 操作 localStorage/sessionStorage
  2. 在不同分頁透過 JS 操作 localStorage/sessionStorage

另外要注意的是 storage event 好像 沒辦法分辨改變的是 localStorage 還是 sessionStorage,所以命名不要重複為上

--

--