溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

ES6 Proxy如何在JavaScript中使用

發布時間:2021-01-26 16:02:23 來源:億速云 閱讀:204 作者:Leah 欄目:web開發

ES6 Proxy如何在JavaScript中使用?針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。

為函數添加特定的功能

 * 為異步函數添加自動超時功能
 * @param timeout 超時時間
 * @param action 異步函數
 * @returns 包裝后的異步函數
 */
function asyncTimeout(timeout, action) {
 return function(...args) {
 return Promise.race([
  Reflect.apply(action, this, args),
  wait(timeout).then(Promise.reject),
 ])
 }
}

一般而言,上面的代碼足以勝任,但問題就在這里,不一般的情況 -- 函數上面包含自定義屬性呢?
眾所周知,JavaScript 中的函數是一等公民,即函數可以被傳遞,被返回,以及,被添加屬性!

例如下面這個簡單的函數 get,其上有著 _name 這個屬性

const get = async i => i
get._name = 'get'

一旦使用上面的 asyncTimeout 函數包裹之后,問題便會出現,返回的函數中 _name 屬性不見了。這是當然的,畢竟實際上返回的是一個匿名函數。那么,如何才能讓返回的函數能夠擁有傳入函數參數上的所有自定義屬性呢?

一種方式是復制參數函數上的所有屬性,但這點實現起來其實并不容易,真的不容易,不信你可以看看 Lodash 的 clone 函數。那么,有沒有一種更簡單的方式呢?答案就是 Proxy,它可以代理對象的指定操作,除此之外,其他的一切都指向原對象。

下面是 Proxy 實現的 asyncTimeout 函數

/**
 * 為異步函數添加自動超時功能
 * @param timeout 超時時間
 * @param action 異步函數
 * @returns 包裝后的異步函數
 */
function asyncTimeout(timeout, action) {
 return new Proxy(action, {
 apply(_, _this, args) {
  return Promise.race([
  Reflect.apply(_, _this, args),
  wait(timeout).then(Promise.reject),
  ])
 },
 })
}

測試一下,是可以正常調用與訪問其上的屬性的

;(async () => {
 console.log(await get(1))
 console.log(get._name)
})()

好了,這便是吾輩最常用的一種方式了 -- 封裝高階函數,為函數添加某些功能。

代理對象的訪問

下面是一段代碼,用以在頁面上展示從后臺獲取的數據,如果字段沒有值則默認展示 ''

模擬一個獲取列表的異步請求

async function list() {
 // 此處僅為構造列表
 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: '幻想鄉' }),
 ]
}

嘗試直接通過解構為屬性賦予默認值,并在默認值實現這個功能

