Proxy
-
1.
get(target, propKey, receiver)拦截对象属性的读取, 比如
proxy.foo和proxy['foo']最后一个参数
receiver是一个对象, 可选, 参见下面Reflect.get部分.const _obj = { name: 'veloma' }; const obj = new Proxy(_obj, { get: function(target, propKey, receiver) { if (propKey in target) return target[propKey]; throw new Error(`Property ${propKey} does not exist.`); } }); obj.name // 'veloma' obj.age // Error: Property age does not exist.我们来解释一下这段代码, 如果访问目标对象不存在的属性, 会抛出一个错误.如果没有这个拦截函数, 访问不存在的属性, 只会返回
undefinedget方法可以继承const proto = new Proxy({}, { get(target, propertyKey, receiver) { console.log('GET' + propertyKey); return target[propertyKey]; } }); const obj = Object.create(proto); obj.name; // GET name但是如果这样写的话,
get方法就监听不到了const obj = Object.create(proto); obj.name = 'veloma'; obj.name; // 这里就会执行get方法如果我们想要他执行, 该怎么办呢?
const proto = new Proxy({}, { get() {...}, set(target, propertyKey, value, receiver) { target[propertyKey] = value; return true; } }); const obj = Object.create(proto); obj.name = 'veloma'; obj.name; // GET name这里我们需要把
proto给改造一下, 给他加上一个set方法才行.🌰: 使用
get拦截, 实现数组读取负数的索引.function craeteArray(...elements) { let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) index = target.length + index; return Reflect.get(target, index, receiver); } }; const target = []; target.push(...elements); return new Proxy(target, handler); } const arr = createArray('a', 'b', 'c'); arr[-1]; // c我们来解释一下这段代码, 数组的位置参数是
-1, 就会输出数组的倒数最后一个成员, 其中有一句代码没有说过Reflect.get(target, propKey, receiver), 看不懂没关系, 这里只要知道他是返回数组的某个索引值即可, 下面Reflect部分会讲到.利用proxy, 可以将读取属性的操作(
get), 转变为执行某个函数, 从而实现属性的链式操作.const global = { double: n => n * 2, pow: n => n * n, reverseInt: n => n.toString().split('').reverse().join('') | 0 }; const pipe = (function () { return function (value) { const funcStack = []; const proxy = new Proxy({}, { get(pipeObject, fnName) { if (fnName === 'get') { return funcStack.reduce((val, fn) => fn(val), value); } funcStack.push(global[fnName]); return proxy; } }); return proxy; } }()); pipe(3).double.pow.reverseInt.get; // 63我们来解释一下这段代码
- 调用
pipe将value=3传进去, 这时候不会执行proxy, 最后将proxy返回. - 这时候
pipe(3)是一个proxy, 当调用double方法的时候, 会执行proxy的get方法, 当执行get方法的时候,pipeObject一直为{}, 而这时候fnName则是double, 判断fnName不为'get', 则将该方法push到funcStack中, 再返回proxy. - 这时候
pipe(3).double依旧是一个proxy, 当调用pow方法的时候也会执行proxy的get方法, 而这时候fnName是pow,fnName依旧不是'get', 所以也会将pow方法push到funcStack中, 然后返回proxy. - 这时候
pipe(3).double.pow依旧是proxy, 接着调用reverseInt方法, 执行结果与double方法和pow方法一致. - 最后在调用
get方法的时候, 将funcStack中的方法, 一个一个拿出来执行, 并返回最终的执行结果(最后返回的结果不是proxy).
上面的代码设置proxy后, 达到了奖函数名链式使用的结果.
🌰: 利用
get拦截, 实现一个生成各种DOM节点的通用函数dom.const dom = new Proxy({}, { get(target, property) { return function(attrs = {}, ...children) { const el = document.createElement(property); for (let prop of Object.keys(attrs)) { el.setAttribute(prop, attrs[prop]); } for (let child of children) { if (typeof child === 'string') { child = document.createTextNode(child); } el.appendChild(child); } return el; } } }); const el = dom.div({}, 'Hello, my name is ', dom.a({href: '//example.com'}, 'Mark'), '. I like', dom.ul({}, dom.li({}, 'The web'), dom.li({}, 'Food'), dom.li({}, '...actually that\'s it') ) );上面这个方法就比较简单了, 其实就是相当于这样写
function createElement(property, attrs = {}, ...children) { const el = document.createElement(property); for (let prop of Object.keys(attrs)) { el.setAttribute(prop, attrs[prop]); } for (let child of children) { if (typeof child === 'string') { child = document.createTextNode(child); } el.appendChild(child); } return el; } const el = createElement('div', {}, 'Hello, my name is ', createElement('a', {hreft: '//example.com'}, 'Mark'), createElement('ul', {}, createElement('li', {}, 'The web'), createElement('li', {}, 'Food'), createElement('li', {}, '...actually that\'s it') ) ); - 调用
-
2.
set(target, propKey, value, receiver)拦截对象属性的设置, 比如
proxy.foo = v或者proxy['foo'] = v, 返回一个布尔值.🌰: 假设Person对象有一个age属性, 该属性应该是一个不大于150的整数, 那么可以使用Proxy保证age的属性之符合要求.
const validator = { set(target, key, value) { if (key === 'age') { // isInteger方法, 看名字就知道, 他用来判断一个值是否为整数, // 要注意, 值必须为number类型, 如果是string类型的数字是不行的 if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 150) { throw new RangeError('The age seems invalid'); } } target[key] = value; return true; } }; const person = new Proxy({}, validator); person.age = 100; person.age; // 100 person.age = 'young'; // TypeError: The age is not an integer person.age = 200; // RangeError: The age seems invalid我们来解释一下这段代码, 由于设置了存值函数
set, 任何不符合要求的age属性赋值, 都会抛出一个错误. 利用set方法, 还可以数据绑定, 即当对象发生变化时, 会自动更新DOM.我们还可以结合
get和set方法来做私有属性const handler = { get(target, key) { invariant(key, 'get'); return target[key]; }, set(target, key, value) { invariant(key, 'set'); target[key] = value; return true; } } function invariant(key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}" property.`); } } const target = { name: 'veloma', _name: 'timer' }; const proxy = new Proxy(target, handler); proxy.name; // veloma proxy.name = 'timer'; proxy._name; // Error: Invalid attempt to get private "_name" property proxy._name = 'veloma'; // Error: Invalid attempt to set private "_name" property我们来解释一下这段代码, 只要读写的属性名的第一个字符是下划线, 一律报错, 从而达到禁止读写内部属性的目的.
-
3.
has(target, propKey)has方法用来拦截HasProperty操作, 即判断对象是否具有某个属性时, 对这个方法会生效. 典型的操作就是in运算符.(拦截propKey in proxy的操作, 以及对象的hasOwnProperty方法, 返回一个布尔值).来个🌰: 使用
has方法隐藏某些属性, 不会被in运算符发现.const handler = { has(target, key) { if (key[0] === '_') return false; return key in target; } }; const target = { _prop: 'foo', prop: 'foo' }; const proxy = new Proxy(target, handler); '_prop' in proxy; // false 'prop' in proxy; // true我们来解释一下这段代码, 如果属性名是以下划线_开头的,
proxy.has就会返回false, 从而不被in运算符发现.如果原对象不可配置或者禁止扩展, 这是
has拦截会报错const obj = { a: 10 }; Object.preventExtensions(obj); const p = new Proxy(obj, { has: (target, prop) => false }); 'a' in p; // TypeError is thrown上面代码中, obj对象禁止扩展, 结果使用has拦截就会报错.
注意:
has方法拦截的是HasProperty操作, 而不是HasOwnProperty操作, 即has方法不判断一个属性是对象自身属性, 还是继承的属性. 另外, 虽然for...in循环也用到了in运算符, 但是has拦截对for...in循环不生效.const stu1 = { name: '张三', score: 59 }; const stu2 = { name: '李四', score: 99 }; const handler = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } } const proxy1 = new Proxy(stu1, handler); const proxy2 = new Proxy(stu2, handler); 'score' in proxy1; // 张三 不及格 false 'score' in proxy2; // true for (let a in proxy1) { console.log(proxy1[a]); } // 张三 // 59 for (let b in proxy2) { console.log(proxy2[b]); } // 李四 // 99上面代码中,
has拦截只对in循环生效, 对for...in循环不生效, 导致不符合要求的属性没有被排除在for...in循环之外. -
4.
deleteProperty(target, propKey)deleteProperty方法用于拦截delete操作, 如果这个方法抛出错误或者返回false, 当前属性就无法被delete命令删除.(拦截delete proxy[propKey]的操作, 返回一个布尔值).const handler = { deleteProperty(target, key) { invariant(key, 'delete'); return true; } } function invariant(key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}" property`); } } const target = { _prop: 'foo' }; const proxy = new Proxy(target, handler); delete proxt._prop; // Invalid attempt to delete private "_prop" property上面代码中,
deleteProperty方法拦截了delete操作符, 删除第一个字符为下划线的属性会报错. -
5.
ownKeys(target)拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy), 返回一个数组.该方法返回对象所有自身的属性, 而Object.keys()仅返回对象可遍历的属性.const target = {}; const handler = { ownKeys(target) { return ['hello', 'world']; } }; const proxy = new Proxy(target, handler); Object.keys(proxy); // ['hello', 'world']上面代码拦截了对于
target对象的Object.keys操作, 返回预先设定的数组.下面的例子是拦截第一个字符为下划线的属性名.
const target = { _bar: 'foo', _prop: 'bar', prop: 'baz' }; const handler = { ownKeys(target) { return Reflect.ownKeys(target).filter(key => key[0] !== '_'); } } const proxy = new Proxy(target, handler); for (let key of Object.keys(proxy)) { console.log(target[key]); } // baz -
6.
getOwnPropertyDescriptor(target, propKey)getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor, 返回一个属性描述对象或者undefined(拦截Object.getPropertyDescriptor(target, propKey)).const handler = { getOwnPropertyDescriptor(target, key) { if (key[0] === '_') { return; } return Object.getOwnPropertyDescriptor(target, key); } }; const target = { _foo: 'bar', baz: 'tar' }; const proxy = new Proxy(target, handler); Object.getOwnPropertyDescriptor(proxy, 'wat'); // undefined Object.getOwnPropertyDescriptor(proxy, '_foo'); // undefined Object.getOwnPropertyDescriptor(proxy, 'baz'); // { value: 'tar', writable: true, enumerable: true, configurable: true }上面代码中,
handler.getOwnPropertyDescriptor方法对于第一个字符为下划线的属性名会返回undefined. -
7.
defineProperty(target, propKey, propDesc)defineProperty方法拦截了Object.defineProperty操作(拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs), 返回一个布尔值).const handler = { defineProperty(target, key, descriptor) { return false; } } const target = {}; const proxy = new Proxy(target, handler); proxy.foo = 'bar'; // TypeError: proxy defineProperty handler returned false for property '"foo"'上面代码中,
defineProperty方法返回false, 导致添加新属性会抛出错误. -
8.
preventExtensions(target)preventExtensions方法拦截Object.preventExtensions.该方法必须返回一个布尔值.这个方法有一个限制, 只有当Object.isExtensible(proxy)为false(即不可扩展)时,proxy.preventExtensions才能返回true, 否则会报错.(拦截Object.preventExtensions(proxy), 返回一个布尔值).const proxy = new Proxy({}, { preventExtensions(target) { return true; } }); Object.preventExtensions(proxy); // 报错上面代码中,
proxy.preventExtensions方法返回true, 但这时Object.isExtensible(proxy)会返回true, 因此报错.为了防止出现这个问题, 通常要在
proxy.preventExtensions方法里面, 调用一次Object.preventExtensions.const proxy = new Proxy({}, { preventExtensions(target) { console.log('called'); Object.preventExtensions(target); return true; } }); Object.preventExtensions(proxy); // called true -
9.
getPrototypeOf(target)getPrototypeOf方法主要用来拦截Object.getPrototypeOf()运算符, 以及其他一些操作(拦截Object.getPrototypeOf(proxy), 返回一个对象).Object.prototype.__proto__Object.prototype.isPrototypeOf()Object.getPrototypeOf()Reflect.getPrototypeOf()instance运算符
来个🌰:
const proto = {}; const proxy = new Proxy({}, { getPrototypeOf(target) { return proto; } }); Object.getPrototypeOf(proxy) === proto; // true上面代码中,
getPrototypeOf方法拦截Object.getPrototypeOf(), 返回proto对象 -
10.
isExtensible(target)拦截
Object.isExtensible(proxy), 返回一个布尔值.const proxy = new Proxy({}, { isExtensible(target) { console.log('calle'); return true; } }); Object.isExtensible(proxy); // called true上面代码设置了
isExtensible方法, 在调用Object.isExtensible时会输出called.这个地方有一个强限制, 如果不能满足下面的条件, 就会抛出错误.
Object.isExtensible(proxy) === Object.isExtensible(target)来个🌰:
const proxy = new Proxy({}, { isExtensible(target) { return false; } }); Object.isExtensible(proxy); // 报错 -
11.
setPrototypeOf(target, proto)拦截
Object.setPrototypeOf(proxy, proto), 返回一个布尔值.如果目标对象是函数, 那么还有两种额外操作可以拦截.
const handler = { setPropertyOf(target, proto) { throw new Error('Changing the prototype is forbidden'); } } const proto = {}; const target = function() {}; const proxy = new Proxy(target, handler); proxy.setPrototypeOf(proxy, proto); // Error: Changing the prototype is forbidden上面代码中, 只要修改
target的原型对象, 就会报错. -
12.
apply(target, object, args)apply方法拦截proxy实例的函数调用、call和apply操作, 比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)const handler = { apply(target, ctx, args) { return Reflect.apply(...arguments); } }apply方法可以接受三个函数, 分别是目标对象(函数自己)、目标对象的上下文对象(this)和目标对象的参数数组来个🌰:
const target = function () { return 'I am the veloma' }; const handler = { apply: () => 'I am the timer' }; const p = new Proxy(target, handler); p(); // I am the timer上面代码中, 变量
p是Proxy的实例, 当他作为函数调用时(p()), 就会被apply方法拦截, 返回一个字符串.再来举个🌰:
const twice = { apply(target, ctx, args) { return Reflect.apply(...arguments) * 2; } }; function sum(left, right) { return left + right; } const proxy = new Proxy(sum, twice); proxy(1, 2); // 6 proxy.call(null, 5, 6); // 22 proxy.apply(null, [7, 8]); // 30上面代码中, 每当执行
proxy函数(直接调用或call和apply调用), 就会被apply方法拦截.另外, 直接调用
Reflect.apply方法也会被拦截Reflect.apply(proxy, null, [9, 10]); // 38 -
13.
constructor(target, args)constructor方法用于拦截new命令, (拦截Proxy实例作为构造函数调用的操作, 比如new proxy(...args).)写法
const handler = { constructor(target, args, newTarget) { return new target(...args); } }constructor方法接受两个参数.target: 目标对象args: 构造函数的参数对象
来个🌰:
const proxy = new Proxy(function() {}, { constructor(target, args) { console.log(`called ${args.join(', ')}`); return { value: args[0] * 10 }; } }); new proxy(1).value; // called: 1 10constructor方法返回的必须是一个对象, 否则会报错const proxy = new Proxy(function() {}, { constructor(target, argumentsList) { return 1; } }); new proxy(); // 报错 -
14.
Proxy.revocable()Proxy.revocable方法返回一个可取消的Proxy实例const target = {}; const handler = {}; const { proxy, revoke } = Proxy.revocable(target, handler); proxy.foo = 123; proxy.foo; // 123 revoke(); proxy.foo; // TypeError: RevokedProxy.revocable方法返回一个对象, 该对象的proxy属性是Proxy实例,revoke属性是一个函数, 可以取消Proxy实例. 上面代码中, 当执行revoke函数之后, 再访问Proxy实例, 就会抛出一个错误(如果访问原target对象的话可以). -
this问题
- 用一句话来概述, 原target与Proxy代理之后的proxy不相等.
- 有一些原生对象的内部属性, 只有通过正确的
this才能拿到, 所以Proxy也无法代理这些原生对象的属性, 看下面的🌰
const target = new Date(); const handler = {}; const proxy = new Proxy(target, handler); proxy.getDate(); // TypeError: this is not a Date object.上面代码中,
getDate方法只能在Date对象实例上面拿到, 如果this不是Date对象实例就会报错.这时,this绑定原始对象, 就可以解决这个问题.const date = new Date('2020-01-01'); const handler = { get(target, prop) { if (prop === 'getDate') { return target.getDate.bind(date); } return Reflect.get(target, prop); } }; const proxy = new Proxy(date, handler); proxy.getDate(); // 1
Reflect
Reflect对象的设计目的
- 将
Object对象的一些明显属于语言内部的方法(比如Object.defineProperty), 放到Reflect对象上. 现阶段, 某些方法同时在Object和Reflect对象上部署, 未来的新方法将只部署在Reflect对象上. - 修改某些
Object方法的返回结果, 让其变的更合理, 比如Object.defineProperty(obj, name, desc)在无法定义属性时, 会抛出一个错误, 而Reflect.defineProperty(obj, name, desc)则会返回false. - 让
Object操作都变成函数行为. 某些Object操作是命令式, 比如name in obj和delete obj[name], 而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让他们变成了函数行为. Reflect对象的方法与Proxy对象的方法一一对应, 只要是Proxy对象的方法, 就能在Reflect对象上找到对应的方法.这就让Proxy对象可以方便的调用对应的Reflect方法, 完成默认行为, 作为修改行为的基础. 也就是说, 不管Proxy怎么修改默认行为, 你总可以在Reflect上获取默认行为.
Reflect对象的方法
Reflect对象的方法清单如下, 共13个.
-
Reflect.apply(fn, thisArg, args)
等同于
Function.prototype.apply.call(fn, thisArg, args).一般来说, 如果要绑定一个函数的this对象, 可以这样写fn.apply(obj, args), 但是如果函数定义了自己的apply方法, 就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作.另外, 需要注意的是,Reflect.set()、Reflect.defineProperty()、Reflect.freeze()、Reflect.seal()和Reflect.preventExtensions()返回一个布尔值, 表示操作是否成功.他们对应的Object方法, 失败时都会抛出错误.// 失败时抛出的错误 Object.defineProperty(obj, name, desc); // 失败时返回false Reflect.defineProperty(obj, name, desc);上面代码中,
Reflect.defineProperty方法的作用与Object.defineProperty是一样的, 都是为对象定义一个属性.但是,Reflect.defineProperty方法失败时, 不会抛出错误, 只会返回false. -
Reflect.construct(target, args)
等同于
new target(...args), 这提供了一种不使用new, 来调用构造函数的方法 -
Reflect.get(target, name, receiver)
查找并返回
target对象的name属性, 如果没有该属性, 则返回undefined.如果
name属性部署了读取函数, 则读取函数的this绑定receiver.const obj = { get foo() { return this.bar(); }, bar: function() { console.log('bar') } } // 下面语句会让this.bar() // 变成调用wrapper.bar() Reflect.get(obj, 'foo', wrapper); -
Reflect.set(target, name, value, receiver)
设置
target对象的name属性等于value.如果name属性设置了赋值函数, 则赋值函数的this绑定receiver. -
Reflect.defineProperty(target, name, desc)
-
Reflect.deleteProperty(target, name)
等同于
delete obj[name] -
Reflect.has(target, name)
等同于
name in obj -
Reflect.ownKeys(target)
-
Reflect.isExtensible(target)
-
Reflect.preventExtensions(target)
-
Reflect.getOwnPropertyDescriptor(target, name)
-
Reflect.getPrototypeOf(target)
读取对象的
__proto__属性, 对应Object.setPrototypeOf(obj, newProto). -
Reflect.setPrototypeOf(target, prototype)
-
实例: 使用Proxy实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象, 一旦对象有变化, 函数就会自动执行.
const person = observable({ name: 'veloma', age: 22 }); function print() { console.log(`${person.name}, ${person.age}`); } observe(print); person.name = 'timer'; // 李四, 20上面代码中, 数据对象
person是观察目标, 函数print是观察者.一旦数据对象发生变化,print就会自动执行.下面, 使用
Proxy写一个观察者模式的最简单实现, 即实现observable和observe这两个函数.思路是observable函数返回一个原始对象的Proxy代理, 拦截赋值操作, 触发充当观察者的各个函数.const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn); const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result; }上面代码中, 先定义了一个
Set集合, 所有观察者函数都放进这个集合. 然后,observable函数返回原始对象的代理, 拦截赋值操作. 拦截函数Set中, 会自动执行所有观察者.