JavaScript 中 Proxy 的理解

介绍

Proxy 是由 ES6 提供的一种机制,可以对外界的访问进行过滤和改写;可以理解成在目标对象之前架设一层 "拦截", 外界对这个对象的访问,都必须先通过这层拦截;
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”;

用法

Proxy 为构造函数,用来生成 Proxy 实例

var proxy = new Proxy(target, handler);

上面的代码,new Proxy() 生成一个 Proxy 实例,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来指定拦截行为;
一个简单的示例:

const person = {
	name: "赵云",
	age: 21
}

const proxy = new Proxy(person, {
	get: function(target, propKey) {
		console.log(target);
		console.log(propKey);
		return target[propKey];
	}
});

// {name: '赵云', age: 21}
// name
// 赵云
console.log(proxy.name);

上面的代码,创建了一个 person 对象,它有两个属性,分别为 name 和 age; 实例化了一个 Proxy ,将 person 作为第一个参数(所要拦截的目标对象),第二个传入的参数是一个对象,它有一个 get 属性,值是一个回调函数;

Proxy 支持的拦截操作

实例化 Proxy 的第二个参数是一个对象,其中可以指定 Proxy 支持的拦截操作;对几种常见的拦截操作进行举例说明;

  1. get(target, propKey, receiver)
    get 方法用于拦截某个属性的读取操作;
    get 方法接收三个参数,目标对象、属性名、proxy 实例本身(操作行为所针对的对象);最后一个参数是可选的;
    示例:
    const person = {
    	name: "赵云"
    }
    
    const proxy = new Proxy(person, {
    	get: function(target, propKey) {
    		if (propKey in target) {
    			return target[propKey];
    		} else {
    			throw new ReferenceError(`属性名${propKey}不存在`);
    		}
    	}
    });
    
    console.log(proxy.name);    // 赵云
    console.log(proxy.age);     // Uncaught ReferenceError: 属性名age不存在
    
  2. set(target,propKey,value,receiver)
    set 方法用来拦截某个属性的赋值操作;
    set 方法接收四个参数,目标对象、属性名、属性值和 Proxy 实例本身;最后一个参数是可选的;
    示例:
    假定 Person 对象有一个 age 属性,该属性应该是一个不大于 200 的整数,那么可以使用 Proxy 保证 age 的属性值符合要求;
    const person = {
    	age: 20
    }
    
    // 年龄大于 200 报错
    const proxy = new Proxy(person, {
    	set: function(target, propKey, propValue) {
    		if (propKey === 'age') {
    			if (!Number.isInteger(propValue)) {
    				throw new ReferenceError(`age 不是一个整数`);
    			}else if (propValue > 200) {
    				throw new ReferenceError(`age 不能超过200`);
    			}
    		}
    
    		// 满足条件的 age 及其他属性,直接保存
    		target[propKey] = propValue;
    		return true;
    	}
    });
    proxy.age = 28;
    
    console.log(proxy.age);         // 28
    console.log(proxy.age = "28");  // Uncaught ReferenceError: age 不是一个整数
    console.log(proxy.age = 288);   // Uncaught ReferenceError: age 不能超过200
    
  3. has(target,propKey)
    has() 方法用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效; 典型的操作就是in运算符;
    has() 方法可以接受两个参数,分别是目标对象、需查询的属性名;
  4. deleteProperty(target,propKey)
    deleteProperty() 方法用于拦截 delete 操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete 命令删除;
  5. ownKeys(target)
    ownKeys() 方法用来拦截对象自身属性的读取操作; 具体来说,拦截以下操作;
    • Object.getOwnPropertyNames()
    • Object.getOwnPropertySymbols()
    • Object.keys()
    • for...in循环
  6. getOwnPropertyDescriptor(target, propKey)
    getOwnPropertyDescriptor() 方法拦截 Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者 undefined;
  7. defineProperty(target, propKey, propDesc)
    defineProperty() 方法拦截了 Object.defineProperty() 操作;
  8. preventExtensions(target)
    preventExtensions() 方法拦截 Object.preventExtensions(); 该方法必须返回一个布尔值,否则会被自动转为布尔值
    注意:
    这个方法有一个限制,只有目标对象不可扩展时(即 Object.isExtensible(proxy) 为 false ),proxy.preventExtensions 才能返回 true,否则会报错;
  9. getPrototypeOf(target)
    getPrototypeOf() 方法主要用来拦截获取对象原型; 具体来说,拦截下面这些操作;
    • Object.prototype.proto
    • Object.prototype.isPrototypeOf()
    • Object.getPrototypeOf()
    • Reflect.getPrototypeOf()
    • instanceof
  10. isExtensible(target)
    isExtensible() 方法拦截 Object.isExtensible() 操作;
  11. setPrototypeOf(target, proto)
    setPrototypeOf() 方法主要用来拦截 Object.setPrototypeOf() 方法;
  12. apply(target, object, args)
    apply 方法拦截函数的调用、callapply 操作;
    apply 方法接受三个参数,目标对象、目标对象的上下文对象(this)、目标对象的参数数组;
  13. construct(target, args)
    construct() 方法用于拦截 new 命令,下面是拦截对象的写法
    const proxy = new Proxy({}, {
    	construct: function (target, args, newTarget) {
    		return new target(...args);
    	}
    });
    
    construct() 方法可以接收三个参数
    1. target:目标对象;
    2. args:构造函数的参数数组;
    3. newTarget:创造实例对象时,new命令作用的构造函数

取消代理

Proxy.revocable(target, handler);

使用场景

  1. 数据校验(校验表单)
    let data = {
    	count: 8
    };
    
    data = new Proxy(data, {
    	set: function(target, propKey, propValue, proxy) {
    		if (propKey === "count" && typeof propValue !== 'number') {
    			// 不是 number 类型抛出错误
    			throw Error(`count 属性只能设置 number 类型数据`);
    		}
    		return Reflect.set(target, propKey, propValue, proxy);
    	}
    });
    data.count = 88;
    console.log(data.count);        // 88
    console.log(data.count = "99"); // Uncaught Error: count 属性只能设置 number 类型数据
    
  2. 私有化 api, 防止内部属性被外部读写
    let api = {
    	_apiKey: "123abc456def",
    	getUser: function(userId) {},
    	setUser: function(userId, config) {}
    }
    
    console.log(api._apiKey);   // 此处可用
    const restricted = ['_apiKey'];
    api = new Proxy(api, {
    	set: function(target, propKey, propsValue, proxy) {
    		if (restricted.indexOf(propKey) > -1) {
    			throw Error(`${propKey} 不可访问`);
    		}
    		return Reflect.set(target, propKey, propsValue, proxy);
    	},
    
    	get: function(target, propKey, proxy) {
    		if (restricted.indexOf(propKey) > -1) {
    			throw Error(`${propKey} 不可访问`);
    		}
    		return Reflect.get(target, propKey, proxy);
    	}
    });
    
    // 一下操作都会抛出错误
    api._apiKey;
    api._apiKey = "123";
    
  3. 预警,拦截,过滤
  4. 实现观察者模式
  5. 服务端代理,跨域,取消请求等
posted @ 2022-04-20 15:34  太轻描淡写了  阅读(525)  评论(0编辑  收藏  举报