JavaScript简明教程-Proxy

724 阅读7分钟

基本概念

对指定对象的操作进行拦截,用设定好的行为覆盖默认行为,达到对外界访问进行过滤和改写(如属性查找、赋值、枚举、函数调用等)

new Proxy(target, handler);

  • target 是用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler 容纳一批特定属性的占位符对象,其属性是当执行一个操作时定义代理的行为的函数
let targetObj = {time:30};

/// proxy 和 proxy1是Proxy的实例化对象
let proxy = new Proxy(targetObj, {
	get: (target, property) => 35
});
let proxy1 = new Proxy(targetObj, {});

proxy.time; // 35 相关方法被拦截,返回改写后的值
proxy1.time; // 30 

/// 如果handler没有设置,代理对象 proxy1 的行为和目标对象行为一致
proxy1.foo = 'foo';
targetObj.foo; // foo 对代理对象的修改,影响到了目标对象
proxy; // Proxy {time: 30, foo: "foo"} 

/// 虽然代理对象 proxy 一样可以看到目标对象值已经更新,但是输出时还按设置的代理行为进行
proxy.foo; // 35 
proxy.foo = 'bar'; // 直接修改到目标对象
proxy1.foo; // bar proxy1没有进行拦截操作,所以行为和目标对象一致

从上面可以看出代理对象非常类似于给目标对象设置了一个别名的操作对象,外界对该对象访问,都必须先通过这层拦截,这层拦截可以对外界的访问进行过滤和改写,一个代理器(Proxy)可以对属性的读取(get)和设置(set)分别进行拦截

let targetObj = {time:30};
let proxy2 = new Proxy(targetObj, {
	get: (target, property) => 35,
	set: (target, key, value, receiver) => {
		console.log(target); // 目标对象
		console.log(key);	// 被设置的属性名
		console.log(value);	// 被设置的新值
		console.log(receiver); // 接收器,这里是proxy2
		target[key] = `由proxy2设置的值:${value}`;
	}
});
proxy2.bar = '22';
targetObj.bar; // 由proxy2设置的值:22

Proxy支持的拦截

  • getPrototypeOf(target) 当读取代理对象的原型时,该方法就会被调用
    • Object.prototype.__proto__Object.prototype.isPrototypeOfObject.getPrototypeOfReflect.getPrototypeOf()instanceof 会触发这个代理方法的运行
    • 参数:
      • target 被代理的目标对象
    • 返回值: 必须返回一个对象值或者null
let info = {name: 'rede'}
let foo = Object.create(info);
let handler = {
	getPrototypeOf(target) {
		console.log('触发拦截器信息');
		return Reflect.getPrototypeOf(target)
	}
}
let proxy = new Proxy(foo, handler);

proxy.__proto__; // 触发拦截器信息 {name: "rede"}
info.isPrototypeOf(proxy); // 触发拦截器信息 true
Object.getPrototypeOf(proxy); // 触发拦截器信息 {name: "rede"}
Reflect.getPrototypeOf(proxy); // 同上
proxy instanceof Object; // 触发拦截器信息 true

该拦截器的返回值必须是对象或者null,否则会报错

let handler = {
	getPrototypeOf(target) {
		return false;
	}
};
let proxy = new Proxy({}, handler);
proxy.__proto__; // Uncaught TypeError
...

let handler = {
	getPrototypeOf(target) {
		return null;
	}
};
let proxy = new Proxy({}, handler);
proxy.__proto__; // null 返回 null 是可以的
  • setPrototypeOf(target, proto) 当为现有对象设置原型时,该方法会被调用
    • Object.setPrototypeOf、Reflect.setPrototypeOf 会触发这个代理方法的运行
    • 参数:
      • target 被拦截对象
      • proto 是对象新原型或者null
    • 返回值:布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截
let info = {name:'info', _name:'name', info:'info'};
let obj = {name:'obj'};
let proxy = new Proxy(obj, {
	setPrototypeOf (target, proto) {
		console.log('触发拦截方法: setPrototypeOf');
		if (Object.isExtensible(target)) {
			return Reflect.setPrototypeOf(target, proto);
		}
	}
});
...

Object.setPrototypeOf(proxy, info);
/// 触发拦截方法: setPrototypeOf
/// Proxy {name: "obj"}

proxy._name; // name

这里我们是对设置原型进行了拦截,此时的拦截行为是如果目标对象是可扩展的,会为拦截对象指定新的原型对象。

