温故知新之ES6(四)

2018-09-09  本文已影响22人  simuty

希望大家看到这个字数,不要被吓到,
Proxy / Reflect 真的很值得学习的

1.Proxy概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

15364770398040.jpg

ES6 中的箭头函数、数组解构、rest 参数等特性一经实现就广为流传,但类似 Proxy 这样的特性却很少见到有开发者在使用,一方面在于浏览器的兼容性,另一方面也在于要想发挥这些特性的优势需要开发者深入地理解其使用场景。就我个人而言是非常喜欢 ES6 的 Proxy,因为它让我们以简洁易懂的方式控制了外部对对象的访问。在下文中,首先我会介绍 Proxy 的使用方式,然后列举具体实例解释 Proxy 的使用场景。

Proxy,见名知意,其功能非常类似于设计模式中的代理模式,该模式常用于三个方面:

1. 拦截和监视外部对对象的访问
2. 降低函数或类的复杂度
3. 在复杂操作前对操作进行校验或对所需资源进行管理

在支持 Proxy 的浏览器环境中,Proxy 是一个全局对象,可以直接使用。Proxy(target, handler) 是一个构造函数,target 是被代理的对象,handlder 是声明了各类代理操作的对象,最终返回一个代理对象。外界每次通过代理对象访问 target 对象的属性时,就会经过 handler 对象,从这个流程来看,代理对象很类似 middleware(中间件)。

那么 Proxy 可以拦截什么操作呢?
最常见的就是 get(读取)、set(修改)对象属性等操作,完整的可拦截操作列表请点击这里。此外,Proxy 对象还提供了一个 【revoke 方法,可以随时注销所有的代理操作】。在我们正式介绍 Proxy 之前,建议你对 Reflect 有一定的了解,它也是一个 ES6 新增的全局对象,详细信息请参考 MDN Reflect。

为了帮助理解,在正式介绍使用场景前,先来两个简单的代码的例子。

1. Proxy第一个例子

const target = {
    name: 'Billy Bob',
    age: 15
};
const handler = {
    get(target, key, proxy) {
        const today = new Date();
        console.log(`GET request made for ${key} at ${today}`);
        return Reflect.get(target, key, proxy);
    }
};
const proxy = new Proxy(target, handler);
proxy.name; 
// => "GET request made for name at Thu Jul 21 2016 15:26:20 GMT+0800 (CST)" 
// => "Billy Bob"著作权归作者所有。

在上面的代码中,我们首先定义了一个被代理的目标对象 target,然后声明了包含所有代理操作的 handler 对象,接下来使用 Proxy(target, handler) 创建代理对象 proxy,此后所有使用 proxy 对 target 属性的访问都会经过 handler 的处理。

2. Proxy第二个例子

属性 说明
get(被代理对象,key) 代理对象属性读取
set(被代理对象,key,value) 代理对象属性设置
has(被代理对象,key) 拦截key in object操作
deleteProperty(被代理对象,key) 拦截删除操作
ownKeys(被代理对象) 拦截Object.keys、Object.getOwnPropertySymbols、Object.getOwnPropertyNames
// 元数据
let obj = {
    time: '2017-08-31',
    name: 'Tim',
    _r: 123
};

//代理商
let monitor = new Proxy(obj, {
    //代理读取对象属性
    get(target, key) {
        //当读取对象属性时,将所有2017替换成2018
        return target[key].replace('2017', '2018')
    },
    //代理设置对象属性
    set(target, key, value) {
        //只有key值是name的时候,才允许修改;否则返回原来的值。
        if (key === 'name') {
            return target[key] = value;
        } else {
            return target[key];
        }
    },

    //拦截key in object操作
    has(target, key) {
        //只有key值是name的时候,才返回true;否则返回false。
        if (key === 'name') {
            return target[key];
        } else {
            return false;
        }
    },

    //拦截删除操作
    deleteProperty(target, key) {
        //只有key值以“_”开头,才允许删除。
        if (key.indexOf('_') === 0) {
            delete target[key];
            return true;
        } else {
            return target[key]
        }
    },

    //拦截Object.keys, Object.getOwnPropertySymbols, Object.getOwnPropertyNames
    ownKeys(target) {
        //使用数组的filter方法过滤掉time属性,不暴露出来
        return Object.keys(target).filter(item => item != 'time')
    }
})

