[译] 实例解析 ES6 Proxy 使用场景

文章永久连接地址:http://pinggod.com/2016/%E5%AE%9E%E4%BE%8B%E8%A7%A3%E6%9E%90-ES6-Proxy-%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF/设计模式

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

Proxy,见名知意,其功能很是相似于设计模式中的代理模式,该模式经常使用于三个方面:数组

  • 拦截和监视外部对对象的访问浏览器

  • 下降函数或类的复杂度app

  • 在复杂操做前对操做进行校验或对所需资源进行管理async

在支持 Proxy 的浏览器环境中,Proxy 是一个全局对象,能够直接使用。Proxy(target, handler) 是一个构造函数,target 是被代理的对象,handlder 是声明了各种代理操做的对象,最终返回一个代理对象。外界每次经过代理对象访问 target 对象的属性时,就会通过 handler 对象,从这个流程来看,代理对象很相似 middleware(中间件)。那么 Proxy 能够拦截什么操做呢?最多见的就是 get(读取)、set(修改)对象属性等操做,完整的可拦截操做列表请点击这里。此外,Proxy 对象还提供了一个 revoke 方法,能够随时注销全部的代理操做。在咱们正式介绍 Proxy 以前,建议你对 Reflect 有必定的了解,它也是一个 ES6 新增的全局对象,详细信息请参考 MDN Reflect函数

Basic

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,此后全部使用 proxytarget 属性的访问都会通过 handler 的处理。性能

1. 抽离校验模块

让咱们从一个简单的类型校验开始作起,这个示例演示了如何使用 Proxy 保障数据类型的准确性:this

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 则能够将校验器从核心逻辑分离出来自成一体:设计

function createValidator(target, validator) {  
    return new Proxy(target, {
        _validator: validator,
        set(target, key, value, proxy) {
            if (target.hasOwnProperty(key)) {
                let validator = this._validator[key];
                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(name, age) {
        this.name = name;
        this.age = age;
        return createValidator(this, personValidators);
    }
}

const bill = new Person('Bill', 25);

// 如下操做都会报错
bill.name = 0;  
bill.age = 'Bill';  
bill.age = 15;

经过校验器和主逻辑的分离,你能够无限扩展 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 = target[key];
        return function(...args) {
            var checkArgs = argChecker(key, args, argTypes[key]);
            return Reflect.apply(value, target, args);
        };
    }
});

function argChecker(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();  
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3

obj.pickyMethodTwo("wopdopadoo", {});  
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1

// No warnings logged
obj.pickyMethodOne({}, "a little string", 123);  
obj.pickyMethodOne(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);
    }
});

// 如下操做都会抛出错误
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 的使用场景,能够归纳为:Proxy 的核心做用是控制外界对被代理者内部的访问,Decorator 的核心做用是加强被装饰者的功能。只要在它们核心的使用场景上作好区别,那么像是访问日志这样的功能,虽然本文使用了 Proxy 实现,但也可使用 Decorator 实现,开发者能够根据项目的需求、团队的规范、本身的偏好自由选择。