之所以会这么设置,是因为拦截器有个特殊规则,如果对象不可扩展,则不能设置改变目标的原型

Object.preventExtensions(obj); // 设置目标对象不可扩展
Object.setPrototypeOf(proxy, info); // Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish

此时如果我们再尝试修改obj的原型时就会报错,简单的处理方式可以像下面这样抛出个错误

proxy = new Proxy(obj, {
	setPrototypeOf (target, proto) {
		if (!Object.isExtensible(target)) {
			throw new Error('该对象无法进行扩展,不可指定新的原型');
		}
		return Reflect.setPrototypeOf(target, proto);
	}
})
  • isExtensible(target)
    • Object.isExtensible、Reflect.isExtensible 会触发这个代理方法的运行,相关方法是判断目标对象是否可扩展
    • 参数:
      • target 目标对象
    • 返回值: 布尔值(该方法只能返回布尔值,否则返回值会被自动转为布尔值)
let info = {name:'rede'};
let handler = {
	isExtensible(target) {
		console.log('触发拦截方法:isExtensible');
		return true;
	}
};
let proxy = new Proxy(info, handler);
Object.isExtensible(proxy); // 触发拦截方法:isExtensible true
...

let info = {name:'rede'};
let handler = {
	isExtensible(target) {
		console.log('触发拦截方法:isExtensible');
		return 1;
	}
};
let proxy = new Proxy(info, handler);
Object.isExtensible(proxy); // 触发拦截方法:isExtensible true (1转为true)
...

/// 这个方法的返回值,必须与目标对象的 isExtensible 属性保持一致,否则就会抛出错误
Object.freeze(info); // 把目标对象设为不可扩展
Object.isExtensible(info); // false
Object.isExtensible(proxy); // Uncaught TypeError: 'isExtensible' on proxy

proxy = new Proxy(info, {
    isExtensible(target) {
      console.log('触发拦截方法:isExtensible');
      return false; // 是否可扩展,改为和目标对象一样
		}
});
Object.isExtensible(proxy); // 触发拦截方法:isExtensible true
  • preventExtensions(target)
    • Object.preventExtensions、Reflect.preventExtensions 会触发这个代理方法的运行,相关方法是让目标对象变成不可扩展
    • 参数:
      • target 目标对象
    • 返回值: 布尔值 true 是设置成功,false 是设置失败,但是方法中不能直接true/false,必须有相关逻辑判断
let obj = {name:'name'}
let proxy = new Proxy(obj, {
	preventExtensions(target) {
		if (Reflect.isExtensible(target)) {
			throw new Error('该对象不可进行此操作')
		}
		return true;
	}
});
Object.preventExtensions(proxy); // Uncaught Error: 该对象不可进行此操作

/// 这里的行为可以理解成代理对象保持和目标对象一样的特性,如果这里不做校验,直接设为true 或者 false 就会存在代理对象和目标对象之间的差异

如果不希望某个对象被设置成不可扩展,就可以使用这个拦截器拦截。

这个拦截器有个特殊的要求,如果目标对象是不可以扩展。返回必须是true,要不就报TypeError

let obj = {name:'name'}
Object.preventExtensions(obj); // obj设为不可扩展
let proxy = new Proxy(obj, {
	preventExtensions(target) {
		if (Reflect.isExtensible(target)) {
			throw new Error('该对象不可进行此操作')
		}
		return false; // 故意改为 false,走到此处的目标对象应该是不可扩展的,此时必须返回 true
	}
});
Object.preventExtensions(proxy); // Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
  • getOwnPropertyDescriptor(target, propKey)
    • Object.getOwnPropertyDescriptor、Reflect.getOwnPropertyDescriptor 会触发这个代理方法的运行,相关方法是返回属性的描述对象
    • 参数:
      • target 目标对象
      • propKey 待返回描述对象的属性名
    • 返回值: Object 或者 undefined
let handler = {
	getOwnPropertyDescriptor (target, propKey) {
		if (propKey[0] === '_') {
			return;
		}
		return Reflect.getOwnPropertyDescriptor(target, propKey);
	}
}
let obj = {info: 'info', _info: 'info'};
let proxy = new Proxy(obj, handler);
Object.getOwnPropertyDescriptor(proxy, '_info'); // undefined

