您好,登錄后才能下訂單哦!
ES6 Proxy如何在JavaScript中使用?針對這個(gè)問題,這篇文章詳細(xì)介紹了相對應(yīng)的分析和解答,希望可以幫助更多想解決這個(gè)問題的小伙伴找到更簡單易行的方法。
為函數(shù)添加特定的功能
* 為異步函數(shù)添加自動超時(shí)功能 * @param timeout 超時(shí)時(shí)間 * @param action 異步函數(shù) * @returns 包裝后的異步函數(shù) */ function asyncTimeout(timeout, action) { return function(...args) { return Promise.race([ Reflect.apply(action, this, args), wait(timeout).then(Promise.reject), ]) } }
一般而言,上面的代碼足以勝任,但問題就在這里,不一般的情況 -- 函數(shù)上面包含自定義屬性呢?
眾所周知,JavaScript 中的函數(shù)是一等公民,即函數(shù)可以被傳遞,被返回,以及,被添加屬性!
例如下面這個(gè)簡單的函數(shù) get,其上有著 _name 這個(gè)屬性
const get = async i => i get._name = 'get'
一旦使用上面的 asyncTimeout 函數(shù)包裹之后,問題便會出現(xiàn),返回的函數(shù)中 _name 屬性不見了。這是當(dāng)然的,畢竟實(shí)際上返回的是一個(gè)匿名函數(shù)。那么,如何才能讓返回的函數(shù)能夠擁有傳入函數(shù)參數(shù)上的所有自定義屬性呢?
一種方式是復(fù)制參數(shù)函數(shù)上的所有屬性,但這點(diǎn)實(shí)現(xiàn)起來其實(shí)并不容易,真的不容易,不信你可以看看 Lodash 的 clone 函數(shù)。那么,有沒有一種更簡單的方式呢?答案就是 Proxy,它可以代理對象的指定操作,除此之外,其他的一切都指向原對象。
下面是 Proxy 實(shí)現(xiàn)的 asyncTimeout 函數(shù)
/** * 為異步函數(shù)添加自動超時(shí)功能 * @param timeout 超時(shí)時(shí)間 * @param action 異步函數(shù) * @returns 包裝后的異步函數(shù) */ function asyncTimeout(timeout, action) { return new Proxy(action, { apply(_, _this, args) { return Promise.race([ Reflect.apply(_, _this, args), wait(timeout).then(Promise.reject), ]) }, }) }
測試一下,是可以正常調(diào)用與訪問其上的屬性的
;(async () => { console.log(await get(1)) console.log(get._name) })()
好了,這便是吾輩最常用的一種方式了 -- 封裝高階函數(shù),為函數(shù)添加某些功能。
代理對象的訪問
下面是一段代碼,用以在頁面上展示從后臺獲取的數(shù)據(jù),如果字段沒有值則默認(rèn)展示 ''
模擬一個(gè)獲取列表的異步請求
async function list() { // 此處僅為構(gòu)造列表 class Person { constructor({ id, name, age, sex, address } = {}) { this.id = id this.name = name this.age = age this.sex = sex this.address = address } } return [ new Person({ id: 1, name: '琉璃' }), new Person({ id: 2, age: 17 }), new Person({ id: 3, sex: false }), new Person({ id: 4, address: '幻想鄉(xiāng)' }), ] }
嘗試直接通過解構(gòu)為屬性賦予默認(rèn)值,并在默認(rèn)值實(shí)現(xiàn)這個(gè)功能
;(async () => { // 為所有為賦值屬性都賦予默認(rèn)值 '' const persons = (await list()).map( ({ id = '', name = '', age = '', sex = '', address = '' }) => ({ id, name, age, sex, address, }), ) console.log(persons) })()
下面讓我們寫得更通用一些
function warp(obj) { const result = obj for (const k of Reflect.ownKeys(obj)) { const v = Reflect.get(obj, k) result[k] = v === undefined ? '' : v } return obj } ;(async () => { // 為所有為賦值屬性都賦予默認(rèn)值 '' const persons = (await list()).map(warp) console.log(persons) })()
暫且先看一下這里的 warp 函數(shù)有什么問題?
這里是答案的分割線
所有屬性需要預(yù)定義,不能運(yùn)行時(shí)決定
沒有指向原對象,后續(xù)的修改會造成麻煩
吾輩先解釋一下這兩個(gè)問題
所有屬性需要預(yù)定義,不能運(yùn)行時(shí)決定
如果調(diào)用了 list[0].a 會發(fā)生什么呢?是的,依舊會是 undefined,因?yàn)?Reflect.ownKeys 也不能找到?jīng)]有定義的屬性(真*undefined),因此導(dǎo)致訪問未定義的屬性仍然會是 undefined 而非期望的默認(rèn)值。
沒有指向原對象,后續(xù)的修改會造成麻煩
如果我們此時(shí)修改對象的一個(gè)屬性,那么會影響到原本的屬性么?不會,因?yàn)?warp 返回的對象已經(jīng)是全新的了,和原對象沒有什么聯(lián)系。所以,當(dāng)你修改時(shí)當(dāng)然不會影響到原對象。
Pass: 我們當(dāng)然可以直接修改原對象,但這很明顯不太符合我們的期望:顯示時(shí)展示默認(rèn)值 '' -- 這并不意味著我們愿意在其他操作時(shí)需要 '',否則我們還要再轉(zhuǎn)換一遍。(例如發(fā)送編輯后的數(shù)據(jù)到后臺)
這個(gè)時(shí)候 Proxy 也可以派上用場,使用 Proxy 實(shí)現(xiàn) warp 函數(shù)
function warp(obj) { const result = new Proxy(obj, { get(_, k) { const v = Reflect.get(_, k) if (v !== undefined) { return v } return '' }, }) return result }
現(xiàn)在,上面的那兩個(gè)問題都解決了!
注: 知名的 GitHub 庫 immer 就使用了該特性實(shí)現(xiàn)了不可變狀態(tài)樹。
作為膠水橋接不同結(jié)構(gòu)的對象
通過上面的例子我們可以知道,即便是未定義的屬性,Proxy 也能進(jìn)行代理。這意味著,我們可以通過 Proxy 抹平相似對象之間結(jié)構(gòu)的差異,以相同的方式處理類似的對象。
Pass: 不同公司的項(xiàng)目中的同一個(gè)實(shí)體的結(jié)構(gòu)不一定完全相同,但基本上類似,只是字段名不同罷了。所以使用 Proxy 實(shí)現(xiàn)膠水橋接不同結(jié)構(gòu)的對象方便我們在不同公司使用我們的工具庫!
嘛,開個(gè)玩笑,其實(shí)在同一個(gè)公司中不同的實(shí)體也會有類似的結(jié)構(gòu),也會需要相同的操作,最常見的應(yīng)該是樹結(jié)構(gòu)數(shù)據(jù)。例如下面的菜單實(shí)體和系統(tǒng)權(quán)限實(shí)體就很相似,也需要相同的操作 -- 樹 <=> 列表 相互轉(zhuǎn)換。
思考一下如何在同一個(gè)函數(shù)中處理這兩種樹節(jié)點(diǎn)結(jié)構(gòu)
/** * 系統(tǒng)菜單 */ class SysMenu { /** * 構(gòu)造函數(shù) * @param {Number} id 菜單 id * @param {String} name 顯示的名稱 * @param {Number} parent 父級菜單 id */ constructor(id, name, parent) { this.id = id this.name = name this.parent = parent } } /** * 系統(tǒng)權(quán)限 */ class SysPermission { /** * 構(gòu)造函數(shù) * @param {String} uid 系統(tǒng)唯一 uuid * @param {String} label 顯示的菜單名 * @param {String} parentId 父級權(quán)限 uid */ constructor(uid, label, parentId) { this.uid = uid this.label = label this.parentId = parentId } }
下面讓我們使用 Proxy 來抹平訪問它們之間的差異
const sysMenuProxy = { parentId: 'parent' } const sysMenu = new Proxy(new SysMenu(1, 'rx', 0), { get(_, k) { if (Reflect.has(sysMenuProxy, k)) { return Reflect.get(_, Reflect.get(sysMenuProxy, k)) } return Reflect.get(_, k) }, }) console.log(sysMenu.id, sysMenu.name, sysMenu.parentId) // 1 'rx' 0 const sysPermissionProxy = { id: 'uid', name: 'label' } const sysPermission = new Proxy(new SysPermission(1, 'rx', 0), { get(_, k) { if (Reflect.has(sysPermissionProxy, k)) { return Reflect.get(_, Reflect.get(sysPermissionProxy, k)) } return Reflect.get(_, k) }, }) console.log(sysPermission.id, sysPermission.name, sysPermission.parentId) // 1 'rx' 0
看起來似乎有點(diǎn)繁瑣,讓我們封裝一下
/** * 橋接對象不存在的字段 * @param {Object} map 代理的字段映射 Map * @returns {Function} 轉(zhuǎn)換一個(gè)對象為代理對象 */ function bridge(map) { /** * 為對象添加代理的函數(shù) * @param {Object} obj 任何對象 * @returns {Proxy} 代理后的對象 */ return function(obj) { return new Proxy(obj, { get(target, k) { // 如果遇到被代理的屬性則返回真實(shí)的屬性 if (Reflect.has(map, k)) { return Reflect.get(target, Reflect.get(map, k)) } return Reflect.get(target, k) }, set(target, k, v) { // 如果遇到被代理的屬性則設(shè)置真實(shí)的屬性 if (Reflect.has(map, k)) { Reflect.set(target, Reflect.get(map, k), v) return true } Reflect.set(target, k, v) return true }, }) } }
現(xiàn)在,我們可以用更簡單的方式來做代理了。
const sysMenu = bridge({ parentId: 'parent', })(new SysMenu(1, 'rx', 0)) console.log(sysMenu.id, sysMenu.name, sysMenu.parentId) // 1 'rx' 0 const sysPermission = bridge({ id: 'uid', name: 'label', })(new SysPermission(1, 'rx', 0)) console.log(sysPermission.id, sysPermission.name, sysPermission.parentId) // 1 'rx' 0
如果想看 JavaScirpt 如何處理樹結(jié)構(gòu)數(shù)據(jù)話,可以參考吾輩的JavaScript 處理樹數(shù)據(jù)結(jié)構(gòu)
監(jiān)視對象的變化
接下來,我們想想,平時(shí)是否有需要監(jiān)視對象的變化,然后進(jìn)行某些處理呢?
例如監(jiān)視用戶復(fù)選框選中項(xiàng)列表的變化并更新對應(yīng)的需要發(fā)送到后臺的 id 拼接字符串。
// 模擬頁面的復(fù)選框列表 const hobbyMap = new Map() .set(1, '小說') .set(2, '動畫') .set(3, '電影') .set(4, '游戲') const user = { id: 1, // 保存興趣 id 的列表 hobbySet: new Set(), // 發(fā)送到后臺的興趣 id 拼接后的字符串,以都好進(jìn)行分割 hobby: '', } function onClick(id) { user.hobbySet.has(id) ? user.hobbySet.delete(id) : user.hobbySet.add(id) } // 模擬兩次點(diǎn)擊 onClick(1) onClick(2) console.log(user.hobby) // ''
下面使用 Proxy 來完成 hobbySet 屬性改變后 hobby 自動更新的操作
/** * 深度監(jiān)聽指定對象屬性的變化 * 注:指定對象不能是原始類型,即不可變類型,而且對象本身的引用不能改變,最好使用 const 進(jìn)行聲明 * @param object 需要監(jiān)視的對象 * @param callback 當(dāng)代理對象發(fā)生改變時(shí)的回調(diào)函數(shù),回調(diào)函數(shù)有三個(gè)參數(shù),分別是對象,修改的 key,修改的 v * @returns 返回源對象的一個(gè)代理 */ function watchObject(object, callback) { const handler = { get(_, k) { try { // 注意: 這里很關(guān)鍵,它為對象的字段也添加了代理 return new Proxy(v, Reflect.get(_, k)) } catch (err) { return Reflect.get(_, k) } }, set(_, k, v) { callback(_, k, v) return Reflect.set(_, k, v) }, } return new Proxy(object, handler) } // 模擬頁面的復(fù)選框列表 const hobbyMap = new Map() .set(1, '小說') .set(2, '動畫') .set(3, '電影') .set(4, '游戲') const user = { id: 1, // 保存興趣 id 的列表 hobbySet: new Set(), // 發(fā)送到后臺的興趣 id 拼接后的字符串,以都好進(jìn)行分割 hobby: '', } const proxy = watchObject(user, (_, k, v) => { if (k === 'hobbySet') { _.hobby = [..._.hobbySet].join(',') } }) function onClick(id) { proxy.hobbySet = proxy.hobbySet.has(id) ? proxy.hobbySet.delete(id) : proxy.hobbySet.add(id) } // 模擬兩次點(diǎn)擊 onClick(1) onClick(2) // 現(xiàn)在,user.hobby 的值將會自動更新 console.log(user.hobby) // 1,2
當(dāng)然,這里實(shí)現(xiàn)的 watchObject 函數(shù)還非常非常非常簡陋,如果有需要可以進(jìn)行更深度/強(qiáng)大的監(jiān)聽,可以嘗試自行實(shí)現(xiàn)一下啦!
缺點(diǎn)
說完了這些 Proxy 的使用場景,下面稍微來說一下它的缺點(diǎn)
運(yùn)行環(huán)境必須要 ES6 支持
這是一個(gè)不大不小的問題,現(xiàn)代的瀏覽器基本上都支持 ES6,但如果泥萌公司技術(shù)棧非常老舊的話(例如支持 IE6),還是安心吃土吧 #笑 #這種公司不離職等著老死
不能直接代理一些需要 this 的對象
這個(gè)問題就比較麻煩了,任何需要 this 的對象,代理之后的行為可能會發(fā)生變化。例如 Set 對象
const proxy = new Proxy(new Set([]), {}) proxy.add(1) // Method Set.prototype.add called on incompatible receiver [object Object]
是不是很奇怪,解決方案是把所有的 get 操作屬性值為 function 的函數(shù)都手動綁定 this
const proxy = new Proxy(new Set([]), { get(_, k) { const v = Reflect.get(_, k) // 遇到 Function 都手動綁定一下 this if (v instanceof Function) { return v.bind(_) } return v }, }) proxy.add(1)
關(guān)于ES6 Proxy如何在JavaScript中使用問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。