//读取
console.log('读取操作', monitor.time); //2018-08-31

//设置
monitor.time = '2019'; //设置失败
monitor.name = 'Chen'; //设置成功
console.log('设置操作', monitor.time, monitor.name); //2018-08-31  Tim

//has in object操作
console.log('has', 'name' in monitor); // true
console.log('has', 'time' in monitor); // false

//删除操作
delete monitor.time; //并没有成功
delete monitor._r; //并没有成功

console.log('删除操作', monitor.time); //time: '2017-08-31', name: 'Tim', _r: 123

//遍历属性操作
console.log(Object.keys(monitor)); // name, _r

3. Reflect【反射】基本认识

看完以上两段代码,对于proxy应该有了基本的认识,为了接下来的多个使用场景,先把Reflect给大家做个简单的介绍,不至于看到不认识的代码苦恼。

属性 说明
get(被代理对象,key) 读取对象属性
set(被代理对象,key,value) 设置对象属性
has(被代理对象,key) 相当于key in object
deleteProperty(被代理对象,key) 删除

汉语、英语、都不如代码来的实在
如果不行🚫,就在看一遍


let obj_1 = {
    time: '2017-08-31',
    name: 'Tim',
    _r: 123
};

//get读取操作
console.log('Reflect get读取',Reflect.get(obj_1,'time'));

//set设置操作
Reflect.set(obj_1,'name','CCC');
console.log(obj_1);  // time: '2017-08-31', name: 'CCC', _r: 123

//has操作,相当于以前的key in obj 
console.log('Reflect has',Reflect.has(obj_1,'name'));  // true

//删除
const myObj = { foo: 'bar' };
Reflect.deleteProperty(myObj, 'foo');

2. ES6 Proxy使用场景

1. 抽离校验模块;

有了上边三个例子想必下边这个代码很容易理解,就是 Proxy 与 Reflect 的结合使用,一个拦截一个设置值。

let numericDataStore = {
    count: 0,
    amount: 1234,
    total: 14
};
numericDataStore = new Proxy(numericDataStore, {
// 拦截
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("Properties in numericDataStore can only be numbers");
        }
// 设置值
        return Reflect.set(target, key, value, proxy);
    }
}); 
// 拋出錯誤,因為 "foo" 不是數值 
numericDataStore.count = "foo";
// 賦值成功 
numericDataStore.count = 333;

如果要直接为对象的所有属性开发一个校验器可能很快就会让代码结构变得臃肿,使用 Proxy 则可以将校验器从核心逻辑分离出来自成一体:

如下代码,将核心区、规则、用法,三者已初步分离,还记得【Nodejs技巧之----Joi数据校验】吗,你也可以轻易实现一个类似功能的中间件。-。—


function createValidator(target, validator) {
    return new Proxy(target, {
        _validator: validator,
        // 拦截 代理设置对象属性
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                /**
                 * validator 在此等价于: personValidators[key] 
                     name(val) {
                        return typeof val === 'string';
                    }
                */
               // 不知道 !! 是啥意思, 如有知悉,请告知,谢啦
               // 断点 、更改为 !都没问题,只是一个 判断
                if (!!validator(value)) {
                    return Reflect.set(target, key, value, proxy);
                } else {
                    throw Error(`Cannot set ${key} to ${value}. Invalid.`);
                }
            } else {
                throw Error(`${key} is not a valid property`)
            }
        }
    });
}