;(async () => {
 // 為所有為賦值屬性都賦予默認值 ''
 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 () => {
 // 為所有為賦值屬性都賦予默認值 ''
 const persons = (await list()).map(warp)
 console.log(persons)
})()

暫且先看一下這里的 warp 函數有什么問題?

這里是答案的分割線

  • 所有屬性需要預定義,不能運行時決定

  • 沒有指向原對象,后續的修改會造成麻煩

吾輩先解釋一下這兩個問題

  1. 所有屬性需要預定義,不能運行時決定

如果調用了 list[0].a 會發生什么呢?是的,依舊會是 undefined,因為 Reflect.ownKeys 也不能找到沒有定義的屬性(真*undefined),因此導致訪問未定義的屬性仍然會是 undefined 而非期望的默認值。

  1. 沒有指向原對象,后續的修改會造成麻煩

如果我們此時修改對象的一個屬性,那么會影響到原本的屬性么?不會,因為 warp 返回的對象已經是全新的了,和原對象沒有什么聯系。所以,當你修改時當然不會影響到原對象。

Pass: 我們當然可以直接修改原對象,但這很明顯不太符合我們的期望:顯示時展示默認值 '' -- 這并不意味著我們愿意在其他操作時需要 '',否則我們還要再轉換一遍。(例如發送編輯后的數據到后臺)

這個時候 Proxy 也可以派上用場,使用 Proxy 實現 warp 函數

function warp(obj) {
 const result = new Proxy(obj, {
 get(_, k) {
  const v = Reflect.get(_, k)
  if (v !== undefined) {
  return v
  }
  return ''
 },
 })
 return result
}

現在,上面的那兩個問題都解決了!

注: 知名的 GitHub 庫 immer 就使用了該特性實現了不可變狀態樹。

作為膠水橋接不同結構的對象

通過上面的例子我們可以知道,即便是未定義的屬性,Proxy 也能進行代理。這意味著,我們可以通過 Proxy 抹平相似對象之間結構的差異,以相同的方式處理類似的對象。

Pass: 不同公司的項目中的同一個實體的結構不一定完全相同,但基本上類似,只是字段名不同罷了。所以使用 Proxy 實現膠水橋接不同結構的對象方便我們在不同公司使用我們的工具庫!

嘛,開個玩笑,其實在同一個公司中不同的實體也會有類似的結構,也會需要相同的操作,最常見的應該是樹結構數據。例如下面的菜單實體和系統權限實體就很相似,也需要相同的操作 -- 樹 <=> 列表 相互轉換。

思考一下如何在同一個函數中處理這兩種樹節點結構

/**
 * 系統菜單
 */
class SysMenu {
 /**
 * 構造函數
 * @param {Number} id 菜單 id
 * @param {String} name 顯示的名稱
 * @param {Number} parent 父級菜單 id
 */
 constructor(id, name, parent) {
 this.id = id
 this.name = name
 this.parent = parent
 }
}
/**
 * 系統權限
 */
class SysPermission {
 /**
 * 構造函數
 * @param {String} uid 系統唯一 uuid
 * @param {String} label 顯示的菜單名
 * @param {String} parentId 父級權限 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

看起來似乎有點繁瑣,讓我們封裝一下

/**
 * 橋接對象不存在的字段
 * @param {Object} map 代理的字段映射 Map
 * @returns {Function} 轉換一個對象為代理對象
 */
function bridge(map) {
 /**
 * 為對象添加代理的函數
 * @param {Object} obj 任何對象
 * @returns {Proxy} 代理后的對象
 */
 return function(obj) {
 return new Proxy(obj, {
  get(target, k) {
  // 如果遇到被代理的屬性則返回真實的屬性
  if (Reflect.has(map, k)) {
   return Reflect.get(target, Reflect.get(map, k))
  }
  return Reflect.get(target, k)
  },
  set(target, k, v) {
  // 如果遇到被代理的屬性則設置真實的屬性
  if (Reflect.has(map, k)) {
   Reflect.set(target, Reflect.get(map, k), v)
   return true
  }
  Reflect.set(target, k, v)
  return true
  },
 })
 }
}

現在,我們可以用更簡單的方式來做代理了。

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 如何處理樹結構數據話,可以參考吾輩的JavaScript 處理樹數據結構

監視對象的變化

接下來,我們想想,平時是否有需要監視對象的變化,然后進行某些處理呢?

例如監視用戶復選框選中項列表的變化并更新對應的需要發送到后臺的 id 拼接字符串。

// 模擬頁面的復選框列表
const hobbyMap = new Map()
 .set(1, '小說')
 .set(2, '動畫')
 .set(3, '電影')
 .set(4, '游戲')
const user = {
 id: 1,
 // 保存興趣 id 的列表
 hobbySet: new Set(),
 // 發送到后臺的興趣 id 拼接后的字符串,以都好進行分割
 hobby: '',
}
function onClick(id) {
 user.hobbySet.has(id) ? user.hobbySet.delete(id) : user.hobbySet.add(id)
}

// 模擬兩次點擊
onClick(1)
onClick(2)

console.log(user.hobby) // ''

下面使用 Proxy 來完成 hobbySet 屬性改變后 hobby 自動更新的操作

/**
 * 深度監聽指定對象屬性的變化
 * 注:指定對象不能是原始類型,即不可變類型,而且對象本身的引用不能改變,最好使用 const 進行聲明
 * @param object 需要監視的對象
 * @param callback 當代理對象發生改變時的回調函數,回調函數有三個參數,分別是對象,修改的 key,修改的 v
 * @returns 返回源對象的一個代理
 */
function watchObject(object, callback) {
 const handler = {
 get(_, k) {
  try {
  // 注意: 這里很關鍵,它為對象的字段也添加了代理
  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)
}

// 模擬頁面的復選框列表
const hobbyMap = new Map()
 .set(1, '小說')
 .set(2, '動畫')
 .set(3, '電影')
 .set(4, '游戲')
const user = {
 id: 1,
 // 保存興趣 id 的列表
 hobbySet: new Set(),
 // 發送到后臺的興趣 id 拼接后的字符串,以都好進行分割
 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)
}
// 模擬兩次點擊
onClick(1)
onClick(2)

// 現在,user.hobby 的值將會自動更新
console.log(user.hobby) // 1,2

當然,這里實現的 watchObject 函數還非常非常非常簡陋,如果有需要可以進行更深度/強大的監聽,可以嘗試自行實現一下啦!

缺點

說完了這些 Proxy 的使用場景,下面稍微來說一下它的缺點

運行環境必須要 ES6 支持

這是一個不大不小的問題,現代的瀏覽器基本上都支持 ES6,但如果泥萌公司技術棧非常老舊的話(例如支持 IE6),還是安心吃土吧 #笑 #這種公司不離職等著老死

不能直接代理一些需要 this 的對象

這個問題就比較麻煩了,任何需要 this 的對象,代理之后的行為可能會發生變化。例如 Set 對象

const proxy = new Proxy(new Set([]), {})
proxy.add(1) // Method Set.prototype.add called on incompatible receiver [object Object]

是不是很奇怪,解決方案是把所有的 get 操作屬性值為 function 的函數都手動綁定 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)

關于ES6 Proxy如何在JavaScript中使用問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女