原文链接: www.w3cplus.com
前端时间学习Vue的时候,碰到Proxy
,当时就一脸蒙逼了。所以返过头来补一下相关的知识。在JavaScript中有Proxy
和Reflect
的两个概念。最近几天一直在学习这两个概念,今天整整这方面的相关知识点。
术语介绍
Proxy
又称为代理。在现实生活中,大家对代理二字并不会太陌生,比如某产品的代理。打个比方来说,我们要买一台手机,我们不会直接到一家手机厂去买,会在手机的代理商中买。
在JavaScript中,Proxy
用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程,即对编程语言进行编程。
Proxy
可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy
这个词的原意是代理,用在这里表示由它来代理某些操作,可以译为代理器。
Reflect
称为反射。它也是ES6中为了操作对象而提供的新的API,用来替代直接调用Object
的方法。Reflect
是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。
Reflect
与大多数全局对象不同,Reflect
没有构造函数。你不能将其与一个new
运算符一起使用,或者将Reflect
对象作为一个函数来调用。
简单的看一个小示例:
let target = {} let handler = { get: function (target, key, receiver) { console.log(`Getting ${key}!`) return Reflect.get(target, key, receiver) }, set: function (target, key, value, receiver) { console.log(`Setting ${key}!`) return Reflect.set(target, key, value, receiver) } } let proxy = new Proxy(target, handler) console.log(proxy.name) proxy.name = '大漠' console.log(proxy.name) proxy.count = 1 proxy.count++ console.log(proxy.count)
运行的结果如下:
上面代码对一个空对象架设了一层拦截,重新定义了属性的读取(get
)和设置(set
)行为。这里暂时先不解释具体的语法,只看运行结果。
在上面的示例中,我们看到了target
、handler
和trap
。简单对介绍一下:
target
代表了被代理的对象。这是你需要控制对其访问的对象。它始终作为Proxy
构造器的第一个参数被传入,同时它也会被传入每个trap
。handler
是一个包含了你想要拦截和处理的操作的对象。它会被作为Proxy
构造器的第二个参数传入。它实现了Proxy API(比如:get
,set
,apply
等等)。- 一个
trap
代表了handler
中一个被处理的函数。因此,如果要拦截get
请求你需要创建一个get
的trap
。以此类推。
基本语法
前面我们了解了相关的术语,我们来看看他们的基本语法。首先来看看Proxy
的基本语法格式:
// @param {Object} target 用来被代理的对象 // @param {Object} handler 用来设置代理的对象 let proxy = new Proxy(target, handler)
ES6原生提供Proxy
构造函数,用来生成Proxy
实例。在使用过程当中,Proxy
都像上面这种形式,不同的只是handler
参数的写法。其中,new Proxy()
表示生成一个Proxy
实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
let proxy = new Proxy({}, { get: function(target, property) { return property in target ? target[property] : `Error: ${target} object not has ${property} property! ` } }) console.log(proxy.time) console.log(proxy.name) console.log(proxy.title) proxy.time = new Date() proxy.name = '大漠' proxy.title = '切图仔' console.log(proxy.time) console.log(proxy.name) console.log(proxy.title)
上面代码中,作来构造函数,Proxy
接受两个参数。第一个参数是所要代理的目标对象(一个空对象{}
),即如果没有Proxy
的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代码的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面的代码中,配置对象有一个get
方法,用来拦截对目标对象属性的访问请求。get
方法的两个参数分别是target
(目标对象)和property
(所要访问的属性)。
注意,要使得
Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
Reflect
只是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同(稍后会介绍)。Reflect
不是一个函数对象,因此它是不可构造的。
new Reflect() // 错误的写法
Reflect
提供了一些静态方法,静态方法是指只能通过对象自身访问的方法:
处理器对象
处理器对象handler
一共提供了14
种可代理操作,每种操作的代号(属性名/方法名)和触发这种操作的方式如下:
handler.getPrototypeOf()
:在读取代理对象的原型时触发该操作,比如在执行Object.getPrototypeOf(proxy)
时handler.setPrototypeOf()
:在设置代理对象的原型时触发该操作,比如在执行Object.setprototypeOf(proxy, null)
时handler.isExtensible()
:在判断一个代理对象是否是可扩展时触发该操作,比如在执行Object.isExtensible(proxy)
时handler.preventExtensions()
:在让一个代理对象不可扩展时触发该操作,比如在执行Object.preventExtensions(proxy)
时handler.getOwnPropertyDescriptor()
:在获取代理对象某个属性的属性描述时触发该操作,比如在执行Object.getOwnPropertyDescriptor(proxy, 'foo')
时handler.defineProperty()
:在定义代理对象某个属性时的属性描述时触发该操作,比如在执行Object.defineProperty(proxy,'foo',{})
时handler.has()
:在判断代理对象是否拥有某个属性时触发该操作,比如在执行'foo' in proxy
时handler.get()
:在读取代理对象的某个属性时触发该操作,比如在执行proxy.foo
时handler.set()
:在给代理对象的某个赋值时触发该操作,比如在执行proxy.foo = 1
时handler.deleteProperty()
:在删除代理对象的某个属性时触发该操作,比如在执行delete proxy.foo
时handler.ownKeys()
:在获取代理对象的所有属性键时触发该操作,比如在执行Object.getOwnPropertyNames(proxy)
时handler.apply()
:在调用一个目标对象为函数的代理对象时触发该操作,比如在执行proxy()
时handler.construct()
:在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy()
时
Reflect
对象拥有对应的可以控制各种元编程任务的静态方法。这些功能和Proxy
一一对应。
下面的这些名称你可能看起来很眼熟(因为他们也是Object
上的方法):
Reflect.getOwnPropertyDescriptor(..)
Reflect.defineProperty(..)
Reflect.getPrototypeOf(..)
Reflect.setPrototypeOf(..)
Reflect.preventExtensions(..)
Reflect.isExtensible(..)
这些方法和在Object
上的同名方法一样。然后,一个区别在于,Object
上这么方法的第一个参数是一个对象,Reflect
遇到这种情况会扔出一个错误。
如果handler
没有设置任何拦截,那就等同于直接通向原对象。
let target = {} let handler = {} let proxy = new Proxy(target, handler) proxy.name = '大漠' console.log(`Proxy: ${proxy.name}`) // => Proxy: 大漠 console.log(`Target: ${target.name}`) // => Target: 大漠
上面代码中,handler
是一个空对象,没有任何拦截效果,访问handler
就等同于访问target
。
Proxy
支持的拦截操作
handler.get()
get(target, propKey, receiver)
:拦截对象属性的读取,比如proxy.foo
和proxy['foo']
、Reflect.get(...)
,返回类型不限。最后一个参数receiver
可选,当target
对象设置了propKey
属性的get
函数时,receiver
对象会绑定get
函数的this
对象
let person = { name: '大漠' } let handler = { get: function (target, propKey, receiver) { if (propKey in target) { return target[propKey] } else { throw new ReferenceError(`Property ${propKey} does not exist!`) } } } let proxy = new Proxy(person, handler) console.log(proxy.name) console.log(proxy.age)
上面的代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined
。
get()
方法是可以继承的。
let proxy = new Proxy({},{ get(target, propKey, receiver) { console.log(`GET: ${propKey}`) return target[propKey] } }) let obj = Object.create(proxy) console.log(obj.name) 复制代码
上面代码中,拦截操作定义在Prototype
对象上面,所以如果读取obj
对象继承的属性时,拦截会生效。
下面的例子,使用get
拦截,实现数组读取负数的索引。
function createArray(...items) { let handler = { get(target, propKey, receiver) { let index = parseInt(propKey) if (index < 0) { propKey = String(target.length + index) } return Reflect.get(target, propKey, receiver) } } let target = [] target.push(...items) return new Proxy(target, handler) } let array = createArray('大漠', 'w3cplus.com', '切图仔') console.log(array[-1]) // => 切图仔
上面代码中,数组的位置参数是-1
,就会输出数组的倒数第一个item
值,也就是切图仔
。
handler.set()
set(target, propKey, value, receiver)
用来拦截对象属性的设置,比如proxy.foo = v
或proxy['foo'] = v
、Reflect.set(...)
,返回一个布尔值。
假定Person
对象有一个age
属性,该属性应该是一个不大于100
的整数,那么可以使用Proxy
对象保证age
的属性值符合要求。
let validator = { set: function (target, propKey, value, receiver) { if (propKey === 'age') { if (!Number.isInteger(value)) { throw new TypeError('age不是一个整数') } if (value > 100) { throw new RangeError('age不是有效值') } } // 对于age以外的属性,直接保存 target[propKey] = value } } let Person = new Proxy({}, validator) Person.age = 36 Person.name = '大漠' console.log(Person.age) // => 36 console.log(Person.name) // => 大漠 Person.age = 200 console.log(Person.age) // => age不是有效值
上面的示例中,由于设置了存值函数set()
,任何不符合要求的age
属性赋值,都会抛出一个错误。利用set()
方法,还可以数据绑定,即每当对象发生变化时,会自动更新。在使用JavaScript对表单进行验证时,非常有用。
利用proxy
拦截不符合要求的数据
function validator(target, validator, errorMsg) { return new Proxy(target, { _validator: validator, set(target, key, value, proxy) { let errMsg = errorMsg if (value == '') { alert(`${errMsg[key]}不能为空!`) return target[key] = false } let va = this._validator[key] if (!!va(value)) { return Reflect.set(target, key, value, proxy) } else { alert(`${errMsg[key]}格式不正确`) return target[key] = false } } }) }
负责校验的逻辑代码
const validators = { name(value) { return value.length > 6 }, passwd(value) { return value.length > 6 }, moblie(value) { return /^1(3|5|7|8|9)[0-9]{9}$/.test(value) }, email(value) { return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value) } }
客户端调用代码
const errorMsg = { name: '用户名', passwd: '密码', moblie: '手机号码', email: '邮箱地址' } const vali = validator({}, validators, errorMsg) let registerForm = document.querySelector('#registerForm') registerForm.addEventListener( 'submit', function() { let validatorNext = function*() { yield vali.name = registerForm.userName.value yield vali.passwd = registerForm.passWord.value yield vali.moblie = registerForm.phoneNumber.value yield vali.email = registerForm.emailAddress.value } let validator = validatorNext() validator.next(); !vali.name || validator.next(); //上一步的校验通过才执行下一步 !vali.passwd || validator.next(); !vali.moblie || validator.next(); }, false)
上面这部分验证表单的代码来自于@jawil的《探索两种优雅的表单验证》
handler.has()
has(target, propKey)
用来拦截propKey in proxy
、Reflect.has(...)
的操作,返回一个布尔值。
has()
方法可以隐藏某些属性,不被in
操作符发现。
let handler = { has: function(target, key) { if (key[0] === '_') { return false } return key in target } } let target = { _prop: '大漠', prop: 'W3cplus' } let proxy = new Proxy(target, handler) console.log('_prop' in proxy) // => false console.log('prop' in proxy) // => true
如果原对象的属性名的第一个字符是下划线,proxy.has
就会返回false
,从而不会被in
运算符发现。如果原对象不可配置或者禁止扩展,这个时候,has()
拦截就会报错。
let target = { age: 30, name: '大漠' } Object.preventExtensions(target) let handler = { has: function (target, propKey) { return false } } let proxy = new Proxy(target, handler) console.log(proxy.age) console.log(proxy.name) console.log('age' in proxy)
上例中,target
对象禁止扩展,结果使用has()
拦截就会报错。
handler.construct()
construct(target, args, proxy)
用来拦截Proxy
实例作为构造函数调用的操作,比如new proxy(...args)
和Reflect.construct(...)
。
construct()
方法用来拦截new
命令。
let handler = { construct: function (target, args) { console.log('Called:' + args.join(',')) return {value: args[0] * 10} } } let target = function (){} let proxy = new Proxy(target, handler) console.log(new proxy(2).value)
如果construct()
方法返回的不是对象,就会抛出错误。
let handler = { construct: function (target, args) { return 'w3cplus' } } let target = function () {} let proxy = new Proxy(target, handler) console.log(new proxy())
handler.deleteProperty()
deleteProperty(target, propKey)
用于拦截delete proxy[propKey]
和Reflect.deleteProperty(...)
的操作,返回一个布尔值。如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
let target = { _prop: '大漠', prop: 'w3cplus' } let handler = { deleteProperty: function (target, propKey) { invariant(propKey, 'delete') return true } } function invariant(key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private '${key}' property!`) } } let proxy = new Proxy(target, handler) console.log(delete proxy.prop) console.log(delete proxy._prop)
上面示例中,deleteProperty()
方法拦截了delete
操作符,删除第一个字符为下划线的属性会报错。