简介

ES6中新增了操作对象的新API:Proxy对象。从字面上看可以理解为代理器,主要用于改变对象的默认访问行为,实际表现是在访问对象之前增加一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,可以增加自定义的行为。
基本语法const proxy = new Proxy(target, handler)实际是一个构造函数,接收两个参数,一个是目标对象target;另一个是配置对象handler,用来定义拦截的行为。

  1. // 定义target目标对象
  2. const person = {
  3. name: 'kingx',
  4. age: 23
  5. };
  6. // 定义handle配置对象
  7. let handler = {
  8. get: function (target, prop, receiver) {
  9. console.log("你访问了person的属性");
  10. return target[prop];
  11. }
  12. };
  13. const p = new Proxy(person, handler);
  14. console.log(p.name);
  15. // 你访问了person的属性
  16. // kingx
  17. console.log(person.name) // 直接通过目标对象person访问name属性,则不会触发拦截行为。
  18. // kingx
  19. const p2 = new Proxy(person, {}); // 配置对象为空对象,则没有设置任何拦截,实际是对目标对象的访问
  20. console.log(p2.name); // kingx

注意事项:
(1)必须通过代理实例访问:如果需要配置对象的拦截行为生效,那么必须是对代理实例的属性进行访问,而不是直接对目标对象进行访问。
(2)配置对象不能为空对象:如果需要配置对象的拦截行为生效,那么配置对象不能为空对象。如果为空对象,则代表没有设置任何拦截,实际是对目标对象的访问。另外配置对象不能为null,否则会抛出异常。

函数

Proxy支持总共13种函数:

  • get(target, propKey, receiver)。拦截对象属性的读取操作,例如调用person.name或者person[name],其中target表示的是目标对象,propKey表示的是读取的属性值,receiver表示的是配置对象。
  • set(target, propKey, value, receiver)。拦截对象属性的写操作,即设置属性值,例如person.name='kingx'或者person[name]='kingx',其中target表示目标对象,propKey表示的是将要设置的属性,value表示将要设置的属性的值,receiver表示的是配置对象。
  • has(target, propKey)。拦截hasProperty的操作,返回一个布尔值,最典型的表现形式是执行propKey in target,其中target表示目标对象,propKey表示判断的属性。
  • deleteProperty(target, propKey)。拦截delete person[propKey]的操作,返回一个布尔值,表示是否执行成功,其中target表示目标对象,propKey表示将要删除的属性。
  • ownKeys(target)。拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环等操作,其中target表示的是获取对象自身所有的属性名。
  • getOwnPropertyDescriptor(target, propKey)。拦截Object.getOwnPropertyDescriptor(proxy, propKey)操作,返回属性的属性描述符构成的对象,其中target表示目标对象,propKey表示需要获取属性描述符集合的属性。
  • defineProperty(target, propKey, propDesc)。拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy,propDescs)操作,返回一个布尔值,其中target表示目标对象,propKey表示新增的属性,propDesc表示的是属性描述符对象。
  • preventExtensions(target)。拦截Object.preventExtensions(proxy)操作,返回一个布尔值,表示的是让一个对象变得不可扩展,不能再增加新的属性,其中target表示目标对象。
  • getPrototypeOf(target)。拦截Object.getPrototypeOf(proxy)操作,返回一个对象,表示的是拦截获取对象原型属性,其中target表示目标对象。
  • isExtensible(target)。拦截Object.isExtensible(proxy),返回一个布尔值,表示对象是否是可扩展的,其中target表示目标对象。
  • setPrototypeOf(target, proto)。拦截Object.setPrototypeOf(proxy, proto)操作,返回一个布尔值,表示的是拦截设置对象的原型属性的行为,其中target表示目标对象,proto表示新的原型对象。
  • apply(target, object, args)。拦截Proxy实例作为函数调用的操作,例如proxy(...args)proxy.call(object,...args)proxy.apply(object, [...]),其中target表示目标对象,object表示函数的调用方,args表示函数调用传递的参数。
  • construct(target, args)。拦截Proxy实例作为构造函数调用的操作,例如new proxy(...args),其中target表示目标对象,args表示函数调用传递的参数。

这些函数都有一个通用的特性,即如果在target中使用了this关键字,再通过Proxy处理后,this关键字指向的是Proxy的实例,而不是目标对象target。

  1. const person = {
  2. getName: function () {
  3. console.log(this === proxy);
  4. }
  5. };
  6. const proxy = new Proxy(person, {});
  7. proxy.getName(); // true this指向Proxy的实例proxy
  8. person.getName(); // false this指向person

get()函数

1. 读取不存在属性

在正常情况下,读取一个对象不存在的属性时,会返回“undefined”。通过Proxy的get()函数可以设置读取不存在的属性时抛出异常,从而避免对undefined值的兼容性处理。

  1. let person = {
  2. name: 'kingx'
  3. };
  4. const proxy = new Proxy(person, {
  5. get: function (target, propKey) {
  6. if(propKey in target) {
  7. return target[propKey];
  8. } else {
  9. throw new ReferenceError(`访问的属性${propKey}不存在`);
  10. }
  11. }
  12. });
  13. console.log(proxy.name); // kingx
  14. console.log(proxy.age); // ReferenceError: 访问的属性age不存在

