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.
我们来解释一下这段代码, 如果访问目标对象不存在的属性, 会抛出一个错误.如果没有这个拦截函数, 访问不存在的属性, 只会返回
undefined
get
方法可以继承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 10
constructor
方法返回的必须是一个对象, 否则会报错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: Revoked
Proxy.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
中, 会自动执行所有观察者.