Appearance
Proxy
Proxy 用于修改默认行为。属于元编程(meta programming),即对编程语言进行编程。
Proxy :目标对象之前设置拦截,外界对该对象的访问,都必须先通过这层拦截。
js
var obj = new Proxy(
{},
{
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`)
return Reflect.get(target, propKey, receiver)
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`)
return Reflect.set(target, propKey, value, receiver)
}
}
)js
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2ES6 提供 Proxy构造函数,生产 Proxy 实例
js
var proxy = new Proxy(target, handler)target:所要拦截的目标对象
handler:定制拦截行为,处理函数
Proxy 使用
- 没有定制拦截行为,访问
proxy等于访问target
js
var target = {}
var handler = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
target.a // "b"- 将 Proxy 对象,设置到
object.proxy属性,可以在object对象上调用
js
var object = { proxy: new Proxy(target, handler) }- Proxy 实例也可作为其他对象的原型对象
js
var proxy = new Proxy(
{},
{
get: function (target, propKey) {
return 35
}
}
)
let obj = Object.create(proxy)
obj.time // 35- 拦截多个操作
js
var handler = {
get: function (target, name) {
if (name === 'prototype') {
return Object.prototype
}
return 'Hello, ' + name
},
apply: function (target, thisBinding, args) {
return args[0]
},
construct: function (target, args) {
return { value: args[1] }
}
}
var fproxy = new Proxy(function (x, y) {
return x + y
}, handler)
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === 'Hello, foo' // trueProxy 实例方法
Proxy.get
get(target, propKey, receiver):拦截对象属性的读取
target:目标对象propKey:属性名reciver(可选):Proxy 或继承 Proxy 对象
get方法可以继承
js
let proto = new Proxy(
{},
{
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey)
return target[propertyKey]
}
}
)
let obj = Object.create(proto)
obj.foo // "GET foo"第三个参数:指向原始读操作对象,一般为 Proxy 实例
getReceiver 属性:获取 receiver
js
const proxy = new Proxy(
{},
{
get: function (target, key, receiver) {
return receiver
}
}
)
proxy.getReceiver === proxy // truejs
const d = Object.create(proxy)
d.a === d // trued对象本身没有a属性,d会去原型proxy对象寻找,此时 receiver指向d,代表原始读操作那个对象
如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性
js
const target = Object.defineProperties(
{},
{
foo: {
value: 123,
writable: false,
configurable: false
}
}
)
const handler = {
get(target, propKey) {
return 'abc'
}
}
const proxy = new Proxy(target, handler)
proxy.foo
// TypeError: Invariant check failed应用:实现数组负数索引
js
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey)
if (index < 0) {
propKey = String(target.length + index)
}
return Reflect.get(target, propKey, receiver)
}
}
let target = []
target.push(...elements)
return new Proxy(target, handler)
}
let arr = createArray('a', 'b', 'c')
arr[-1] // c应用:实现属性链式操作
js
var pipe = function (value) {
var funcStack = []
var oproxy = new Proxy(
{},
{
get: function (pipeObject, fnName) {
// 非get, 函数入栈
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
// double(3) - 6
// pow(6) - 36
// reverseInt(36) - 63
return fn(val)
}, value)
}
funcStack.push(window[fnName])
return oproxy
}
}
)
return oproxy
}
var double = (n) => n * 2
var pow = (n) => n * n
var reverseInt = (n) => n.toString().split('').reverse().join('') | 0
pipe(3).double.pow.reverseInt.get // 63应用:生成各种 DOM 节点的通用函数dom(渲染函数)
js
const dom = new Proxy(
{},
{
get(target, property) {
// attrs - 属性
// children - 子元素
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"))
)
document.body.appendChild(el)Proxy.Set
set(target, propKey, value, receiver):拦截对象属性的设置
target:目标对象propKey:属性名value:属性值receiver(可选):...
set代理应当返回一个布尔值。严格模式下,set代理返回false或者undefined,都会报错。
某个属性不可写(writable),set不起作用
第四参数 receiver,原始的操作行为所在的那个对象
js
const handler = {
set: function (obj, prop, value, receiver) {
obj[prop] = receiver
return true
}
}
const proxy = new Proxy({}, handler)
const myObj = {}
Object.setPrototypeOf(myObj, proxy)
myObj.foo = 'bar'
myObj.foo === myObj // true应用:防止内部属性被外部读写
js
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 = {}
const proxy = new Proxy(target, handler)
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" propertyProxy.apply
apply(target, object, args):拦截函数的调用、call和apply操作
target:目标对象object:目标对象上下文(this)args:目标对象参数数组
js
var twice = {
apply(target, ctx, args) {
return Reflect.apply(...arguments) * 2
}
}
function sum(left, right) {
return left + right
}
var proxy = new Proxy(sum, twice)
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
// 直接调用`Reflect.apply`方法,也会被拦截
Reflect.apply(proxy, null, [9, 10]) // 38Proxy.has
has(target, propKey):拦截HasProperty操作,判断是否具有某个属性。
target:目标对象propKey:需查询属性名- 返回值:布尔值
注意项
对象不可配置(
configurable),或禁止扩展,has 拦截将报错拦截
HasProperty操作,而不是HasOwnProperty操作,不管自身还是继承属性has()拦截只对in运算符生效,对for...in循环不生效
js
let stu1 = { name: '张三', score: 59 }
let stu2 = { name: '李四', score: 99 }
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`)
return false
}
return prop in target
}
}
let oproxy1 = new Proxy(stu1, handler)
let oproxy2 = new Proxy(stu2, handler)
'score' in oproxy1
// 张三 不及格
// false
'score' in oproxy2
// true
for (let a in oproxy1) {
console.log(oproxy1[a])
}
// 张三
// 59
for (let b in oproxy2) {
console.log(oproxy2[b])
}
// 李四
// 99补充:禁止扩展对象
js
var obj = { a: 10 }
Object.preventExtensions(obj) // 禁止扩展应用:隐藏属性,不被 in 发现
js
var handler = {
has(target, key) {
if (key[0] === '_') {
return false
}
return key in target
}
}
var target = { _prop: 'foo', prop: 'foo' }
var proxy = new Proxy(target, handler)
'_prop' in proxy // falseProxy.construct
construct(target, args):拦截 Proxy 实例作为构造函数调用的(new)操作
target:目标对象,由于拦截构造函数,所以必须是函数args:构造函数的参数数组newTarget:创造实例对象时,new命令作用的构造函数返回值:返回必须为一个对象,否则会报错
this:指向
handler,而非实例对象jsconst handler = { construct: function (target, args) { console.log(this === handler) return new target(...args) } } let p = new Proxy(function () {}, handler) new p() // true
Proxy.deleteProperty
deleteProperty(target, propKey):拦截delete的操作
- target:目标对象
- propKey:属性名
- 返回值:返回一个布尔值。方法抛出错误或返回
false,表示当前属性无法被delete
注意点
- 拦截后需要手动
delete - 目标对象自身的不可配置(
configurable)的属性,无法删除,否则报错
Proxy.defineProperty
defineProperty(target, propKey, propDesc):拦截Object.defineProperty操作
- target:目标对象
- propKey:属性名
- propDesc:定义或修改的属性描述
- 返回一个布尔值
只是拦截,需要重写逻辑
对象不可扩展(non-extensible):defineProperty()不能增加不存在属性
属性不可写(writable)或不可配置(configurable):defineProperty()方法不得改变这两个设置
Proxy.getOwnPropertyDescriptor
getOwnPropertyDescriptor(target, propKey):返回属性的描述对象或undefined。拦截Object.getOwnPropertyDescriptor()
Proxy.getPrototypeOf
getPrototypeOf(target):拦截获取对象原型,返回一个对象(必须是object或null)。
拦截下面操作
Object.prototype.__proto__Object.prototype.isPrototypeOf()Object.getPrototypeOf()Reflect.getPrototypeOf()instanceof
目标对象不可扩展(non-extensible), getPrototypeOf()方法必须返回目标对象的原型对象
Proxy.isExtensible
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
注意事项:
只能返回布尔值,其他值会自动转为布尔
强限制:返回值必须与目标对象的
isExtensible属性保持一致,否则抛出错误
js
Object.isExtensible(proxy) === Object.isExtensible(target)Proxy.ownKeys
ownKeys(target):拦截对象自身属性的读取操作
拦截下面操作
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys()for...in循环
注意事项:
- 必须返回数组,且数组成员为字符串或Symbol。如果有其他类型的值,或者返回的根本不是数组,就会报错。
js
var obj = {}
var p = new Proxy(obj, {
ownKeys: function (target) {
return [123, true, undefined, null, {}, []]
}
})
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 123 is not a valid property name- for...in 循环拦截,没有 a,b 属性,不会有任何输出
js
const obj = { hello: 'world' }
const proxy = new Proxy(obj, {
ownKeys: function () {
return ['a', 'b']
}
})
for (let key in proxy) {
console.log(key) // 没有任何输出
}- 不可配置成员必须返回
js
var obj = {}
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: true,
value: 10
})
var p = new Proxy(obj, {
ownKeys: function (target) {
return ['b']
}
})
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'- 目标对象是不可扩展的(non-extensible),必须返回原对象的所有属性,且不能包含多余属性
js
var obj = {
a: 1
}
Object.preventExtensions(obj)
var p = new Proxy(obj, {
ownKeys: function (target) {
return ['a', 'b']
}
})
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensibleObject.keys()自动过滤以下三类属性,不会返回
- 目标对象上不存在的属性
- 属性名为 Symbol 值
- 不可遍历(
enumerable)的属性
js
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for('secret')]: '4'
}
Object.defineProperty(target, 'key', {
enumerable: false,
configurable: true,
writable: true,
value: 'static'
})
let handler = {
ownKeys(target) {
return ['a', 'd', Symbol.for('secret'), 'key']
}
}
let proxy = new Proxy(target, handler)
Object.keys(proxy)
// ['a']Proxy.preventExtensions
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
限制:只有目标对象不可扩展(即Object.isExtensible(proxy)为false),proxy.preventExtensions才能返回true
解决:在proxy.preventExtensions()方法里面,调用一次Object.preventExtensions()
js
var proxy = new Proxy(
{},
{
preventExtensions: function (target) {
console.log('called')
Object.preventExtensions(target)
return true
}
}
)
Object.preventExtensions(proxy)
// "called"
// Proxy {}Proxy.setPrototypeOf
setPrototypeOf(target, proto):返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。拦截Object.setPrototypeOf(proxy, proto),
注意:只能返回布尔值,否则会被自动转为布尔值
目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型
Proxy.revocable()
Proxy.revocable():返回一个可取消的 Proxy 实例
js
let target = {}
let handler = {}
let { proxy, revoke } = Proxy.revocable(target, handler)
proxy.foo = 123
proxy.foo // 123
revoke()
proxy.foo // TypeError: Revoked使用场景:目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
this 问题
Proxy 代理针对目标对象的访问,不是目标对象的透明代理。
即不做任何拦截,也无法保证与目标对象行为一致。
主要原因:
Proxy 代理下,目标对象内部的this关键字会指向 Proxy 代理
js
const target = {
m: function () {
console.log(this === proxy)
}
}
const handler = {}
const proxy = new Proxy(target, handler)
target.m() // false
proxy.m() // true常见
- 由于 this 指向的变化,导致 Proxy 无法代理目标对象
js
const _name = new WeakMap()
class Person {
constructor(name) {
_name.set(this, name)
}
get name() {
return _name.get(this)
}
}
const jane = new Person('Jane')
jane.name // 'Jane'
const proxy = new Proxy(jane, {})
proxy.name // undefined- 有些原生对象的内部属性,只有通过正确的
this才能拿到
js
const target = new Date()
const handler = {}
const proxy = new Proxy(target, handler)
proxy.getDate()
// TypeError: this is not a Date object.解决:通过 this 绑定原始对象
js
const target = new Date('2015-01-01')
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target)
}
return Reflect.get(target, prop)
}
}
const proxy = new Proxy(target, handler)
proxy.getDate() // 1- Proxy 拦截函数内部的
this,指向的是handler对象
js
const handler = {
get: function (target, key, receiver) {
console.log(this === handler)
return 'Hello, ' + key
},
set: function (target, key, value) {
console.log(this === handler)
target[key] = value
return true
}
}
const proxy = new Proxy({}, handler)
proxy.foo
// true
// Hello, foo
proxy.foo = 1
// true实例:Web 服务的客户端
js
function createWebService(baseUrl) {
return new Proxy(
{},
{
get(target, propKey, receiver) {
return () => httpGet(baseUrl + '/' + propKey)
}
}
)
}js
const service = createWebService('http://example.com/data')
service.employees().then((json) => {
const employees = JSON.parse(json)
// ···
})