ES6之Proxy 的巧用

作者: Fundebug | 来源:发表于2019-07-27 10:21 被阅读914次

    摘要: Proxy的骚操作。

    Fundebug经授权转载,版权归原作者所有。

    Proxy 介绍

    使用Proxy,你可以将一只猫伪装成一只老虎。下面大约有6个例子,我希望它们能让你相信,Proxy 提供了强大的 Javascript 元编程。

    尽管它不像其他ES6功能用的普遍,但Proxy有许多用途,包括运算符重载对象模拟简洁而灵活的API创建对象变化事件,甚至Vue 3背后的内部响应系统提供动力

    Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。

    ES6原生提供了Proxy构造函数,用来生成Proxy实例。

    var proxy = new Proxy(target, handler);
    

    Proxy对象的所有用法,都是上面的这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。

    下面是 Proxy 最简单的例子是,这是一个有陷阱的代理,一个get陷阱,总是返回42

    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')等。

    但是,Proxy 陷阱当然不限于属性的读取。 它只是十几个不同陷阱中的一个:

    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之类的陷阱。 除了闭包之外,这可能是最接近真正私有属性的方法,因为它们无法通过枚举,克隆,访问或修改来访问。

    [图片上传失败...(image-ea9ab5-1564194070930)]

    但是,它们在开发控制台中可见。 只有闭包才能免于这种命运。

    缓存

    在客户端和服务器之间同步状态时遇到困难并不罕见。数据可能会随着时间的推移而发生变化,很难确切地知道何时重新同步的逻辑。

    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对1、微脉、青团社等众多品牌企业。欢迎大家免费试用!

    相关文章

      网友评论

        本文标题:ES6之Proxy 的巧用

        本文链接:https://www.haomeiwen.com/subject/ouusrctx.html