6种ES6 proxies的使用案例

240 阅读6分钟

译者:loveky

原文链接

只有我有这种感觉吗?还是在ES6新特性大张旗鼓的宣传中Proxy真的被遗忘了?

这可能是由于Safari(完全不支持),Node(v6是第一个支持它的版本),和转译工具(Babel/TypeScript)对它缓慢且受限的支持造成的。同时,由于Proxy是一种元编程特性,使用它的好处并不如使用新的类语法,箭头函数已经解构和rest参数那么明显。

对于ES6 Proxy这个特性我个人还是很兴奋的。它是JS应用中一种缓和对象访问的简明且有语义的结构。

在本文中,我会尽我所能的解释它是如何工作的并且会通过几个示例来展示一些可能的用法。

什么是Proxy?

在现实生活中,proxy是一个被授权代表其他人的人。比如,许多州允许代理投票,这意味着你可以授权他人在选举中代表你投票。

Proxy也是科技行业中的一种常见模式。你很可能听说过proxy服务器,它会接收来自你这的所有流量,代表你发送给另一端,并把响应返回给你。当你不希望请求的目的地知道你请求的具体来源时,使用proxy服务器就很有用了。所有的目标服务器看到的只是来自proxy服务器的请求。

再接近本文的主题一些,proxy也是程序开发中一种常见的设计模式。这种类型的代理和ES6 proxy要做的就很类似了,涉及到使用类(B)去包装类(A)并拦截/控制对(A)的访问。

当你想进行以下操作时proxy模式通常会很有用:

  • 拦截或控制对某个对象的访问

  • 通过隐藏事务或辅助逻辑来减小方法/类的复杂性

  • 防止在未经验证/准备的情况下执行重度依赖资源的操作

ES6中的Proxy

Proxy构造器可以在全局对象上访问到。通过它,你可以有效的拦截针对对象的各种操作,收集访问的信息,并按你的意愿返回任何值。从这个角度来说,proxy和中间件有很多相似之处。

具体来说,proxy允许你拦截许多对象上常用的方法和属性,最常见的有getsetapply(针对函数)和construct(针对使用new关键字调用的构造函数)。关于使用proxy可以拦截的方法的完整列表,请参考规范。Proxy还可以配置成随时停止接受请求,有效的取消所有针对被代理的目标对象的访问。这可以通过一个revoke方法实现,我会在后文中讨论。

术语

在进一步讨论之前,你需要理解三个概念:targethandlertrap

target代表了被代理的对象。这是你需要控制对其访问的对象。它始终作为Proxy构造器的第一个参数被传入,同时它也会被传入每个trap

handler是一个包含了你想要拦截和处理的操作的对象。它会被作为Proxy构造器的第二个参数传入。它实现了Proxy API(比如:getsetapply等等)。

一个trap代表了handler中一个被处理的函数。因此,如果要拦截get请求你需要创建一个get的trap。以此类推。

最后一点,你也应该了解ReflectAPI,它也可以在全局对象上访问到。关于它的介绍我会交给MDN,因为它很好的介绍了Reflect,同时也由于Reflect是那种你一眼就能看明白的东西。相信我。

基本用法

在介绍我认为有趣的Proxy使用场景之前,先让我们来看一个'hello world'的例子。关于proxy更详细的介绍,参考Nicholas Zakas编写的 Understanding ES6中的章节。这本书很棒而且可以免费在线阅读。

let dataStore = {
  name: 'Billy Bob',
  age: 15
};

let handler = {
  get(target, key, proxy) {
    const today = new Date();
    console.log(`GET request made for ${key} at ${today}`);
    return Reflect.get(target, key, proxy);
  }
}

dataStore = new Proxy(dataStore, handler);

// 这会执行我们的拦截逻辑,记录请求并把值赋给`name`变量
const name = dataStore.name;

ES6 Proxy使用案例

你可能已经对proxy的使用场景有一些了解了。以下是我的一些使用方式。

1. 剥离验证逻辑

