ES6之Proxy 的巧用

摘要: Proxy的騷操做。javascript

Fundebug經受權轉載,版權歸原做者全部。html

Proxy 介紹

使用Proxy,你能夠將一隻貓假裝成一隻老虎。下面大約有6個例子,我但願它們能讓你相信,Proxy 提供了強大的 Javascript 元編程。前端

儘管它不像其餘ES6功能用的廣泛,但Proxy有許多用途,包括運算符重載對象模擬簡潔而靈活的API建立對象變化事件,甚至Vue 3背後的內部響應系統提供動力vue

Proxy用於修改某些操做的默認行爲,也能夠理解爲在目標對象以前架設一層攔截,外部全部的訪問都必須先經過這層攔截,所以提供了一種機制,能夠對外部的訪問進行過濾和修改。這個詞的原理爲代理,在這裏能夠表示由它來「代理」某些操做,譯爲「代理器」。java

ES6原生提供了Proxy構造函數,用來生成Proxy實例。node

var proxy = new Proxy(target, handler);

Proxy對象的全部用法,都是上面的這種形式。不一樣的只是handle參數的寫法。其中new Proxy用來生成Proxy實例,target是表示所要攔截的對象,handle是用來定製攔截行爲的對象。git

下面是 Proxy 最簡單的例子是,這是一個有陷阱的代理,一個get陷阱,老是返回42es6

let target = {
    x: 10,
    y: 20
};

let hanler = {
    get: (obj, prop) => 42
};

target = new Proxy(target, hanler);

target.x; //42
target.y; //42
target.x; // 42

結果是一個對象將爲任何屬性訪問操做都返回「42」。 這包括target.xtarget['x']Reflect.get(target, 'x')等。github

可是,Proxy 陷阱固然不限於屬性的讀取。 它只是十幾個不一樣陷阱中的一個:golang

Proxy 用例

默認值/「零值」

在 Go 語言中,有零值的概念,零值是特定於類型的隱式默認結構值。其思想是提供類型安全的默認基元值,或者用gopher的話說,給結構一個有用的零值。

雖然不一樣的建立模式支持相似的功能,但Javascript沒法用隱式初始值包裝對象。Javascript中未設置屬性的默認值是undefined。但 Proxy 能夠改變這種狀況。

const withZeroValue = (target, zeroValue) =>
    new Proxy(target, {
        get: (obj, prop) => (prop in obj ? obj[prop] : zeroValue)
    });

函數withZeroValue 用來包裝目標對象。 若是設置了屬性,則返回屬性值。 不然,它返回一個默認的**「零值」**。

從技術上講,這種方法也不是隱含的,但若是咱們擴展withZeroValue,以Boolean (false), Number (0), String (""), Object ({}),Array ([])等對應的零值,則多是隱含的。

let pos = {
    x: 4,
    y: 19
};

console.log(pos.x, pos.y, pos.z); // 4, 19, undefined

pos = withZeroValue(pos, 0);

console.log(pos.z, pos.y, pos.z); // 4, 19, 0

此功能可能有用的一個地方是座標系。 繪圖庫能夠基於數據的形狀自動支持2D和3D渲染。 不是建立兩個單獨的模型,而是始終將z默認爲 0 而不是undefined,這多是有意義的。

負索引數組

在JS中獲取數組中的最後一個元素方式經過寫的很冗長且重複,也容易出錯。 這就是爲何有一個TC39提案定義了一個便利屬性Array.lastItem來獲取和設置最後一個元素。

其餘語言,如Python和Ruby,使用負組索引更容易訪問最後面的元素。例如,能夠簡單地使用arr[-1]替代arr[arr.length-1]訪問最後一個元素。

使用 Proxy 也能夠在 Javascript 中使用負索引。

const negativeArray = els =>
    new Proxy(els, {
        get: (target, propKey, receiver) =>
            Reflect.get(
                target,
                +propKey < 0 ? String(target.length + +propKey) : propKey,
                receiver
            )
    });

一個重要的注意事項是包含handler.get的陷阱字符串化全部屬性。 對於數組訪問,咱們須要將屬性名稱強制轉換爲Numbers,這樣就可使用一元加運算符簡潔地完成。

如今[-1]訪問最後一個元素,[-2]訪問倒數第二個元素,以此類推。

const unicorn = negativeArray(["🐴", "🎂", "🌈"]);

unicorn[-1]; // '🌈'

隱藏屬性

衆所周知 JS 沒有私有屬性。 Symbol最初是爲了啓用私有屬性而引入的,但後來使用像Object.getOwnPropertySymbols這樣的反射方法進行了淡化,這使得它們能夠被公開發現。

長期以來的慣例是將私有屬性命名爲前下劃線_,有效地標記它們「不要訪問」。Prox 提供了一種稍微更好的方法來屏蔽這些屬性。

