1. 程式人生 > 程式設計 >JS帶你深入領略Proxy的世界

JS帶你深入領略Proxy的世界

1. Proxy 的基本結構

Proxy 的基本使用方式:

/**
 * target: 表示要代理的目標,可以是object,array,function型別
 * handler: 是一個物件,可以編寫各種代理的方法
 */
const proxy = new Proxy(target,handler);

例如我們想要代理一個物件,可以通過設定 get 和 set 方法來代理獲取和設定資料的操作:

const person = {
  name: 'wenzi',age: 20,};
const personProxy = new Proxy(person,{
  get(target,key,receiver) {
    console.log(`get value by ${key}`);
    return target[key];
  },set(target,value) {
    console.log(`set ${key},old value ${target[key]} to ${value}`);
    target[key] = value;
  },});

Proxy 僅僅是一個代理,personProxy 上有 person 所有的屬性和方法。我們通過personProxy獲取和設定 name 時,就會有相應的 log 輸出:

personProxy.name; // "wenzi"
// log: get value by name

personProxy.name = 'hello';
// log: set name,old value wenzi to hello

並且通過 personProxy 設定資料時,代理的原結構裡的資料也會發生變化。我們列印下 person,可以發現欄位 name 的值  也變成了hello:

console.log(person); // {name: "hello",age: 20}

Proxy 的第 2 個引數 handler 除了可以設定 get 和 set 方法外,還有更多豐富的方法:

1.get(target,propKey,receiver):攔截物件屬性的讀取,比如 proxy.foo 和 proxy['foo']。

2.set(target,value,receiver):攔截物件屬性的設定,比如 proxy.foo = v 或 proxy['foo'] = v,返回一個布林值。

3.has(target,propKey):攔截 propKey in proxy 的操作,返回一個布林值。

4.deleteProperty(target,propKey):攔截 delete proxy[propKey]的操作,返回一個布林值。

5.ownKeys(target):攔截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 迴圈,返回一個數組。該方法返回目標物件所有自身的屬性的屬性名,而 Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。

6.getOwnPropertyDescriptor(target,propKey):攔截 Object.getOwnPropertyDescriptor(proxy,propKey),返回屬性的描述物件。

7.defineProperty(target,propDesc):攔截 Object.defineProperty(proxy,propDesc)、Object.defineProperties(proxy,propDescs),返回一個布林值。

8.preventExtensions(target):攔截 Object.preventExtensions(proxy),返回一個布林值。

9.getPrototypeOf(target):攔截 Object.getPrototypeOf(proxy),返回一個物件。

10.isExtensible(target):攔截 Object.isExtensible(proxy),返回一個布林值。