一个把Proxy用于验证的例子是Zaka在他的书中提到的确保一个数据源中的所有属性都是同一类型。下面的例子中我们要确保每次给numericDataStore数据源设置一个属性时,它的值必须是数字。

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);
  }
});

// 这会抛出异常
numericDataStore.count = ""foo"";

// 这会设置成功
numericDataStore.count = 333;

这很有意思,但有多大的可能性你会创建一个这样的对象呢?

如果你想为一个对象上的部分或全部属性编写自定义的校验规则,代码可能会更复杂一些,但我非常喜欢Proxy可以帮你把校验代码与核心代码分离开这一点。难道只有我讨厌把校验代码和方法或类混在一起吗?

// 定义一个接收自定义校验规则并返回一个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;

通过这种方式,你就可以无限的扩展校验规则而不用修改类或方法。

再说一个和校验有关的点子。假设你想检查传给一个方法的参数并在传入的参数与函数签名不符时输出一些有用的帮助信息。你可以通过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

// 不会输出警告信息
obj.pickyMethodOne({}, ""a little string"", 123);
obj.pickyMethodOne(123, {});

2. JavaScript中真正的私有属性

我曾经与一位对JavaScript中没有真正的私有属性颇为恼火的程序员一起工作。他之前使用的语言是Java,在Java中你可以明确地设置任何属性是私有的(只能在类中访问)还是公开的(在类内外都可以访问到)。

在JavaScript中常见的做法是在属性名之前或之后放一个下划线来标识该属性仅供内部使用。但这并不能阻止其他人读取或修改它。

在下面的例子中,有一个我们想在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,你可以通过若干方式来实现真实,完全的私有属性。

首先,你可以使用一个proxy来截获针对某个属性的请求并作出限制或是直接返回undefined

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

// Add other restricted properties to this array
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);
    }
});

// throws an error
console.log(api._apiKey);

// throws an error
api._apiKey = '987654321'; 

你还可以使用hastrap来掩盖这个属性的存在。

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

// Add other restricted properties to this array
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可以很容易的悄悄在后台做到这一点。

注意:很不幸,你不能仅仅使用applytrap来拦截方法。Axel Rauschmayer在这里有详细的解释。基本概念是,任何使用当你要执行某个方法时,你首先需要get这个方法。因此,如果你要拦截一个方法调用,你需要先拦截对该方法的get操作,然后拦截apply操作。

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

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);
    };
  }
});

// executes apply trap in the background
api.getUsers();

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

这很酷,因为你可以记录各种各样的信息而不用修改应用程序的代码或是阻塞代码执行。并且只需要在这些代码的基础上稍事修改就可以记录特性函数的执行性能了。

4. 给出提示信息或是阻止特定操作

假设你想阻止其他人删除noDelete属性,想让调用oldMethod方法的人知道该方法已经被废弃,或是想阻止其他人修改doNotChange属性。以下是一种快捷的方法。

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

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

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. 防止不必要的资源消耗操作

假设你有一个服务器接口返回一个巨大的文件。当前一个请求还在处理中,或是文件正在被下载,又或是文件已经被下载之后你不想该接口被再次请求。代理在这种情况下可以很好的缓冲对服务器的访问并在可能的时候读取缓存,而不是按照用户的要求频繁请求服务器。在这里我会跳过大部分代码,但下面的例子还是足够向你展示它的工作方式。

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支持随时撤销对目标对象的访问。当你想彻底封锁对某些数据或API的访问时(比如,出于安全,认证,性能等原因),这可能会很有用。以下是一个使用revocable方法的简单例子。注意当你使用它时,你不需要对Proxy方法使用new关键字。

let sensitiveData = {  
  username: 'devbryce'
};

const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);

function handleSuspectedHack(){  
  // Don't panic
  // Breathe
  revokeAccess();
}

// logs 'devbryce'
console.log(sensitiveData.username);

handleSuspectedHack();

// TypeError: Revoked
console.log(sensitiveData.username); 

好吧,以上就是所有我要讲的内容。我很希望能听到你在工作中是如何使用Proxy的。