const hide = (target, prefix = "_") =>
    new Proxy(target, {
        has: (obj, prop) => !prop.startsWith(prefix) && prop in obj,
        ownKeys: obj =>
            Reflect.ownKeys(obj).filter(
                prop => typeof prop !== "string" || !prop.startsWith(prefix)
            ),
        get: (obj, prop, rec) => (prop in rec ? obj[prop] : undefined)
    });

hide函數包裝目標對象,並使得從in運算符和Object.getOwnPropertyNames等方法沒法訪問帶有下劃線的屬性。

let userData = hide({
    firstName: "Tom",
    mediumHandle: "@tbarrasso",
    _favoriteRapper: "Drake"
});

userData._favoriteRapper(
    // undefined
    "_favoriteRapper" in userData
); // false

更完整的實現還包括諸如deletePropertydefineProperty之類的陷阱。 除了閉包以外,這多是最接近真正私有屬性的方法,由於它們沒法經過枚舉,克隆,訪問或修改來訪問。

clipboard.png

可是,它們在開發控制檯中可見。 只有閉包才能免於這種命運。

緩存

在客戶端和服務器之間同步狀態時遇到困難並不罕見。數據可能會隨着時間的推移而發生變化,很難確切地知道什麼時候從新同步的邏輯。

Proxy啓用了一種新方法:根據須要將對象包裝爲無效(和從新同步)屬性。 全部訪問屬性的嘗試都首先檢查緩存策略,該策略決定返回當前在內存中的內容仍是採起其餘一些操做。

const ephemeral = (target, ttl = 60) => {
    const CREATED_AT = Date.now();
    const isExpired = () => Date.now() - CREATED_AT > ttl * 1000;

    return new Proxy(target, {
        get: (obj, prop) => (isExpired() ? undefined : Reflect.get(obj, prop))
    });
};

這個函數過於簡化了:它使對象上的全部屬性在一段時間後都沒法訪問。然而,將此方法擴展爲根據每一個屬性設置生存時間(TTL),並在必定的持續時間或訪問次數以後更新它並不困難。

let bankAccount = ephemeral(
    {
        balance: 14.93
    },
    10
);

console.log(bankAccount.balance); // 14.93

setTimeout(() => {
    console.log(bankAccount.balance); // undefined
}, 10 * 1000);

這個示例簡單地使銀行賬戶餘額在10秒後沒法訪問。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

枚舉和只讀視圖

這些例子來自Csaba Hellinge 關於[代理用例][23]和[Mozilla黑客][24]的文章。方法是包裝一個對象以防止擴展或修改。雖然object.freeze`如今提供了將對象渲染爲只讀的功能,可是能夠對這種方法進行擴展,以便訪問不存在屬性的枚舉對象能更好地處理拋出錯誤。

只讀視圖

const NOPE = () => {
    throw new Error("Can't modify read-only view");
};

const NOPE_HANDLER = {
    set: NOPE,
    defineProperty: NOPE,
    deleteProperty: NOPE,
    preventExtensions: NOPE,
    setPrototypeOf: NOPE
};

const readOnlyView = target => new Proxy(target, NOPE_HANDLER);

枚舉視圖

const createEnum = target =>
    readOnlyView(
        new Proxy(target, {
            get: (obj, prop) => {
                if (prop in obj) {
                    return Reflect.get(obj, prop);
                }
                throw new ReferenceError(`Unknown prop "${prop}"`);
            }
        })
    );

如今咱們能夠建立一個Object,若是嘗試訪問不存在的屬性如今不是返回undefined,而是會拋出異常。 這使得在早期捕獲和解決問題變得更加容易。

咱們的enum示例也是代理上的代理的第一個示例,它確認代理是另外一個代理的有效目標對象。這經過組合代理功能促進了代碼重用。

let SHIRT_SIZES = createEnum({
    S: 10,
    M: 15,
    L: 20
});

SHIRT_SIZES.S; // 10
SHIRT_SIZES.S = 15;

// Uncaught Error: Can't modify read-only view

SHIRT_SIZES.XL;

// Uncaught ReferenceError: Unknown prop "XL"

這種方法能夠進一步擴展,包括模擬方法nameOf,它返回給定enum值的屬性名,模仿Javascript等語言中的行爲。

雖然其餘框架和語言超集(好比TypeScript)提供enum類型,可是這個解決方案的獨特之處在於,它使用普通Javascript,而不使用特殊的構建工具或轉置器。

運算符重載

也許從語法上講,最吸引人的 Proxy 用例是重載操做符的能力,好比使用handler.hasin操做符。

in操做符用於檢查指定的屬性是否位於指定的對象或其原型鏈中。但它也是語法上最優雅的重載操做符。這個例子定義了一個連續range函數來比較數字。

const range = (min, max) =>
    new Proxy(Object.create(null), {
        has: (_, prop) => +prop >= min && +prop <= max
    });

與Python不一樣,Python使用生成器與有限的整數序列進行比較,這種方法支持十進制比較,能夠擴展爲支持其餘數值範圍。

const X = 10.5;
const nums = [1, 5, X, 50, 100];

if (X in range(1, 100)) {
    // true
    // ...
}

nums.filter(n => n in range(1, 10)); // [1, 5]

儘管這個用例不能解決複雜的問題,但它確實提供了乾淨、可讀和可重用的代碼。

除了in運算符,咱們還能夠重載deletenew

cookie對象

若是你曾經與cookie進行交互,那麼必須處理document.cookie。 這是一個不尋常的API,由於API是一個String,它讀出全部cookie,以分號分隔

document.cookie是一個看起來像這樣的字符串:

_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1

簡而言之,處理document.cookie比較麻煩且容易出錯。 一種方法是使用簡單的cookie框架,能夠適用於使用 Proxy。

const getCookieObject = () => {
    const cookies = document.cookie
        .split(";")
        .reduce(
            (cks, ck) => ({
                [ck.substr(0, ck.indexOf("=")).trim()]: ck.substr(
                    ck.indexOf("=") + 1
                ),
                ...cks
            }),
            {}
        );
    const setCookie = (name, val) => (document.cookie = `${name}=${val}`);
    const deleteCookie = name =>
        (document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GMT;`);

    return new Proxy(cookies, {
        set: (obj, prop, val) => (
            setCookie(prop, val), Reflect.set(obj, prop, val)
        ),
        deleteProperty: (obj, prop) => (
            deleteCookie(prop), Reflect.deleteProperty(obj, prop)
        )
    });
};

