讀完這篇文章,就能擁有炫同事一臉的超能力:JavaScript 魔幻代理

前言

什麼是代理?

上小學的時候,李小紅來你家叫你出去玩,第一個迴應的不是你自己,是你媽:“王小明在家寫作業,今天不出去!”

上中學的時候,趙二虎帶着小弟們放學在校門口等着揍你,走在前面的不是你自己,是二虎他爸:“考試沒及格還學會裝黑社會了!”拎起二虎就是一頓胖揍。

上了大學,躺在宿舍裏的牀上,好餓。出門買飯並交代好不要蔥蒜多放辣最後還直接端到牀上的不是你自己,是快遞小哥。

這些都是代理。

什麼是 JavaScript 代理?

用官方的洋文來說,是 Proxy

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

通過 Proxy 我們可以攔截並改變一個對象的幾乎所有的根本操作,包括但不限於屬性查找、賦值、枚舉、函數調用等等。

在生活中,通過代理我們可以自動屏蔽小紅的邀請、自動趕走二虎的威脅、自動買好乾淨的飯端到牀上。在 JavaScript 世界裏,代理也可以幫你做類似的事情,接下來讓我們一起琢磨一番。

初識代理:Hello World

以小學經歷爲例子,心裏是喜歡小紅的,於是我們定義:

const me = { name: '小明', like: '小紅' }

這個時候如果調用 console.log(me.like),結果必然是 小紅。然而生活並不是這樣,作爲一個未成年人,總是有各種的代理人圍繞在你身邊,比如這樣:

const meWithProxy = new Proxy(me, {
  get(target, prop) {
    if (prop === 'like') {
      return '學習';
    }
    return target[prop];
  }
});

這個時候如果調用 console.log(me.like) 依然是 小紅 ,因爲真心不會說謊。但當我們調用 console.log(meWithProxy.like) 的時候,就會可恥的輸出 學習 ,告訴大家說我們喜歡的是 學習

小試牛刀:不要停止我的音樂

剛纔我們簡單瞭解了代理能夠攔截對象屬性的獲取,可以隱藏真實的屬性值而返回代理想要返回的結果,那麼對於對象屬性的賦值呢?讓我們一起來看看。

假設你正在聽音樂:

const me = { name: '小明', musicPlaying: true }

此時如果我們執行 me.musicPlaying = false 這樣就輕而易舉地停止了你的音樂,那麼如果我們掛上代理人:

const meWithProxy = new Proxy(me, {
  set(target, prop, value) {
    if (prop === 'musicPlaying' && value !== true) {
      throw Error('任何妄圖停止音樂的行爲都是耍流氓!');
    }
    target[prop] = value;
  }
});

這時候如果我們執行 me.musicPlaying = false,就會被毫不留情地掀了桌子:

> meWithProxy.musicPlaying = false
Error: 任何妄圖停止音樂的行爲都是耍流氓!
    at Object.set (repl:4:13)
>

釋放魔法:封裝全宇宙所有 RESTful API

現在我們已經知道通過 Proxy 可以攔截屬性的讀寫操作,那然後呢?沒什麼用?

僅僅是攔截屬性的讀寫操作,的確沒有太大的發揮空間,或許可以方便的做一些屬性賦值校驗工作等等。但是,或許你還沒有意識到一個驚人的祕密:Proxy 在攔截屬性讀寫操作時,並不在乎屬性是否真的存在!

那麼,也就是說:利用 Proxy,我們可以攔截並不存在的屬性的讀取。

再進一步思考:利用 Proxy,我們可以在屬性讀取的那一瞬間,動態構造返回結果。

然而,屬性並不侷限於字符串、布爾值,屬性可以是對象、函數、任何東西。

至此,你想到了什麼?

沒想到?不要緊!根據剛纔的分析,讓我們一起通過下面 17 行代碼,來封裝全宇宙所有的 RESTful API !

import axios from 'axios';
const api = new Proxy({}, {
  get(target, prop) {
    const method = /^[a-z]+/.exec(prop)[0];
    const path = '/' + prop
          .substring(method.length)
          .replace(/([a-z])([A-Z])/g, '$1/$2')
          .replace(/\$/g, '/$/')
          .toLowerCase();
    return (...args) => { // <------ 返回動態構造的函數!
      const url = path.replace(/\$/g, () => args.shift());
      const options = args.shift() || {};
      console.log('Requesting: ', method, url, options);
      return axios({ method, url,  ...options });
    }
  }
});