/// 上面的行为相当于是对私有属性的保护,以``_``开头的属性,不能直接看到相关属性信息
  • defineProperty(target, property, descriptor)
    • Object.defineProperty、Object.defineProperties、Reflect.defineProperty、proxy.property='value' 会触发这个代理方法的运行,相关方法是直接在目标对象上设置相关属性
    • 参数:
      • target 目标对象
      • property 待检索的属性名
      • descriptor 待定义或修改的属性描述
        • descriptor 只接受 enumerable、configurable、writable、value、get、set这几个值的设置,其他值会被无视
    • 返回值: 布尔值 表示该属性操作是否成功
let obj = new Proxy({}, {
	defineProperty (target, property, descriptor) {
		if (property.slice(0,1) === '_') {
			throw new Error('禁止新添加的私有属性');
		}
		return Reflect.defineProperty(target, property, descriptor);
	}
});
obj.foo = 'foo'; // 会被正常赋值
obj._foo = 'foo'; // Uncaught Error: 禁止新添加的私有属性

使用这个方式可以在属性定义时更好的规范代码写法,这个拦截器一样可以拦截Object.defineProperties 的设置

Object.defineProperties(obj, {
	bar: {value:'bar'},
	_info: {value:'info'}	
});  // Uncaught Error: 禁止新添加的私有属性
obj; // Proxy {bar: "bar"}  bar 属性会正常添加
...

/// 基于这种代理方法的限制,要留意设置属性时的顺序
Object.defineProperties(obj, {
	_bar: {value:'bar'},
	info: {value:'info'}	
});  // Uncaught Error: 禁止新添加的私有属性
obj; // Proxy {bar: "bar"} info没有设置成功

如果返回 false,属性也一样不能添加成功

let handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
let target = {};
let proxy = new Proxy(target, handler);
proxy.foo = 'bar';
proxy.foo; // undefined
  • has(target, propKey)
    • 属性查询: foo in proxy、继承属性查询: foo in Object.create(proxy)、Reflect.has 会触发这个代理方法的运行,相关方法是直接在目标对象上是否有相关属性
    • 参数:
      • target 目标对象
      • propKey 待检测的属性名
    • 返回值: 布尔值
/// proxy 只拦截has行为,其他行为和目的对象一致
let proxy = new Proxy({ time: 30, foo: "bar" }, {
	has: (target, propKey) => {
		if (propKey[0] === '_') { // 针对 _开头的属性,进行特殊处理
			return false;
		}
		return propKey in target;
	}
})
proxy; // Proxy {time: 30, foo: "bar"}

proxy.name = 'name';
'name' in proxy; // true

proxy._name = 'name';
'_name' in proxy; // false 此时 _name 属性是存在的
proxy; // Proxy {time: 30, foo: "bar", name: "name", _name: "name"}

约束:如果目标对象的某一属性本身不可被配置(或目标对象为不可扩展对象),则该属性不能够被代理隐藏

let obj = {name:'obj', info: 'info'};
Object.preventExtensions(obj);
let proxy = new Proxy(obj, {
	has: (target, propKey) => {
		return false
	},
});
'name' in obj; // true
'name' in proxy; // Uncaught TypeError
/// 原因就是 obj 此时是不可配置,代理方法的返回就不能是 false
  • get(target, propKey, receiver)
    • 访问属性: proxy[foo]和 proxy.bar、访问原型链上的属性: Object.create(proxy)[foo]、Reflect.get 会触发这个代理方法的运行,相关方法是在目标对象上进行属性的读取操作
    • 参数:
      • target 目标对象
      • propKey 属性名
      • receiverproxy 实例化本身,这个参数可选
    • 返回值: 任何值
let person = {
  name: "张三"
};

let proxy = new Proxy(person, {
  get: function(target, property) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError(`Property ${property} does not exist.`);
    }
  }
});
proxy.name; // 张三
proxy.age; // Uncaught ReferenceError: Property "age" does not exist.
person.age; // undefined 
/// proxy因为有拦截器的存在,所以访问不存在的属性才会报错,否则只会返回undefined

设置的 get 属性可以被继承

let obj = Object.create(proxy);
obj.age; // Uncaught ReferenceError: Property "age" does not exist.

get 特性的拦截可以使我们写出一些比较有想象力的写法。比如我们在项目运行时一般会区分不同的环境,可以根据这个环境不同返回不同的信息