11.setPrototypeOf(target,proto):攔截 Object.setPrototypeOf(proxy,proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。

12.apply(target,object,args):攔截 Proxy 例項作為函式呼叫的操作,比如 proxy(...args)、proxy.call(object,...args)、proxy.apply(...)。

13.construct(target,args):攔截 Proxy 例項作為建構函式呼叫的操作,比如 new proxy(...args)。

如我們通過 delete 刪除其中一個元素時,可以通過deleteProperty()方法來攔截這個操作。還是上面代理 person 的程式碼,我們新增一個 deleteProperty:

const person = {
  name: 'wenzi',{
  // 忽略get和set方法,與上面一樣
  // ...
  deleteProperty(target,receiver) {
    console.log(`delete key ${key}`);
    delete target[key];
  },});

當執行 delete 操作時:

delete personProxy['age'];
// log: delete key age

2. Proxy 與 Reflect

Proxy 與 Reflect 可以說形影不離了,Reflect 裡所有的方法和使用方式與 Proxy 完全一樣。

例如上面 Proxy 裡的 get(),set()和 deleteProperty()方法我們都是直接操作原代理物件的,這裡我們改成使用Reflect來操作:

const personProxy = new Proxy(person,receiver) {
    console.log(`get value by ${key}`);
    return Reflect.get(target,receiver);
  },receiver) {
    console.log(`set ${key},old value ${target[key]} to ${value}`);
    return Reflect.set(target,deleteProperty(target,receiver) {
    console.log(`delete key ${key}`);
    return Reflect.deleteProperty(target,});

可以發現完美地實現這些功能。

JS帶你深入領略Proxy的世界

3. 代理陣列

我們在之前的文章 vue 中對陣列特殊的操作 中,討論過 Vue 為什麼沒有使用Object.defineProperty來劫持資料,而是重寫了 Array 原型鏈上的幾個方法,通過這幾個方法來實現 Vue 模板中資料的更新。

但若 Proxy 的話,就可以直接代理陣列:

const arr = [1,2,3,4];
const arrProxy = new Proxy(arr,receiver) {
    console.log('arrProxy.get',target,key);
    return Reflect.get(target,receiver) {
    console.log('arrProxy.set',value);
    return Reflect.set(target,key) {
    console.log('arrProxy.deleteProperty',key);
    return Reflect.deleteProperty(target,key);
  },});

現在我們再來操作一下代理後的陣列 arrProxy 看下:

arrProxy[2] = 22; // arrProxy.set (4) [1,4] 2 22
arrProxy[3]; // arrProxy.get (4) [1,22,4] 3
delete arrProxy[2]; // arrProxy.deleteProperty (4) [1,4] 2
arrProxy.push(5); // push操作比較複雜,這裡進行了多個get()和set()操作
arrProxy.length; // arrProxy.get (5) [1,empty,4,5] length

可以看到無論獲取、刪除還是修改資料,都可以感知到。還有陣列原型鏈上的一些方法,如:

1.push()

2.pop()

3.shift()

4.unshift()

5.splice()

6.sort()

7.reverse()

也都能通過 Proxy 中的代理方法劫持到。

concat()方法比較特殊的是,他是一個賦值操作,並不改變原陣列,因此在呼叫 concat()方法運算元組時,如果沒有賦值操作,那麼這裡只有 get()攔截到。

JS帶你深入領略Proxy的世界

4. 代理函式

Proxy 中還有一個apply()方法,是表示自己作為函式呼叫時,被攔截的操作。

const getSum = (...args) => {
  if (!args.every((item) => typeof item === 'number')) {
    throw new TypeError('引數應當均為number型別');
  }
  return args.reduce((sum,item) => sum + item,0);
};
const fnProxy = new Proxy(getSum,{
  /**
   * @params {Fuction} target 代理的物件
   * @params {any} ctx 執行的上下文
   * @params {any} args 引數
   */
  apply(target,ctx,args) {
    console.log('ctx',ctx);
    console.log(`execute fn ${getSum.name},args: ${args}`);
    return Reflect.apply(target,args);
  },});

執行 fnProxy:

// 10,ctx為undefined,log: execute fn getSum,args: 1,4
fnProxy(1,4);

// ctx為undefined,Uncaught TypeError: 引數應當均為number型別
fnProxy(1,'4');

// 10,ctx為window,4
fnProxy.apply(window,[1,4]);

// 6,3
fnProxy.call(window,1,3);

// 6,ctx為person,3
fnProxy.apply(person,3]);

5. 一些簡單的應用場景

我們知道 Vue3 裡已經用 Proxy 重寫了響應式系統,mobx 也已經用了 Proxy 模式。在可見的未來,會有更多的 Proxy 的應用場景,我們這裡也稍微講解幾個。

5.1 統計函式被呼叫的上下文和次數

這裡我們用 Proxy 來代理函式,然後函式被呼叫的上下文和次數。

const countExecute = (fn) => {
  let count = 0;

  return new Proxy(fn,{
    apply(target,args) {
      ++count;
      console.log('ctx上下文:',ctx);
      console.log(`${fn.name} 已被呼叫 ${count} 次`);
      return Reflect.apply(target,args);
    },});
};

現在我們來代理下剛才的getSum()方法:

const getSum = (...args) => {
  if (!args.every((item) => typeof item === 'number')) {
    throw new TypeError('引數應當均為numbe程式設計客棧r型別');
  }
  return args.reduce((sum,0);
};
www.cppcns.com
const useSum = countExecute(getSum);

useSum(1,3); // getSum 已被呼叫 1 次

useSum.apply(window,[2,4]); // getSum 已被呼叫 2 次

useSum.call(person,5); // getSum 已被呼叫 3 次

5.2 實現一個防抖功能

基於上面統計函式呼叫次數的功能,也給我們實現一個函式的防抖功能添加了靈感。

const throttleByProxy = (fn,rate) => {
  let lastTime = 0;
  return new Proxy(fn,args) {
      const now = Date.now();
      if (now - lastTime > rate) {
        lastTime = now;
        return Reflect.apply(target,args);
      }
    },});
};

const logTimeStamp = () => cxBjeoonsole.log(Date.now());
window.addEventListener('scroll',throttleByProxy(lohttp://www.cppcns.comgTimeStamp,300));

logTimeStamp()至少需要 300ms 才能執行一次。

5.3 實現觀察者模式

我們在這裡實現一個最簡單類 mobx 觀察者模式。

const list = new Set();
const observe = (fn) => list.add(fn);
const observable = (obj) => {
  return new Proxy(obj,{
    set(target,receiver) {
      const result = Reflect.set(target,receiver);
      list.forEach((observer) => observer());
      return result;www.cppcns.com
    },});
};
const person = observable({ name: 'wenzi',age: 20 });
const App = () => {
  console.log(`App -> name: ${person.name},age: ${person.age}`);
};
observe(App);

person就是使用 Proxy 創建出來的代理物件,每當 person 中的屬性發生變化時,就會執行 App()函式。這樣就實現了一個簡單的響應式狀態管理。

6. Proxy 與 Object.defineProperty 的對比

上面很多例子用Object.defineProperty也都是可以實現的。那麼這兩者都各有什麼優缺點呢?

6.1 Object.defineProperty 的優劣

Object.defineProperty的相容性可以說比 Proxy 要好很多,出特別低的 IE6,IE7 瀏覽器外,其他瀏覽器都有支援。

但 Object.defineProperty 支援的方法很多,並且主要是基於屬性進行攔截的。因此在 Vue2 中只能重寫 Array 原型鏈上的方法,來運算元組。

6.2 Proxy 的優劣

Proxy與上面的正好相反,Proxy 是基於物件來進行代理的,因此可代理更多的型別,例如 Object,Array,Function 等;而且代理的方法也多了很多。

劣勢就是相容性不太好,即使用 polyfill,也無法完美的實現。

7. 總結

Proxy 能實現的功能還有很多,後面我們也會繼續進行探索,並且儘可能去了解下基於 Proxy 實現的類庫,例如 mobx5 的原始碼和實現原理等。

以上就是js帶你深入領略Proxy的世界的詳細內容,更多關於JS中的代理Proxy的資料請關注我們其它相關文章!