ES6 Proxy 代理详解:从基础用法到高级场景实战

作者:拾光时间:2026-10-30 11:04:01浏览:489

本文深入解析ES6中的Proxy全局对象,阐述其作为中间件拦截对象访问的核心机制。通过类型校验、私有属性模拟、访问日志记录及操作拦截等具体案例,展示Proxy在控制外部访问、增强功能及安全管理方面的实际应用,帮助开发者掌握这一强大特性。

前言

Proxy基础机制与核心概念

在ES6规范中,Proxy是一个全局可用的构造函数,它允许开发者在目标对象与操作行为之间建立一层拦截机制,从而收集并处理各种请求操作,最终返回一个经过处理的对象。尽管ES6中的箭头函数、数组解构和rest参数等特性已广泛普及,但Proxy这类高级特性却较少被日常使用。这主要受限于浏览器兼容性问题,以及开发者对其适用场景理解的深度不足。就个人而言,我非常推崇ES6的Proxy功能,因为它提供了一种简洁且直观的方式,让开发者能够精确控制外部对对象属性的访问行为。下文将首先介绍Proxy的基本用法,随后通过具体实例深入解析其应用场景。

Proxy,顾名思义,其设计理念与软件设计模式中的“代理模式”高度一致。在软件开发中,代理模式通常应用于以下三个主要方面:

1. 监控并记录外部对目标对象的访问行为。

2. 降低函数或类的直接复杂度,通过代理层进行逻辑分发。

3. 在执行具体操作前进行校验,或对所需资源进行统一管理与分配。

在支持Proxy的现代浏览器环境中,它是一个全局对象,可直接调用。Proxy(target, handler) 是一个构造函数,其中 target 参数代表被代理的目标对象,handlder 参数是一个声明了各类拦截操作(trap)的对象。Proxy最终返回一个代理对象。每当外界通过代理对象访问 target 对象的属性时,请求都会先经过 handler 对象的处理。从流程上看,代理对象非常类似于中间件(middleware)。那么Proxy具体可以拦截哪些操作呢?最常见的包括 get(读取属性)和 set(修改属性)等,完整的可拦截操作列表可参考官方文档。此外,Proxy对象还提供了一个 revoke 方法,用于随时注销所有已注册的代理操作。在正式深入介绍Proxy之前,建议读者先了解另一个ES6新增的全局对象——Reflect,它与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 处理器中定义逻辑的处理。

Proxy在数据校验中的应用

1. 抽离数据校验模块

让我们从简单的数据类型校验入手,这个示例演示了如何利用Proxy确保数据类型的准确性:


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

利用Proxy实现私有属性

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...")
  }
}

Proxy在日志记录与操作拦截中的实战

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

装饰器模式对比

ES7中引入的Decorator(装饰器)概念,对应于设计模式中的装饰器模式。如果简单区分Proxy和Decorator的使用场景,可以概括为:Proxy的核心作用是控制外界对被代理者内部状态的访问权限,而Decorator的核心作用是动态增强被装饰者的功能。只要在核心使用场景上做好区分,像本文使用的访问日志功能,虽然可以用Proxy实现,同样也可以使用Decorator实现。开发者可根据项目需求、团队规范或个人偏好自由选择。

总结

ES6的Proxy是一个非常实用的特性,看似简单的接口背后蕴含着巨大的潜力。希望本文能为大家学习ES6提供有益的参考和帮助。

作者最新文章