定義了 api 這個代理之後,我們就可以像下面這樣調用:

api.get()
// GET /

api.getUsers()
// 獲取所有用戶
// GET /users

api.getUsers$Books(42)
// 獲取 ID 爲 42 的用戶的所有書籍
// GET /users/42/books

api.getUsers$Books(42, { params: { page: 2 } })
// 獲取 ID 爲 42 的用戶的所有書籍的第二頁
// GET /users/42/books?page=2

api.postUsers({ data: { name: '小明' } })
// 創建名字爲 小明 的用戶
// POST /users Payload { name: '小明' }

以上所有的函數都在你調用的那一瞬間,通過代理人的魔法之手動態生成,供我們隨意取用。

簡潔、優雅,哇~ 真是太棒啦!

終極魔幻:通讀代理人的魔法祕笈

到此,我們僅僅使用 Proxy 改造了對象的屬性獲取、賦值操作,而對於 Proxy 來說,只是冰山一角。

Proxy 的基本語法如下:

new Proxy(target, handler)

其中 target 是即將被代理的對象(比如:想要出門找小紅玩耍的 me),handler 就是代理的魔法之手,用來攔截、改造 target 的行爲。

對於 handler 對象,我們剛纔僅僅用到了 getset 函數,而實際上一共有 13 種可代理的操作:

  • handler.getPrototypeOf()

    在讀取代理對象的原型時觸發該操作,比如在執行 Object.getPrototypeOf(proxy) 時。

  • handler.setPrototypeOf()

    在設置代理對象的原型時觸發該操作,比如在執行 Object.setPrototypeOf(proxy, null) 時。

  • handler.isExtensible()

    在判斷一個代理對象是否是可擴展時觸發該操作,比如在執行 Object.isExtensible(proxy) 時。

  • handler.preventExtensions()

    在讓一個代理對象不可擴展時觸發該操作,比如在執行 Object.preventExtensions(proxy) 時。

  • handler.getOwnPropertyDescriptor()

    在獲取代理對象某個屬性的屬性描述時觸發該操作,比如在執行 Object.getOwnPropertyDescriptor(proxy, "foo") 時。

  • handler.defineProperty()

    在定義代理對象某個屬性時的屬性描述時觸發該操作,比如在執行 Object.defineProperty(proxy, "foo", {}) 時。

  • handler.has()

    在判斷代理對象是否擁有某個屬性時觸發該操作,比如在執行 "foo" in proxy 時。

  • handler.get()

    在讀取代理對象的某個屬性時觸發該操作,比如在執行 proxy.foo 時。

  • handler.set()

    在給代理對象的某個屬性賦值時觸發該操作,比如在執行 proxy.foo = 1 時。

  • handler.deleteProperty()

    在刪除代理對象的某個屬性時觸發該操作,比如在執行 delete proxy.foo 時。

  • handler.ownKeys()

    在獲取代理對象的所有屬性鍵時觸發該操作,比如在執行 Object.getOwnPropertyNames(proxy) 時。

  • handler.apply()

    在調用一個目標對象爲函數的代理對象時觸發該操作,比如在執行 proxy() 時。

  • handler.construct()

    在給一個目標對象爲構造函數的代理對象構造實例時觸發該操作,比如在執行new proxy() 時。

對於以上 13 種可代理的操作,還需要讀者自行研究並實踐方可踏上終極魔幻之旅。

同學,我看好你。


參考鏈接:


文 / 王小明

本文已由作者授權發佈,版權屬於創宇前端。歡迎註明出處轉載本文。本文鏈接:https://knownsec-fed.com/2018...

想要訂閱更多來自知道創宇開發一線的分享,請搜索關注我們的微信公衆號:創宇前端(KnownsecFED)。歡迎留言討論,我們會儘可能回覆。

歡迎點贊、收藏、留言評論、轉發分享和打賞支持我們。打賞將被完全轉交給文章作者。

感謝您的閱讀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章