學(xué)會 Proxy 真的可以為所欲為
Proxy 是 JavaScript 2015 的一個新特性,下面讓我們看看他實(shí)現(xiàn)哪些有趣的東西。
更安全的枚舉類型
在 JavaScript 里,我們通常用一個對象來表示枚舉值。
但這往往是不安全,我們希望枚舉值:
如果不存在的話,報錯。 不允許動態(tài)設(shè)置,否則報錯。 不允許刪除,否則報錯。
我們下面會寫一個 enum 的函數(shù),不過先讓我們來看看他在 redux 的 action types 的應(yīng)用。
// enum.test.js
test('enum', () => {
// 我們定義了倆個 action type
const actionTypes = {
ADD_TODO: 'add_todo',
UPDATE_TODO: 'update_todo'
}
const safeActionTypes = enum(actionTypes)
// 當(dāng)讀取一個不存在的枚舉值時會報錯
// 因?yàn)?nbsp;'DELETE_TODO' 并沒有定義,所以此時會報錯
expect(() => {
safeActionTypes['DELETE_TODO']
}).toThrowErrorMatchingSnapshot()
// 當(dāng)刪除一個枚舉值時會報錯
expect(() => {
delete safeActionTypes['ADD_TODO']
}).toThrowErrorMatchingSnapshot()
})
那么,enum 函數(shù)怎么寫呢?
很簡單,只要用 Proxy 的 get , set 和 deleteProperty 鉤子就好了。
// erum.js
export default function enum(object) {
return new Proxy(object, {
get(target, prop) {
if (target[prop]) {
return Reflect.get(target, prop)
} else {
throw new ReferenceError(`Unknown enum '${prop}'`)
}
},
set() {
throw new TypeError('Enum is readonly')
},
deleteProperty() {
throw new TypeError('Enum is readonly')
}
})
}
拓展一下的話,我們是不是可以寫個類型校驗(yàn)庫,在這里我們就不展開了。
測試,Mock
利用 apply 鉤子,Proxy 可以檢測一個函數(shù)的調(diào)用情況。
下面是一個簡單的,用于單元測試的 spy 庫。他可以獲取函數(shù)的調(diào)用次數(shù),以及調(diào)用時的參數(shù)等。
// spy.js
export function spy() {
const spyFn = function() {}
spyFn.toBeCalledTimes = 0
spyFn.lastCalledWith = undefined
return new Proxy(spyFn, {
apply(target, thisArg, argumentsList) {
target.toBeCalledTimes += 1
target.lastCalledWith = argumentsList.join(', ')
}
})
}
// spy.test.js
const colors = ['red', 'blue']
const callback = spy()
colors.forEach(color => callback(color))
expect(callback.toBeCalledTimes).toBe(colors.length)
expect(callback.lastCalledWith).toBe(colors[1])
另外,用 Proxy 寫一個斷言庫也是挺方便的,這里就不展開了。
Immutable
我們也可以利用 Proxy 在數(shù)據(jù)結(jié)構(gòu)上做些操作,比如實(shí)現(xiàn)一個像 immer 的 Immutable 庫。
import { shallowCopy } from './utils/index'
export function produce(base, producer) {
const state = {
base, // 原來的數(shù)據(jù)
copy: null, // 新的,復(fù)制的數(shù)據(jù)
modified: false, // 是否修改過
}
const proxy = new Proxy(state, {
get(target, prop) {
// 如果修改過,則返回副本數(shù)據(jù),或者返回原來的數(shù)據(jù)
return target.modified ? target.copy[prop] : target.base[prop]
},
set(target, prop, value) {
// set 鉤子的時候,設(shè)置 modifyied 為 true
if (!target.modifyied) {
target.modified = true
target.copy = shallowCopy(target.base)
}
target.copy[prop] = value
return true
}
})
producer.call(proxy, proxy)
return proxy
}
實(shí)際效果就像下面這個樣子:
我們得到了新的不同的 nextState ,但是原來的 baseState 并沒有發(fā)生變化。
test('produce', () => {
const baseState = {
name: 'foo'
}
const nextState = produce(baseState, draft => {
draft.name = 'bar'
})
expect(nextState.name).toBe('bar') // nestState 發(fā)生了變化
expect(baseState.name).toBe('foo') // 而 baseState 保持不變
})
Observe,響應(yīng)式系統(tǒng)
用 Proxy 來實(shí)現(xiàn)一個 pub/sub 模式也是挺簡單的。
// observe.js
export function observe(target, onChange) {
return createProxy(target, onChange)
}
function createProxy(target, onChange) {
const trap = {
get(object, prop) {
const value = object[prop]
// 這里可以優(yōu)化一下,不應(yīng)該每次都創(chuàng)建新的 proxy
if (typeof value === 'object' && value !== null) {
return createProxy(object[prop], onChange)
}
return value
},
set(object, prop, value, ...args) {
onChange()
return Reflect.set(object, prop, value, ...args)
}
}
return new Proxy(target, trap)
}
// observe.test.js
test('observe', () => {
const stub = jest.fn()
const data = {
user: {
name: 'foo',
},
colors: ['red'],
}
const reactiveData = observe(data, stub)
// push 會觸發(fā)兩次 set 鉤子
// 第一次把 colors 的 2 屬性設(shè)置為 'blue'
// 第二次把 colors 的 length 屬性設(shè)置為 2
reactiveData.colors.push('blue')
reactiveData.user.name = 'baz'
// 動態(tài)增加一個新的屬性
reactiveData.type = 'zzz'
expect(stub).toHaveBeenCalledTimes(4)
})
從上面可以發(fā)現(xiàn),Proxy 不僅可以代理對象,也可以代理數(shù)組;還可以代理動態(tài)增加的屬性如 type 。這也是 Object.defineProperty 做不到的。
加個依賴追蹤的話,我們就可以實(shí)現(xiàn)一個類似 Vue 或者 Mobx 的響應(yīng)式系統(tǒng)了。
更多有趣的例子
我們還可以用 Proxy 實(shí)現(xiàn)很多東西,比如埋點(diǎn)可以不,性能監(jiān)控可以不?
How to use JavaScript Proxies for Fun and Profit ES6 Features - 10 Use Cases for Proxy proxy-fun
Proxy 的更多玩法,大家好好挖掘挖掘
