一文理解 ES6 中的代理模式——Proxy

一、介紹

定義: 用於定義基本操作的自定義行爲

本質: 修改的是程序默認形爲,就形同於在編程語言層面上做修改,屬於元編程(meta programming)

元編程(Metaprogramming,又譯超編程,是指某類計算機程序的編寫,這類計算機程序編寫或者操縱其它程序(或者自身)作爲它們的數據,或者在運行時完成部分本應在編譯時完成的工作

一段代碼來理解

#!/bin/bash
# metaprogram
echo '#!/bin/bash' >program
for ((I=1; I<=1024; I++)) do
    echo "echo $I" >>program
done
chmod +x program

這段程序每執行一次能幫我們生成一個名爲program的文件,文件內容爲 1024 行echo,如果我們手動來寫 1024 行代碼,效率顯然低效

Proxy 亦是如此,用於創建一個對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等)

二、用法

Proxy爲 構造函數,用來生成 Proxy實例

var proxy = new Proxy(target, handler)

參數

target表示所要攔截的目標對象(任何類型的對象,包括原生數組,函數,甚至另一個代理))

handler通常以函數作爲屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行爲

handler 解析

關於handler攔截屬性,有如下:

Reflect

若需要在Proxy內部調用對象的默認行爲,建議使用Reflect,其是ES6中操作對象而提供的新 API

基本特點:

下面我們介紹proxy幾種用法:

get()

get接受三個參數,依次爲目標對象、屬性名和 proxy 實例本身,最後一個參數可選

var person = {
  name: "張三"
};

var proxy = new Proxy(person, {
  get: function(target, propKey) {
    return Reflect.get(target,propKey)
  }
});

proxy.name // "張三"

get能夠對數組增刪改查進行攔截,下面是試下你數組讀取負數的索引

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a''b''c');
arr[-1] // c

注意:如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則會報錯

const target = Object.defineProperties({}{
  foo: {
    value: 123,
    writable: false,
    configurable: false
  },
});

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};

const proxy = new Proxy(target, handler);

proxy.foo
// TypeError: Invariant check failed

set()

set方法用來攔截某個屬性的賦值操作,可以接受四個參數,依次爲目標對象、屬性名、屬性值和 Proxy 實例本身

假定Person對象有一個age屬性,該屬性應該是一個不大於 200 的整數,那麼可以使用Proxy保證age的屬性值符合要求

let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 對於滿足條件的 age 屬性以及其他屬性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯

如果目標對象自身的某個屬性,不可寫且不可配置,那麼set方法將不起作用

const obj = {};
Object.defineProperty(obj, 'foo'{
  value: 'bar',
  writable: false,
});

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = 'baz';
  }
};

const proxy = new Proxy(obj, handler);
proxy.foo = 'baz';
proxy.foo // "bar"

注意,嚴格模式下,set代理如果沒有返回true,就會報錯

'use strict';
const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
    // 無論有沒有下面這一行,都會報錯
    return false;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
// TypeError: 'set' on proxy: trap returned falsish for property 'foo'

deleteProperty()

deleteProperty方法用於攔截delete操作,如果這個方法拋出錯誤或者返回false,當前屬性就無法被delete命令刪除

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    Reflect.deleteProperty(target,key)
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`無法刪除私有屬性`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 無法刪除私有屬性

注意,目標對象自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯

取消代理

Proxy.revocable(target, handler);

三、使用場景

Proxy其功能非常類似於設計模式中的代理模式,常用功能如下:

使用 Proxy 保障數據類型的準確性

let numericDataStore = { count: 0, amount: 1234, total: 14 };
numericDataStore = new Proxy(numericDataStore, {
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("屬性只能是number類型");
        }
        return Reflect.set(target, key, value, proxy);
    }
});

numericDataStore.count = "foo"
// Error: 屬性只能是number類型

numericDataStore.count = 333
// 賦值成功

聲明瞭一個私有的 apiKey,便於 api 這個對象內部的方法調用,但不希望從外部也能夠訪問 api._apiKey

let api = {
    _apiKey: '123abc456def',
    getUsers: function(){ },
    getUser: function(userId){ },
    setUser: function(userId, config){ }
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
    get(target, key, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} 不可訪問.`);
        } return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} 不可修改`);
        } return Reflect.get(target, key, value, proxy);
    }
});

console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都拋出錯誤

還能通過使用Proxy實現觀察者模式

觀察者模式(Observer mode)指的是函數自動觀察數據對象,一旦對象有變化,函數就會自動執行

observable函數返回一個原始對象的 Proxy 代理,攔截賦值操作,觸發充當觀察者的各個函數

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

觀察者函數都放進Set集合,當修改obj的值,在會set函數中攔截,自動執行Set所有的觀察者

參考文獻

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uMpveY8at1vkhbEZvktfQw