此函數返回一個鍵值對對象,但代理對document.cookie進行持久性的全部更改。

let docCookies = getCookieObject();

docCookies.has_recent_activity; // "1"
docCookies.has_recent_activity = "2"; // "2"
delete docCookies2["has_recent_activity"]; // true

在11行代碼中,修改cookie提供了更好的交互,儘管在生產環境中還須要諸如字符串規範化之類的附加功能。

細節決定成敗,Proxy 也不例外。

Polyfill

在撰寫本文時(2019年5月),Proxy 沒有完整的 polyfill。然而,有一個由谷歌編寫的 partial polyfill for Proxy ,它支持getsetapplyconstruct trap,並適用於IE9+。

它是 Proxy 嗎?

肯定一個對象是不是代理是不可能的

根據Javascript語言規範,沒法肯定對象是不是代理。 可是,在 Node 10+上,可使用util.types.isProxy方法。

目標是什麼?

給定一個代理對象,就不可能得到或更改目標對象。也不可能獲取或修改處理程序對象。

最近似的是Ben Nadel的文章Using Proxy to Dynamically Change THIS Binding,它使用一個空對象做爲Proxy目標和閉包來巧妙地從新分配對象的Proxy操做。

Proxy 原語

new Proxy("To be, or not to be...", {});

// TypeError: Cannot create proxy with a non-object as target or handler

不幸的是,Proxy的一個限制是目標必須是Object。 這意味着咱們不能直接使用像String這樣的原語。 😞

性能

Proxy的一個主要缺點是性能。 因瀏覽器和使用而異,可是對於性能有要求的代碼來講,代理不是最好的方法。 固然,能夠衡量影響並肯定代理的優點是否超過對性能的影響。

爲何要使用 Proxy?

Proxy 提供虛擬化接口來控制任何目標 Object的行爲。 這樣作能夠在簡單性和實用性之間取得平衡,而不會犧牲兼容性。

也許使用Proxy的最使人信服的理由是,上面的許多示例只有幾行,而且能夠輕鬆組合以建立複雜的功能。 最後一個例子,咱們能夠從幾個用例中組合函數來建立一個只讀cookie對象,該對象返回不存在或「私有」隱藏cookie的默認值。

// document.cookie = "_octo=GH1.2.2591.47507; _ga=GA1.1.62208.4087; has_recent_activity=1"

let docCookies = withZeroValue(
    hide(readOnlyView(getCookieObject())),
    "Cookie not found"
);

docCookies.has_recent_activity; // "1"
docCookies.nonExistentCookie; // "Cookie not found"
docCookies._ga; // "Cookie not found"
docCookies.newCookie = "1"; // Uncaught Error: Can't modify read-only view

我但願這些例子已經代表,對於Javascript中的小衆元編程來講,代理不只僅是一個深奧的特性。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對一、微脈、青團社等衆多品牌企業。歡迎你們免費試用!

相關文章
相關標籤/搜索