Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写
let obj = new Proxy({}, {
get (target, p, receiver) {
console.log('getter')
return Reflect.get(target, p, receiver)
},
set (target, p, value, receiver) {
console.log('setter')
return Reflect.set(target, p, value, receiver)
},
})
obj.count = 1// setter
console.log(obj.count)// getter 1
同一拦截器函数可以设置拦截多个操作
const handler = {
get (target, name) {
if (name === 'prototype') {
return Object.prototype
}
return `hello ${name}`
},
apply (target, thisBinding, args) {
return args[0]
},
construct (target, args) {
return { value: args[1] }
},
}
let fproxy = new Proxy(function (x, y) {
return x + y
}, handler)
console.log(fproxy(1, 2))// 1
console.log(new fproxy(1, 2))// {value: 2}
console.log(fproxy.prototype === Object.prototype)// true
console.log(fproxy.foo)// hello foo
this 问题
在 Proxy 代理的的情况下,目标对象内部的 this 关键字会指向 Proxy 代理
const target = {
test: function() {
console.log(this === proxy)
}
}
const proxy = new Proxy(target, {})
target.test()// false
proxy.test()// true
然而,有些原生对象的内部属性只有通过正确的 this 才能获取,所以 Proxy 也无法代理这些原生对象的属性
const target = new Date()
const handler = {}
const proxy = new Proxy(target, handler)
console.log(target.getDate())// 13
console.log(proxy.getDate())// Uncaught TypeError: this is not a Date object.
解决方法:
const target = new Date()
const handler = {
get(target, prop) {
if (prop === 'getDate') {
// 用 this 绑定原始原始对象解决这个问题
return target.getDate.bind(target)
}
return Reflect.get(target, prop)
}
}
const proxy = new Proxy(target, handler)
console.log(target.getDate()) // 13
console.log(proxy.getDate()) // 13
方法 | 属性 |
---|---|
get(target,propKey,receiver) | 拦截对象属性的读取(如 proxy.foo ),最后一个参数是可选参数,参见下面 Reflect.get 部分 |
set(target,propKey,value,receiver) | 拦截对象属性的设置(如 proxy.foo=v ),返回一个布尔值 |
has(target,propKye) | 拦截 propKey in proxy 的操作,返回一个布尔值 |
deleteProperty(target,propKey) | 拦截 delete proxy[propKey] 的操作,返回一个布尔值 |
ownKeys(target) | 拦截 Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy) 返回一个数组 |
getOwnPropertyDescriptor(target,propKey) | 拦截 Object.getOwnPropertyDescriptor(proxy,propKey) ,返回属性的描述对象 |
defineProperty(target,propKey,propDesc) | 拦截 Object.defineProperty(proxy,propKey,PropDesc) Object.defineProperties(propxy,propDescs) 返回一个布尔值 |
preventExtensions(target) | 拦截 Object.preventExtensions(proxy) ,返回一个对象 |
getPrototypeOf(target) | 拦截 Object.getPrototypeOf(proxy) ,返回一个对象 |
isExtensible(target) | 拦截 Object.isExtensible(proxy) ,返回一个布尔值 |
setPrototypeOf(target,proto) | 拦截 Object.setPrototypeOf(proxy,proto) ,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截 |
apply(target,object,args) | 拦截 Proxy 实例,并将其作为函数调用的操作,比如 proxy(...args) proxy.call(object,...args) proxy.apply(...) |
construct(target,args) | 拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args) |
实例:
get
- 设置 Proxy 实现链式使用函数名的效果
const pipe = (function () {
return (value) => {
let funcStack = []
let oproxy = new Proxy({}, {
get (target, p, receiver) {
if (p === 'get') {
return funcStack.reduce((val, fn) => {
console.log(fn, val)
/*
n => n * 2 4
n => n * n 8
n => n.toString().split('').reverse().join('') | 0 64
*/
return fn(val)
}, value)
}
// 浏览器的全局对象是 window,node 的全局对象是 global
// 因此要放在页面执行, node 执行会出错
funcStack.push(window[p])
return oproxy
},
})
return oproxy
}
}())
// 注意要使用 var ,全局定义变量
var double = n => n * 2
var pow = n => n * n
var reverseInt = n => n.toString().split('').reverse().join('') | 0
console.log(pipe(4).double.pow.reverseInt.get)// 46
我的优化
let math = {
double: n => n * 2,
pow: n => n * n,
sqrt: n => Math.sqrt(n),
}
let pipe = x => new Proxy({}, {
get (target, key) {
if (key === 'end') {
return x
}
return pipe(math[key](x))
},
})
console.log(`result: ${pipe(4).pow.double.end}`)// result: 32
console.log(`result: ${pipe(4).sqrt.pow.double.end}`)// result: 8
- 函数传值
function fun(data) {
return new Proxy({}, {
get(target, prop, receiver) {
return `${data}——${prop}`
}
})
}
/*proxy.hhh().then(res => {
console.log(res)
})*/
const instanceFun = fun('pig')
console.log(instanceFun.wenhua)// pig——wenhua
注意:如果一个属性不可配置(configurable)或不可写(writable),则该属性不能被代理,通过 Proxy 对象访问该属性将会报错
set
- 验证年龄
let validator = {
set (obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer')
}
if (value > 200) {
throw new TypeError('The age seems invalid')
}
}
obj[prop] = value
},
}
let person = new Proxy({}, validator)
person.age = 100
// 报错
// person.age = 201
// person.age = 1.1
- 防止内部属性被外部读/写
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 proxy = new Proxy({}, handler)
proxy._prop = 1// Invalid attempt to set private "_prop" property
console.log(proxy._prop)// Invalid attempt to get private "_prop" property
注意:如果目标对象自身的某个属性不可写也不可配置,那么 set 不得改变这个属性的值,只能返回同样的值,否则报错
apply
apply 方法拦截函数的调用、call 和 apply 操作,可以接收三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组
// 下面代码,变量 p 是 Proxy 的实例,作为函数调用时就会被 apply 方法拦截,返回一个字符串
const handler = {
apply () {
return 'I am the proxy'
},
}
const target = function () {
return 'I am the target'
}
let p = new Proxy(target, handler)
console.log(p())// I am the proxy
- 二倍乘积拦截
const twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2
},
}
function sum (left, right) {
return left + right
}
let proxy = new Proxy(sum, twice)
console.log(proxy(1, 2))// 6
console.log(proxy.call(null, 4, 3))// 14
console.log(proxy.apply(null, [5, 9]))// 28
has
用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效,典型操作就是 in 运算符
- 隐藏某些属性,使其不被 in 运算符发现
const handler = {
has (target, key) {
if (key[0] === '_') {
return false
}
return key in target
},
}
let target = { _prop: 'foo', prop: 'bar' }
let proxy = new Proxy(target, handler)
console.log('_prop' in proxy)// false
console.log('prop' in proxy)// true
// 注意:has 方法拦截的是 HasProperty 操作,而不是 HasOwnProperty 操作
// 即 has 方法不判断一个属性是对象自身的属性还是继承的属性
// has 拦截只对 in 循环生效,对 for ... in 循环不生效
for (let key in proxy) {
console.log(proxy[key])// foo bar
}
construct
拦截 new 命令,接收两个参数:目标对象、构建函数的参数对象
const p = new Proxy(function () {}, {
construct (target, argArray, newTarget) {
console.log(`called: ${argArray.join(',')}`)
return { value: argArray[0] * 10 }
},
})
console.log((new p(2, 4, 5)).value)// 2,4,5 20
construct 方法返回的必须是一个对象,否则会报错
const p = new Proxy(function () {}, {
construct (target, argArray, newTarget) {
return 1
},
})
new p()// 报错
deleteProperty
拦截 delete 操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete 命令删除
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`)
}
}
let target = { _prop: 'foo', prop: 'bar' }
let proxy = new Proxy(target, handler)
console.log(delete proxy._prop)// Invalid attempt to delete private "_prop" property
console.log(delete proxy.prop)// true
注意:目标对象自身的不可配置(configurable)的属性不能被 deleteProperty 方法删除,否则会报错
defineProperty
defineProperty 方法拦截了 Object.defineProperty 操作
new Vue({
render: h => h(App),
}).$mount('#app')
const handler = {
defineProperty(target, key, descriptor) {
return false
}
}
let target = {}
let proxy = new Proxy(target, handler)
// 'defineProperty' on proxy: trap returned falsish for property 'foo'
proxy.foo = 'bar'
上面代码中,defineProperty 方法返回 false,导致添加新属性会抛出错误
注意:如果目标对象不可扩展(extensible),则 defineProperty 不能增加目标对象中不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则 defineProperty 方法不得改变这两个设置
getOwnPropertyDescriptor
getOwnPropertyDescriptor 方法拦截 Object.getOwnPropertyDescriptor,返回一个属性描述对象或者 undefined
- 设置对有 _ 的返回 undefined
const handler = {
getOwnPropertyDescriptor(target, key) {
if (key[0] === '_') {
return
}
return Object.getOwnPropertyDescriptor(target, key)
}
}
let target = { _foo: 'bar', baz: 'tar' }
let proxy = new Proxy(target, handler)
let [a, b, c] = [
Object.getOwnPropertyDescriptor(proxy, 'wat'),
Object.getOwnPropertyDescriptor(proxy, '_foo'),
Object.getOwnPropertyDescriptor(proxy, 'baz'),
]
// undefined undefined {value: "tar", writable: true, enumerable: true, configurable: true}
console.log(a,b,c)
getPrototypeOf
getPrototypeOf 方法主要用来拦截获取对象原型,具体来说,用于拦截以下操作:
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
- 拦截 Object.prototype(),返回 proto 对象
const proto = {}
const proxy = new Proxy({}, {
getPrototypeOf(target) {
return proto
}
})
console.log(Object.getPrototypeOf(proxy) === proto)// true
注意:getPrototypeOf 方法的返回值必须是对象或者 null,否则会报错。另外,如果目标对象不可扩展(extensible),getPrototypeOf 方法必须返回目标对象的原型对象
实例:web 服务的客户端
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来编写 Web 服务的客户端
const service = createWebService('http://example.com/data')
service.employees().then(json => {
const employees = JSON.parse(json)
// ...
})
上面代码新建了一个 Web 服务的接口实例,这个接口实例返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截即可
function createWebservice(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl + '/' + propKey)
}
})
}
相关文档
Vue 3.0 使用 ES6 Proxy 代替 Object.defineProperty
Proxy 和 Reflect