JavaScript 中的 ES6 Proxy

JavaScript 中的 ES6 Proxy

吾輩博客的原文地址: https://blog.rxliuli.com/p/c3...

場景

就算只是扮演,也會成爲真實的自個人一部分。對人類的精神來講,真實和虛假其實並無明顯的界限。入戲太深不是一件好事,但對於你來講並不成立,由於戲中的你纔是真正符合你的身份的你。現在的你是真實的,就算一開始你只是在模仿着這種形象,如今的你也已經成爲了這種形象。不管如何,你也不可能再回到過去了。

Proxy 代理,在 JavaScript 彷佛很陌生,卻又在生活中無處不在。或許有人在學習 ES6 的時候有所涉獵,但卻並未真正瞭解它的使用場景,平時在寫業務代碼時也不會用到這個特性。git

相比於文縐縐的定義內容,想必咱們更但願瞭解它的使用場景,使其在真正的生產環境發揮強大的做用,而不只僅是做爲一個新的特性 -- 而後,實際中徹底沒有用到!github

  • 爲函數添加特定的功能
  • 代理對象的訪問
  • 做爲膠水橋接不一樣結構的對象
  • 監視對象的變化
  • 還有更多。。。

若是你尚未了解過 Proxy 特性,能夠先去 MDN Proxy 上查看基本概念及使用。瀏覽器

爲函數添加特定的功能

下面是一個爲異步函數自動添加超時功能的高階函數,咱們來看一下它有什麼問題數據結構

/**
 * 爲異步函數添加自動超時功能
 * @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 中的函數是一等公民,即函數能夠被傳遞,被返回,以及,被添加屬性!app

例以下面這個簡單的函數 get,其上有着 _name 這個屬性異步

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

一旦使用上面的 asyncTimeout 函數包裹以後,問題便會出現,返回的函數中 _name 屬性不見了。這是固然的,畢竟實際上返回的是一個匿名函數。那麼,如何才能讓返回的函數可以擁有傳入函數參數上的全部自定義屬性呢?
一種方式是複製參數函數上的全部屬性,但這點實現起來其實並不容易,真的不容易,不信你能夠看看 Lodash 的 clone 函數。那麼,有沒有一種更簡單的方式呢?答案就是 Proxy,它能夠代理對象的指定操做,除此以外,其餘的一切都指向原對象。
下面是 Proxy 實現的 asyncTimeout 函數async

/**
 * 爲異步函數添加自動超時功能
 * @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 而非指望的默認值。
  2. 沒有指向原對象,後續的修改會形成麻煩
    若是咱們此時修改對象的一個屬性,那麼會影響到本來的屬性麼?不會,由於 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)

總結

Proxy 是個很強大的特性,可以讓咱們實現一些曾經難以實現的功能(因此這就是你不支持 ES5 的理由?#打),就連 Vue3+ 都開始使用 Proxy 實現了,你還有什麼理由在意上古時期的 IE 而不用呢?(v^_^)v

相關文章
相關標籤/搜索