let env = 'stg';
let proxy = new Proxy({}, {
	get (targetObj, propKey) {
		if (propKey === 'info' && env) {
			return env === 'stg' ? '项目运行在stg环境' : '项目运行在非stg环境';
		}
		return targetObj[propKey]
	}
});
proxy.name; // undefined
proxy.info; // 项目运行在stg环境
env = 'prd'; // 假定现在运行环境是prd
proxy.info; // 项目运行在非stg环境
/// 可以使用类似这种方式达到不同环境运行不同代码的逻辑 
  • set(target, propKey, value, receiver)
    • 指定属性值:proxy[foo] = bar 和 proxy.foo = bar、指定继承者的属性值:Object.create(proxy)[foo] = bar、Reflect.set 会触发这个代理方法的运行,相关方法是在目标对象上进行属性的设置操作
    • 参数:
      • target 目标对象
      • propKey 被设置的属性名
      • value 被设置的新值
      • receiver 最初被调用的对象
    • 返回值: 布尔值
/// get 和 set 在使用时,可以添加规则,实现私有变量不对外使用的语法(假定以_开头的变量为私有变量)
let obj = {name:'obj'};
let proxy = new Proxy(obj, {
	get (target, propKey) {
		if (propKey[0] === '_') {
			throw new ReferenceError("私有属性禁止访问");
		}
		return target[propKey];
	},
	set (target, propKey, value) {
		if (propKey[0] === '_') {
			throw new ReferenceError("私有属性禁止访问");
		}
		target[propKey] = value;
	}
});
proxy.name; // obj
proxy._name; // Uncaught ReferenceError: 私有属性禁止访问
proxy._name = 'info'; // Uncaught ReferenceError: 私有属性禁止访问
/// 这样就可以通过代理,添加一些访问/设置的限制
  • deleteProperty(target, propKey)
    • 删除属性: delete proxy[foo] 和 delete proxy.foo、Reflect.deleteProperty 会触发这个代理方法的运行,相关方法是在目标对象上进行属性的删除操作
    • 参数:
      • target 目标对象
      • propKey 待删除的属性
    • 返回值: 布尔值
// 使用上面的例子
let obj = {name:'obj', _name: 'info'};
let proxy = new Proxy(obj, {
	has: (target, propKey) => {
		return Reflect.has(target, propKey);
	},
	deleteProperty: (target, propKey) => {
		// 这里我们可以加一些特殊条件比如无法删除以_开头的变量
		if (propKey[0] === '_') {
			throw new Error('该属性为私有属性无法删除');
		}
		delete target[propKey]; // 从目标对象删除相关key值
		console.log('删除成功');
		return true;
	},
});
delete proxy.name;
// 删除成功
// true

delete proxy._name; // Uncaught Error: 该属性为私有属性无法删除
  • ownKeys(target)
    • Object.getOwnPropertyNames、Object.getOwnPropertySymbols、Object.keys、Reflect.ownKeys 会触发这个代理方法的运行,相关方法是获取目标对象自身的属性键组成的数组
    • 参数:
      • target 目标对象
    • 返回值: 可枚举对象
let obj = {info:'info', _name:'name', bar:'bar'}
let handler = {
	ownKeys (target) {
		return Reflect.ownKeys(target).filter(key => key[0] !== '_');
	}
};
let proxy = new Proxy(obj, handler);
Object.getOwnPropertyNames(proxy); // ["info", "bar"]
// 此时拦截器的行为是返回不以'_'开头的属性

ownKeys 如果显式返回结果,则要求结果必须是数组,数组中的元素必须是 String 类型或者 Symbol 类型

let obj = {info:'info', _name:'name', bar:'bar'};
Object.defineProperty(obj, 'key', {
  enumerable: false,
  value: 'static'
});
let handler = {
	ownKeys (target) {
		return false;
	}
}
let proxy = new Proxy(obj, handler);
Object.getOwnPropertyNames(proxy); // Uncaught TypeError...此时ownKeys返回的布尔值

使用 Object.keys 方法时,不存在的属性,Symbol值以及不可遍历属性 ownKeys 方法会过滤,不返回

let obj = {info:'info', _name:'name', bar:'bar'};
Object.defineProperty(obj, 'key', {
  enumerable: false, //  不可以遍历
  configurable: true,
  writable: true,
  value: 'static'
});
let handler = {
	ownKeys (target) {
		return ['info', '_info', '_name', 'bar', 'key', Symbol.for('secret')]; // 这里显示的返回了一些不存在的属性
	}
}
let proxy = new Proxy(obj, handler);
Object.keys(proxy); // ["info", "_name", "bar"] 不存在属性,Symbol,不可遍历属性都不返回

