JavaScript - 代理与反射(代理模式 + 小结)
文章目录
- 一、跟踪属性访问
- 二、隐藏属性
- 三、属性验证
- 四、函数与构造函数参数验证
- 五、数据绑定与可观察对象
- 六、小结
使用代理可以在代码中实现一些有用的编程模式。
一、跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:
const user = {
name: 'Jake'
}
const proxy = new Proxy(user, {
get (target, property, receiver) {
console.log(`Getting ${property}`)
return Reflect.get(...arguments)
},
set (target, property, value, receiver) {
console.log(`Setting ${property}=${value}`)
return Reflect.set(...arguments)
}
})
// Getting name
// Jake
console.log(proxy.name)
// Setting age=27
// 27
console.log((proxy.age = 27))
二、隐藏属性
代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。比如:
const hiddenProperties = ['foo', 'bar']
const targetObject = {
foo: 1,
bar: 2,
baz: 3
}
const proxy = new Proxy(targetObject, {
get (target, property) {
if (hiddenProperties.includes(property)) {
return undefined
} else {
return Reflect.get(...arguments)
}
},
has (target, property) {
if (hiddenProperties.includes(property)) {
return false
} else {
return Reflect.has(...arguments)
}
}
})
// get()
console.log(proxy.foo) // undefined
console.log(proxy.bar) // undefined
console.log(proxy.baz) // 3
// has()
console.log('foo' in proxy) // false
console.log('bar' in proxy) // false
console.log('baz' in proxy) // true
三、属性验证
因为所有赋值操作都会触发 set() 捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:
const target = {
onlyNumbersGoHere: 0
}
const proxy = new Proxy(target, {
set (target, property, value) {
if (typeof value !== 'number') {
return false
} else {
return Reflect.set(...arguments)
}
}
})
proxy.onlyNumbersGoHere = 1
console.log(proxy.onlyNumbersGoHere) // 1
proxy.onlyNumbersGoHere = '2'
console.log(proxy.onlyNumbersGoHere) // 1
四、函数与构造函数参数验证
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值:
function median (...nums) {
return nums.sort()[Math.floor(nums.length / 2)]
}
const proxy = new Proxy(median, {
apply (target, thisArg, argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== 'number') {
throw 'Non-number argument provided'
}
}
return Reflect.apply(...arguments)
}
})
console.log(proxy(4, 7, 1)) // 4
console.log(proxy(4, '7', 1))
// Error: Non-number argument provided
类似地,可以要求实例化时必须给构造函数传参:
class User {
constructor (id) {
this.id_ = id
}
}
const proxy = new Proxy(User, {
construct (target, argumentsList, newTarget) {
if (argumentsList[0] === undefined) {
throw 'User cannot be instantiated without id'
} else {
return Reflect.construct(...arguments)
}
}
})
new proxy(1)
new proxy()
// Error: User cannot be instantiated without id
五、数据绑定与可观察对象
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。
比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
const userList = []
class User {
constructor (name) {
this.name_ = name
}
}
const proxy = new Proxy(User, {
construct () {
const newUser = Reflect.construct(...arguments)
userList.push(newUser)
return newUser
}
})
new proxy('John')
new proxy('Jacob')
new proxy('Jingleheimerschmidt')
console.log(userList) // [User {}, User {}, User{}]
另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:
const userList = []
function emit (newValue) {
console.log(newValue)
}
const proxy = new Proxy(userList, {
set (target, property, value, receiver) {
const result = Reflect.set(...arguments)
if (result) {
emit(Reflect.get(target, property, receiver))
}
return result
}
})
proxy.push('John')
// John
proxy.push('Jacob')
// Jacob
六、小结
- 代理是 ECMAScript 6 新增的令人兴奋和动态十足的新特性。尽管不支持向后兼容,但它开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地
- 从宏观上看,代理是真实 JavaScript 对象的透明抽象层。代理可以定义包含捕获器的处理程序对象,而这些捕获器可以拦截绝大部分 JavaScript 的基本操作和方法。在这个捕获器处理程序中,可以修改任何基本操作的行为,当然前提是遵从捕获器不变式
- 与代理如影随形的反射 API,则封装了一整套与捕获器拦截的操作相对应的方法。可以把反射 API 看作一套基本操作,这些操作是绝大部分 JavaScript 对象 API 的基础
- 代理的应用场景是不可限量的。开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象