2. 读取负索引的值

负索引实际就是从数组的尾部元素开始,从后往前,寻找元素的位置。
数组的索引值是从0开始依次递增的,正常情况下们无法读取负索引的值,但是通过Proxy的get()函数可以做到这一点。

  1. const arr = [1, 4, 9, 16, 25];
  2. const proxy = new Proxy(arr, {
  3. get: function (target, index) {
  4. index = Number(index);
  5. if (index > 0) {
  6. return target[index];
  7. } else {
  8. // 索引为负值,则从尾部元素开始计算索引
  9. return target[target.length + index];
  10. }
  11. }
  12. });
  13. console.log(proxy[2]); // 9
  14. console.log(proxy[-2]); // 16

3. 禁止访问私有属性

在一些约定俗成的写法中,私有属性都会以下画线(_)开头,当不希望用户能访问到私有属性,这可以通过设置Proxy的get()函数来实现。

  1. const person = {
  2. name: 'kingx',
  3. _pwd: '123456'
  4. };
  5. const proxy = new Proxy(person, {
  6. get: function (target, prop) {
  7. if (prop.indexOf('_') === 0) { // 如果访问的某个属性是以下画线(_)开头的,则直接抛出异常
  8. throw new ReferenceError('不可直接访问私有属性');
  9. } else {
  10. return target[prop];
  11. }
  12. }
  13. });
  14. console.log(proxy.name); // kingx
  15. console.log(proxy._pwd); // ReferenceError: 不可直接访问私有属性

4. Proxy访问属性的限制

  1. const target = Object.defineProperties({}, {
  2. // 可写的name属性
  3. name: {
  4. value: 'kingx',
  5. congurable: true, // 可配置即可以delete该属性
  6. writable: true // 可以修改该属性值
  7. },
  8. // 不可写的age属性
  9. age: {
  10. value: 12,
  11. congurable: false, // 不可配置,即这个属性不可被删除
  12. writable: false // 不可写即不可以被修改,可以理解为常量
  13. }
  14. });
  15. const proxy = new Proxy(target, {
  16. get: function (targetObj, prop) {
  17. return 'abc';
  18. }
  19. });
  20. console.log(proxy.name); // abc
  21. console.log(proxy.age); // Error: (expected '12' but got 'abc')

set()函数

1. 拦截属性赋值操作

  1. const proxy = new Proxy({}, {
  2. set: function (target, prop, value) {
  3. if (prop === 'age') { // 只拦截age属性
  4. if (!Number.isInteger(value)) { // 如果赋值的不是整数值,就抛出异常
  5. throw new TypeError('The age is not an integer');
  6. }
  7. if (value > 200 || value < 0) { // 如果设置的值不在0~200以内,抛出RangeError异常
  8. throw new RangeError('The age is invalid');
  9. }
  10. } else {
  11. target[prop] = value;
  12. }
  13. }
  14. });
  15. proxy.name = 'kingx'; // 正常
  16. proxy.age = 10; // 正常
  17. proxy.age = 201; // RangeError: The age is invalid

2. 私有属性不应该被修改

可以通过get()函数实现私有属性不可以被访问,也可以通过set()函数实现私有属性不可以被修改

has()函数

1. 隐藏内部私有属性

has()函数用于拦截hasProperty()函数,即判断对象是否具有某个属性,而不是hasOwnProperty()函数,即has()函数不判断一个属性是对象自身的属性,还是对象继承的属性。如果具有则返回“true”,如果不具有则返回“false”,典型的就是in操作符。has()函数只会对in操作符生效,而不会对for…in循环操作符生效。

  1. const obj = {
  2. _name: 'kingx',
  3. age: 13
  4. };
  5. const proxy = new Proxy(obj, {
  6. has: function (target, prop) {
  7. if(prop[0] === '_') { // 如果属性名第一个字符是下画线,则直接返回“false”,表示的是属性不存在对象中
  8. return false;
  9. }
  10. return prop in target;
  11. }
  12. });
  13. console.log('age' in proxy); // true
  14. console.log('_name' in proxy); // false
  15. for (let key in proxy) { // has()函数并没有生效。
  16. console.log(proxy[key]);
  17. }
  18. // kingx
  19. // 13

deleteProperty()函数

1. 禁止删除某些属性

deleteProperty()函数,用于拦截delete操作,返回“true”时表示属性删除成功,返回“false”时表示属性删除失败。

  1. let obj = {
  2. _name: 'kingx',
  3. age: 12
  4. };
  5. const proxy = new Proxy(obj, {
  6. deleteProperty: function (target, prop) {
  7. if (prop[0] === '_') { // 禁止删除私有属性
  8. throw new Error(`Invalid attempt to delete private "${prop}" property`);
  9. }
  10. return true;
  11. }
  12. });
  13. delete proxy.age; // 删除成功
  14. delete proxy._name; // Error: Invalid attempt to delete private "_name" property

apply()函数

1. 函数的拦截

apply()函数,用于拦截函数调用,可以加入自定义操作,从而得到新的函数处理结果。
函数调用包括直接调用、call()函数调用、apply()函数调用3种方式。

  1. function sum(num1, num2) {
  2. return num1 + num2;
  3. }
  4. const proxy = new Proxy(sum, {
  5. apply: function (target, obj, args) {
  6. console.log(target); // sum
  7. return target.apply(obj, args) * 2;
  8. }
  9. });
  10. console.log(proxy(1, 3)); // (1 + 3)×2 = 8
  11. console.log(proxy.call(null, 3, 4)); // (3 + 4)×2 = 14
  12. console.log(proxy.apply(null, [5, 6])); // (5 + 6)×2 = 22

使用场景

1. 实现真正的私有

  • 不能访问到私有属性,如果访问到私有属性则返回“undefined”。
  • 不能直接修改私有属性的值,即使设置了也无效。
  • 不能遍历出私有属性,遍历出来的属性中不会包含私有属性。

    1. const apis = {
    2. _apiKey: '12ab34cd56ef',
    3. getAllUsers: function () {
    4. console.log('这是查询全部用户的函数');
    5. },
    6. getUserById: function (userId) {
    7. console.log('这是根据用户id查询用户的函数');
    8. },
    9. saveUser: function (user) {
    10. console.log('这是保存用户的函数');
    11. }
    12. };
    13. const proxy = new Proxy(apis, {
    14. get: function (target, prop) {
    15. if (prop[0] === '_') {
    16. return undened;
    17. }
    18. return target[prop];
    19. },
    20. set: function (target, prop, value) {
    21. if (prop[0] !== '_') {
    22. target[prop] = value;
    23. }
    24. },
    25. has: function (target, prop) {
    26. if (prop[0] === '_') {
    27. return false;
    28. }
    29. return prop in target;
    30. }
    31. });
    32. console.log(proxy._apiKey); // undefined
    33. console.log(proxy.getAllUsers()); // 这是查询全部用户的函数
    34. proxy._apiKey = '123456789'; // 设置无效
    35. console.log('getUserById' in proxy); // true
    36. console.log('_apiKey' in proxy); // false

    2. 增加日志记录

    通过get()函数拦截到调用的函数名,然后会返回一个函数,在这个函数内通过apply()调用原始函数,然后调用记录操作日志的函数。

    1. const apis = {
    2. _apiKey: '12ab34cd56ef',
    3. getAllUsers: function () {
    4. console.log('这是查询全部用户的函数');
    5. },
    6. getUserById: function (userId) {
    7. console.log('这是根据用户id查询用户的函数');
    8. },
    9. saveUser: function (user) {
    10. console.log('这是保存用户的函数');
    11. }
    12. };
    13. // 记录日志的方法
    14. function recordLog() {
    15. console.log('这是记录日志的函数');
    16. }
    17. const proxy = new Proxy(apis, {
    18. get: function (target, prop) {
    19. const value = target[prop];
    20. return typeof val === 'function' ?
    21. function (...args) {
    22. // 此处增加一个调用记录日志的函数
    23. recordLog();
    24. val.apply(null, args);
    25. } : val;
    26. }
    27. });
    28. proxy.getAllUsers();

    3. 提供友好提示或者阻止特定操作

  • 某些被弃用的函数被调用时,给用户提供友好提示。get()函数

  • 阻止删除属性的操作。deleteProperty()函数
  • 阻止修改某些特定的属性的操作。set()函数
    1. let dataStore = {
    2. noDelete: 1234, // 不能删除的属性
    3. oldMethod: function () {/*...*/}, // 已废弃的函数
    4. doNotChange: tried and true // 不能改变的属性
    5. };
    6. let NO_DELETE = ['noDelete'];
    7. let DEPRECATED = ['oldMethod'];
    8. let NO_CHANGE = ['doNotChange'];
    9. const proxy = new Proxy(dataStore, {
    10. set(target, key, value, proxy) {
    11. if (NO_CHANGE.includes(key)) {
    12. throw Error(`Error! ${key} is immutable.`);
    13. }
    14. return true;
    15. },
    16. deleteProperty(target, key) {
    17. if (NO_DELETE.includes(key)) {
    18. throw Error(`Error! ${key} cannot be deleted.`);
    19. }
    20. return true;
    21. },
    22. get(target, key, proxy) {
    23. if (DEPRECATED.includes(key)) {
    24. console.warn(`Warning! ${key} is deprecated.`);
    25. }
    26. const val = target[key];
    27. return typeof val === 'function' ?
    28. function (...args) {
    29. val.apply(null, args);
    30. } : val;
    31. }
    32. });
    33. proxy.doNotChange = "foo"; // Error! doNotChange is immutable.
    34. delete proxy.noDelete; // Error! noDelete cannot be deleted.
    35. proxy.oldMethod(); // Warning! oldMethod is deprecated.