使用 Object.getOwnPropertyNames 时,不会过滤不存在属性以及不可遍历属性,但会过滤 Symbol 值

Object.getOwnPropertyNames(proxy); 
// ["info", "_info", "_name", "bar", "key"]
...

// Object.getOwnPropertySymbols 则会只返回 Symbol 值
Object.getOwnPropertySymbols(proxy); 
// [Symbol(secret)]
...

// for...in 一样可以触发此拦截器,返回值规则和 Object.keys 一致
for (let key in proxy) {
  console.log(key);
}
// info
// _name
// bar

如果目标对象不可扩展,ownKeys设置的返回数组只能是目标对象的属性,且不能有多余属性否则会报错

let obj = {name:'name', _name:'name'}
Object.freeze(obj);
let proxy = new Proxy(obj, {
	ownKeys (target) {
		return ['name', '_name', 'key']
	}
});
Object.keys(proxy); 
// Uncaught TypeError
/// 如果目标对象没有冻结,按之前的规则,此时返回的数组应该是过滤了 key 的数组
  • apply(target, object, args)
    • proxy(...args)、Function.prototype.apply、Function.prototype.call、Reflect.apply 会触发这个代理方法的运行,相关方法是指定的参数列表发起对目标(target)函数的调用
    • 参数:
      • target:目标对象
      • object:被调用时的上下文对象(this)
      • args:被调用时的参数数组
    • 返回值: 任何值
let handler = {
	apply (target, ctx, args) {
		if (args.length > 2) {
			throw new Error('该方法只接收两个参数,请避免输入过多参数');
		}
		return Reflect.apply(target, ctx, args);
	}
}
let sum = function (num1, num2) {
	return num1 + num2;
}
let proxy = new Proxy(sum, handler);
proxy(1, 2); // 3
proxy(1, 2, 3); // Uncaught Error: 该方法只接收两个参数,请避免输入过多参数
...

let handler = {
	apply (target, ctx, args) {
		if (ctx.version < 10) {
			throw new Error('当前版本不支持该方法');
		}
		return Reflect.apply(target, ctx, args);
	}
}
let sum = function (num1, num2) {
	return num1 + num2;
}
let proxy = new Proxy(sum, handler);
let obj = {version: 9, proxy}; 
obj.proxy(1,2); // Uncaught Error: 当前版本不支持该方法
obj.version = 10; // 更新虚拟的版本号
obj.proxy(1,2); // 3
/// 从上面例子可以看出,apply 的第二个参数是 Proxy 实例化对象的上下文,本例中就是obj对象
  • construct(target, args, newTarget)
    • new proxy(...args)、Reflect.construct 会触发这个代理方法的运行,相关方法是相当于 new 操作
    • 参数:
      • target 目标对象
      • args 参数列表
      • newTarget 最初被调用的构造函数
    • 返回值: 必须是一个对象
let handler = {
	construct (target, args, newTarget) {
		if (args[0] && Object.prototype.toString.call(args[0]) === '[object String]' && args[0].slice(0,4) !== 'mgs_') {
			throw new Error('第一个参数名非法,请以mgs_开头');
		}
		return new target(...args);
	}
}
let obj = new Proxy(function(){}, handler);
// 比如这个方法我们如果传入的第一个参数是字符,则必须是以'msg_'开头的字符
new obj('info'); // Uncaught Error: 第一个参数名非法,请以mgs_开头
// 如果我们想在对象初始化时做一些校验就可以写到这个拦截的方法中

方法

  • Proxy.revocable(target, handler) 创建一个可销毁的代理对象
    • 参数:
      • target 将用 Proxy 封装的目标对象。可以是任何类型的对象,包括原生数组,函数,甚至可以是另外一个代理对象
      • handler 一个对象,其属性是一批可选的函数,这些函数定义了对应的操作被执行时代理的行为
    • 返回值: 返回一个包含了代理对象本身和它的撤销方法的可撤销 Proxy 对象
      • 其结构为: {"proxy": proxy, "revoke": revoke}
        • proxy 表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉
        • revoke 撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象
let obj = {name:'name'};
let {proxy:info, revoke} = Proxy.revocable(obj, {});
info.name; // name
revoke(); // 销毁代理对象
info.name; // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked