JavaScript 設計模式及程式碼實現——代理模式

2022-09-01 21:03:35

代理模式

1 定義

為其他物件提供一種代理以控制對這個物件的存取

在某些情況下,一個物件不適合或者不能直接參照另一個物件,而代理物件可以在使用者端和目標物件之間起到中介的作用。

2 應用舉例

2.1 快取代理

現在我們有一個可以查詢城市經緯度的函數:

const getLatLng = (address) => {
  if (address === "Beijing") {
    return "北京經緯度";
  } else if (address === "Hangzhou") {
    return "杭州經緯度";
  } else if (address === "Shanghai") {
    return "上海經緯度";
  } else if (address === "Nanjing") {
    return "南京經緯度";
  } else {
    return "";
  }
};

如果我們多次查詢南京的經緯度,每次都要經過 4 次判斷,我們通過 getLatLngProxy 函數將查詢結果快取下來,從而避免多次重複判斷

const getLatLngProxy = ((fn) => {
  const geoCache = {};
  return (address) => {
    console.log("快取=" + geoCache[address]);
    return (geoCache[address] ??= fn(address));
  };
})(getLatLng);

getLatLngProxy("Nanjing"); // 快取=undefined
getLatLngProxy("Nanjing"); // 快取=南京經緯度

4 次判斷看不出什麼,但是如果 getLatLng 中的操作不是判斷,而是需要很複雜的計算,需要消耗很長時間,這時快取的優勢就很明顯了

我們在不修改原函數的前提下,通過高階函數建立了一個擁有快取效果的代理函數

2.2 Vue2 響應式原理——資料代理

如果你學習過 Vue2 響應式原理,一定知道其中重要的一環:資料代理。不知道也沒關係,下面舉個簡單的栗子來說明一下。

const obj = {
  name: "JiMing",
};

let name = obj.name; // 存取 obj.name
obj.name = "Ji"; // 修改 obj.name

假設現在有一個物件 obj,如果我想在存取或修改obj.name時做一些額外的操作,比如列印資訊到控制檯,該如何實現?

JS 提供了 **Object.defineProperty()**方法,該方法可以在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件。

我們可以利用這個 API 在代理物件上新增目標物件的同名屬性,同時新增額外的操作

const proxyObj = {}; // 代理物件

Object.defineProperty(proxyObj, "name", {
  get() {
    console.log("存取了 obj.name");
    return obj.name;
  },
  set(val) {
    console.log("修改了 obj.name");
    obj.name = val;
  },
});

現在我們只要存取或修改代理物件的 name 屬性,就可以實現存取或修改obj.name,同時列印資訊到控制檯

Vue2 就是通過此方法將 data 中的屬性新增到 vm 範例上,因此我們可以使用this.屬性名來存取屬性,並且和我們列印資訊到控制檯一樣,Vue 也新增了額外的操作比如通過 set 實現資料監聽,從而完成響應式變化

小結

  1. 根據單一職責原則:就一個類(通常也包括物件和函數)而言,應該只有一個職責。
  2. 我們利用代理模式讓代理物件承擔額外功能,不破壞目標物件,從而不至於讓目標物件變得臃腫而降低複用性和可維護性

3 JavaScript Proxy

JS 提供了 Proxy 類,可以非常方便地建立代理物件,從而實現基本操作的攔截和自定義(如屬性查詢、賦值、列舉、函數呼叫等)。

Proxy 的用法非常簡單:

const proxy = new Proxy(target, handler)
// target
// 要使用 Proxy 包裝的目標物件(可以是任何型別的物件,包括原生陣列,函數,甚至另一個代理)。

// handler
// 一個通常以函數作為屬性的物件,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。

詳見 MDN 檔案 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

3.1 Proxy 實現快取代理

handler 物件有很多可選方法,其中 apply 方法用來攔截函數呼叫操作

apply 方法接受 3 個引數,詳見https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply

// apply 的 3 個引數
// target 目標物件
// thisArg 被呼叫時的上下文物件
// argArray 被呼叫時的引數陣列。
const geoCache = {};
const getLatLngProxy = new Proxy(getLatLng, {
  apply(target, thisArg, argArray) {
    const address = argArray[0];
    console.log("快取=" + geoCache[address]);
    return (geoCache[address] ??= target(address));
  },
});

getLatLngProxy("Hangzhou"); // 快取=undefined
getLatLngProxy("Hangzhou"); // 快取=杭州經緯度

我們呼叫代理函數 getLatLngProxy 時會觸發 apply 方法

注意這裡我們的目標物件是 getLatLng 函數,即 apply 的 target 就是 getLatLng 的參照,因此我們呼叫 target 就相當於呼叫 getLatLng

3.2 Vue3 的資料代理

Vue2 使用 Object.defineProperty 來實現資料代理,但是這個方法存在侷限性,比如:普通屬性我們可以通過 set 方法獲取到其變化的資訊,但是使用 push 方法改變陣列,無法通過 set 獲取到。

因此 Vue3 改用 Proxy 來實現資料代理

和 apply 方法類似,handler 中還有 get 和 set 方法用來攔截對屬性存取、修改的操作

詳見

const obj = {
  name: "JiMing",
};

const proxyObj = new Proxy(obj, {
  // target 目標物件 即 obj
  // property 被獲取的屬性名。
  get(target, property) {
    console.log(`存取了 obj.${property}`);
    return target[property];
  },
  // target 目標物件 即 obj
  // 將被設定的屬性名
  set(target, property, value) {
    console.log(`修改了 obj.${property}`);
    target[property] = value;
  },
});

proxyObj.name; // 存取了 obj.name
proxyObj.name = "Ji"; // 修改了 obj.name

完結,撒花