// 规则
const personValidators = {
    name(val) {
        return typeof val === 'string';
    },
    age(val) {
        return typeof age === 'number' && age > 18;
    }
}
class Person {
    /**
     * constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。
     * 一个类必须有constructor方法,如果没有显式定义,
     * 一个空的constructor方法会被默认添加。
     * 
        class Point {
        }

        // 等同于
        class Point {
            constructor() {}
        }
     *   
     */
    constructor(name, age) {
        // 在createValidator 中没有对 get 拦截
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}
const bill = new Person('Bill', 10); 
// 以下操作都會報錯 
// bill.name = 'Bill';
// bill.age = 15;
// bill.age = 20;

通过校验器和主逻辑的分离,你可以无限扩展 personValidators 校验器的内容,而不会对相关的类或函数造成直接破坏。更复杂一点,我们还可以使用 Proxy 模拟类型检查,检查函数是否接收了类型和数量都正确的参数:

** 注释: 网上代码需要动手写写
本文代码均可以执行,如需真正判断,还需添加代码

let obj = {
    pickyMethodOne: function (obj, str, num) { /* ... */ },
    pickyMethodTwo: function (num, obj) { /*... */ }
};
const argTypes = {
    pickyMethodOne: ["object", "string", "number"],
    pickyMethodTwo: ["number", "object"]
};
obj = new Proxy(obj, {
    get: function (target, key, proxy) {
        // var value = `http://ju.outofmemory.cn/entry/target[key]`;
        // 获取判断规则
        var value = target[key]
        // ...args 获取传入的值,obj.pickyMethodOne(); args为空数组
        return function (...args) {
            // console.log(args, target, key)
            var checkArgs = argChecker(key, args, argTypes[key]);
          // Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。
            return Reflect.apply(value, target, args);
        };
    }
});

function argChecker(name, args, checkers) {
    // console.log(name, args, checkers);
    for (var idx = 0; idx < args.length; idx++) {
        var arg = args[idx];
        var type = checkers[idx];
        if (!arg || typeof arg !== type) {
            console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
        }
    }
}

// 参数为空,故没有判断
obj.pickyMethodOne(); 

obj.pickyMethodTwo("wopdopadoo", {});
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1 
// 以下 没有错误
obj.pickyMethodOne({}, "a little string", 123);
obj.pickyMethodTwo(123, {});

2. 实现私有属性

在 JavaScript 或其他语言中,大家会约定俗成地在变量名之前添加下划线** _ 来表明这是一个私有属性(并不是真正的私有),但我们无法保证真的没人会去访问或修改它。在下面的代码中,我们声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey:**

var api = {  
    _apiKey: '123abc456def',
    /* mock methods that use this._apiKey */
    getUsers: function(){}, 
    getUser: function(userId){}, 
    setUser: function(userId, config){}
};

// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);

// get and mutate _apiKeys as desired
var apiKey = api._apiKey;  
api._apiKey = '987654321';

很显然,【约定俗成是没有束缚力的】。使用 *ES6 Proxy 我们就可以实现真实的私有变量了,下面针对不同的读取方式演示两个不同的私有化方法。第一种方法是使用 set / get 拦截读写请求并返回 undefined:

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} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, proxy);
    },
    set(target, key, value, proxy) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} is restricted. Please see api documentation for further info.`);
        }
        return Reflect.get(target, key, value, proxy);
    }
});

// 以下操作都会抛出错误 【以下是Get Set 两种情况】
console.log(api._apiKey);
api._apiKey = '987654321';  

第二种方法是使用 has 拦截 in 操作:

var api = {  
    _apiKey: '123abc456def',
    getUsers: function(){ }, 
    getUser: function(userId){ }, 
    setUser: function(userId, config){ }
};

const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {  
    has(target, key) {
        return (RESTRICTED.indexOf(key) > -1) ?
            false :
            Reflect.has(target, key);
    }
});

// these log false, and `for in` iterators will ignore _apiKey
console.log("_apiKey" in api);

for (var key in api) {  
    if (api.hasOwnProperty(key) && key === "_apiKey") {
        console.log("This will never be logged because the proxy obscures _apiKey...")
    }
}

3. 访问日志

对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,开发者会希望记录它们的使用情况或性能表现,这个时候就可以使用 Proxy 充当中间件的角色,轻而易举实现日志功能:

let api = {  
    _apiKey: '123abc456def',
    getUsers: function() { /* ... */ },
    getUser: function(userId) { /* ... */ },
    setUser: function(userId, config) { /* ... */ }
};

function logMethodAsync(timestamp, method) {  
    setTimeout(function() {
        console.log(`${timestamp} - Logging ${method} request asynchronously.`);
    }, 0)
}

api = new Proxy(api, {  
    get: function(target, key, proxy) {
        var value = target[key];
        return function(...arguments) {
            logMethodAsync(new Date(), key);
            return Reflect.apply(value, target, arguments);
        };
    }
});

api.getUsers();

4. 预警和拦截

假设你不想让其他开发者删除 noDelete 属性,还想让调用 oldMethod 的开发者了解到这个方法已经被废弃了,或者告诉开发者不要修改 doNotChange 属性,那么就可以使用 Proxy 来实现:

let dataStore = {  
    noDelete: 1235,
    oldMethod: function() {/*...*/ },
    doNotChange: "tried and true"
};

const NODELETE = ['noDelete'];  
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod'];  

dataStore = new Proxy(dataStore, {  
    set(target, key, value, proxy) {
        if (NOCHANGE.includes(key)) {
            throw Error(`Error! ${key} is immutable.`);
        }
        return Reflect.set(target, key, value, proxy);
    },
    deleteProperty(target, key) {
        if (NODELETE.includes(key)) {
            throw Error(`Error! ${key} cannot be deleted.`);
        }
        return Reflect.deleteProperty(target, key);

    },
    get(target, key, proxy) {
        if (DEPRECATED.includes(key)) {
            console.warn(`Warning! ${key} is deprecated.`);
        }
        var val = target[key];

        return typeof val === 'function' ?
            function(...args) {
                Reflect.apply(target[key], target, args);
            } :
            val;
    }
});

// these will throw errors or log warnings, respectively
dataStore.doNotChange = "foo";  
delete dataStore.noDelete;  
dataStore.oldMethod();

5. 过滤操作

某些操作会非常占用资源,比如传输大文件,这个时候如果文件已经在分块发送了,就不需要在对新的请求作出相应(非绝对),这个时候就可以使用 Proxy 对当请求进行特征检测,并根据特征过滤出哪些是不需要响应的,哪些是需要响应的。下面的代码简单演示了过滤特征的方式,并不是完整代码,相信大家会理解其中的妙处:

let obj = {  
    getGiantFile: function(fileId) {/*...*/ }
};

obj = new Proxy(obj, {  
    get(target, key, proxy) {
        return function(...args) {
            const id = args[0];
            // 检测代码,流程明白即可
            let isEnroute = checkEnroute(id);
            let isDownloading = checkStatus(id);      
            let cached = getCached(id);

            if (isEnroute || isDownloading) {
                return false;
            }
            if (cached) {
                return cached;
            }
            return Reflect.apply(target[key], target, args);
        }
    }
});

6. 中断代理

Proxy 支持随时取消对 target 的代理,这一操作常用于完全封闭对数据或接口的访问。在下面的示例中,我们使用了** Proxy.revocable **方法创建了可撤销代理的代理对象:

let sensitiveData = { username: 'devbryce' };
const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);
function handleSuspectedHack(){  
    revokeAccess();
}

// logs 'devbryce'
console.log(sensitiveData.username);
handleSuspectedHack();
// TypeError: Revoked
console.log(sensitiveData.username);
Decorator

ES7 中实现的 Decorator,相当于设计模式中的装饰器模式。
如果*简单地区分 Proxy 和 Decorator 的使用场景
可以概括为:

1. Proxy 的核心作用是控制外界对被代理者内部的访问;
2. Decorator 的核心作用是增强被装饰者的功能。
3. 只要在它们核心的使用场景上做好区别,那么像是访问日志这样的功能,虽然本文使用了 Proxy 实现,但也可以使用 Decorator 实现,
4. 开发者可以根据项目的需求、团队的规范、自己的偏好自由选择。

英文原版无法打开

本文根据@Bryce Johnson的《6 compelling use cases for ES6 proxies》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://devbryce.com/use-cases-for-es6-proxies/

参考

6 compelling use cases for ES6 proxies
实例解析ES6 Proxy使用场景
【ES6系列】Proxy和Reflect

上一篇下一篇

猜你喜欢

热点阅读