JavaScript 魔幻代理

創宇前端發表於2018-01-30

前言

什麼是代理?

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

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

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

這些都是代理。

什麼是 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 種可代理的操作,還需要讀者自行研究並實踐方可踏上終極魔幻之旅。

同學,我看好你。


參考連結:


關注微信公眾號:創宇前端(KnownsecFED),碼上獲取更多優質乾貨!

JavaScript 魔幻代理

相關文章