

ES6 中的 Proxy 的一些奇淫技巧 – 淮城一只猫
source link: https://iiong.com/es6-proxy-some-skills/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

老早之前就使用它处理一些业务,不过后来很少接触复杂的业务,几乎忘了它的存在,正好进来因为 Vue3
特性让我想起来这个对象,花点时间深入了解这个对象使用和一些方法的技巧。
Proxy
翻译中文叫 代理
,联想可以认为它可以在 JavaScript
给代理人进行一些操作。
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
在语法上,也没有很多复杂的东西:
const proxy = new Proxy(target, handler)
Target: 要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 处理器对象,作为代理配置,包涵 traps
(陷阱对象) 对象,也就是拦截操作的方法。常见的陷阱对象有:get
和 set
。
没有配置任何陷阱对象代理函数如下:
const target = {}
const proxy = new Proxy(target, {})
proxy.test = 100
console.log(target.test) // => 100
console.log(proxy.test) // => 100
for(const key in proxy) {
console.log(key) // => test
}
因为上面没有设置代理配置,也就是陷阱对象,所有的操作都转发给 target
:
- 写入操作
proxy.test
值设置给target
- 读取操作
proxy.test
从target
获取返回值 - 从
proxy
迭代获取的key
属性从target
获取返回值
所以从上面得知,没有任何陷阱对象,proxy
是一个透明包装的 target
对象。如果需要更多的功能需要添陷阱对象。在 JavaScript
对象中,对于对象的大部分操作,都有一个所谓的『内部方法』,例如:用于读取属性的 [[Get]]
的内部方法,用于设置属性的 [[Set]]
的内部方法。但这些方法仅在规范中使用,无法直接去调用它们。代理中的陷阱方法在规范调用方法如下:
[[Get]]
get
读取属性:handler.get()
[[Set]]
set
修改属性:handler.set()
[[HasProperty]]
has
in
运算符操作:handler.has()
[[Delete]]
deleteProperty
删除操作:handler.deleteProperty()
[[Call]]
apply
函数调用:handler.apply()
[[Construct]]
construct
实例化对象:handler.construct()
[[GetPrototypeOf]]
getPrototypeOf
Object.getPrototypeOf
[[SetPrototypeOf]]
setPrototypeOf
Object.setPrototypeOf
[[IsExtensible]]
isExtensible
Object.isExtensible
[[PreventExtensions]
preventExtensions
Object.preventExtensions
[[DefineOwnProperty]]
defineProperty
Object.defineProperty, Object.defineProperties
[[GetOwnProperty]]
getOwnPropertyDescriptor
Object.getOwnPropertyDescriptor
[[OwnPropertyKeys]]
ownKeys
Object.getOwnPropertyNames, Object.getOwnPropertySymbols
陷阱对象使用
这边讲述上面列出来各个使用方法,为了提高理解这边对部分陷阱对象含有 receiver
参数不进行详细讲解,就放在后面再讲解吧。
改属性和 handler.set()
一样是常见的陷阱对象,从上述文档得知语法如下:
get(target, property, receiver)
target
:目标对象,作为第一个 参数传递给新的Proxy
property
:属性名称receiver
:Proxy
或者继承Proxy
的对象
返回值:返回访问任何值
在平时开发项目中,要获取一个对象的属性,如果不存在的属性系统会返回一个 undefined
还需要额外判断等操作给予默认值:
const num1 = [1, 2, 3]
console.log(num1[2]) // => 3
console.log(num1[4]) // => undefined
但如果使用 Proxy
方法将上面进一步提高操作可行性:
let num2 = [1, 2, 3]
num2 = new Proxy(num2, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 1 // 如果没有找到集合存在的值,返回一个默认值
}
}
})
console.log(num2[2]) // => 3
console.log(num2[4]) // => 1
set(target, property, value, receiver)
target
:目标对象,作为第一个 参数传递给新的Proxy
property
:将被设置的属性名或Symbol
value
:新属性值receiver
:最初被调用的对象。通常是Proxy
本身,但handler
的set
方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是Proxy
本身)
返回值:返回 Boolean
值
这边使用场景的情景做个案例,比如表单验证,填入年龄是 number
类型,需要根据用户输入判断输入类型来决定是否进行下一步操作:
let num3 = []
num3 = new Proxy(num3, {
set(target, prop, value) {
if (typeof value === 'number') {
target[prop] = value
return true
} else {
return false
}
}
})
num3.push(20)
num3.push(100)
console.log(num3.length) // => 2
num3.push('100') // => 报错:TypeError: 'set' on proxy: trap returned falsish for property '2'
对于
set
它必须用有个true
的返回值,否则告知写入失败。
has(target, property)
target
:目标对象property
:需要检查是否存在的属性
返回值:Boolean
属性值
一般来说是在 in
运算符触发陷阱对象:
let range = {
start: 1,
end: 10
}
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end
}
})
console.log(7 in range) // => true
console.log(20 in range) // => false
apply
apply(target, thisArg, argumentsList)
target
:目标对象thisArg
:被调用时的上下文对象argumentsList
:被调用时的参数数组
返回值:返回任何值
使用 Proxy
实现简单的函数转发功能函数:
function delay(func, ms) {
return new Proxy(func, {
apply(target, thisArg, argumentsList) {
setTimeout(() => target.apply(thisArg, argumentsList), ms)
}
})
}
function sayHello(user) {
console.log(`Hello, ${user}!`)
}
sayHello = delay(sayHello, 5000)
console.log(sayHello.length) // => 1 // Proxy 转发这个操作
sayHello('Jaxson') // => 五秒后出现:Hello, Jaxson!
ownKeys && getOwnPropertyDescriptor
在 Object.keys
, for in
和大部分遍历对象属性的内部方法 [[OwnPropertyKeys]]
来获取属性列表,这些方法都有不同的细节:
Object.getOwnPropertyName(obj)
返回非Symbol
键值Object.getOwnPropertySymbols(obj)
返回Symbol
键值Object.keys/values()
返回可枚举标识的非Symbol
键值for in
遍历具有可枚举标识的非Symbol
键值以及原型链
在下面使用上述进行简单的例子,使用 ownKeys
陷阱对象让 for in
遍历一组用户对象,这个对象包括用户的信息,然后忽略特定的对象属性:
let user = {
name: 'Jaxson',
age: 25,
sex: '男',
_password: '****' // 密码选项不得公开展示
}
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
for (const key in user) console.log(key) // => name, age, sex
console.log(Object.keys(user)) // => ['name', 'age', 'sex']
console.log(Object.values(user)) // => ['Jaxson', 25, '男']
如果是对象不包含任何键值, Object.keys
不会列出该键值:
let user = {}
user = new Proxy(user, {
ownKeys(target) {
return ['user1', 'user2', 'user3']
}
})
console.log(Object.keys(user)) // => 返回空
因为 Object.keys
仅返回带有可枚举标识的属性,在遍历对象它为每个属性调用内部属性 [[GetOwnProperty]]
来获取它描述符,如果没有属性,其描述符为空则跳过。
描述符 是描述对象属性的属性 , 对象里目前存在的属性描述符有两种主要形式:数据描述符 和 存取描述符. 可以通过
Object.getOwnPropertyDescriptor()
函数来获取某个对象下指定属性的对应的 描述符 .
如果想要在上面例子返回一个属性,那么需要将它存在带有可枚举标识的对象里或者拦截对 [[GetOwnProperty]]
的调用,而这个陷阱对象就是 getOwnPropertyDescriptor
来处理,并返回一个描述符 enumerable: true
:
let user = {}
user = new Proxy(user, {
ownKeys(target) {
return ['user1', 'user2', 'user3']
},
getOwnPropertyDescriptor(target, prop) {
return {
enumerable: true,
configurable: true
}
}
})
console.log(Object.keys(user)) // => ['user1', 'user2', 'user3']
deleteProperty
继续拿上面的用户对象来作为例子,希望所有带 _
开头的属性对此进行保护:
get
访问属性时报错告知无法访问set
设置属性时报错告知无法设置deleteProperty
删除该属性时报错告知无法删除ownKeys
排除所有的_
开头属性操作
let user = {
name: 'Jaxson',
age: 25,
sex: '男',
_password: '****' // 密码选项不得公开展示并且不可修改
}
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) throw new Error('访问失败')
const value = target[prop]
return (typeof value === 'function') ? value.bind(target) : value
},
set(target, prop, value) {
if (prop.startsWith('_')) {
throw new Error('访问失败')
} else {
target[prop] = value
return true
}
},
deleteProperty(target, prop) {
if (prop.startsWith('_')) {
throw new Error('访问失败')
} else {
delete target[prop]
return true
}
},
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'))
}
})
user._password // => 报错:Uncaught Error: 访问失败
user._password = 'abc123456' // => 报错:Uncaught Error: 访问失败
delete user._password // => 报错:Uncaught Error: 访问失败
for (const key in user) console.log(key) // => name, age, sex
在上述有一段代码:
return (typeof value === 'function') ? value.bind(target) : value
如果对象包含一个检查这个属性的方法:
let user = {
name: 'Jaxson',
age: 25,
sex: '男',
_password: '****', // 密码选项不得公开展示并且不可修改
checkPassword(value) {
return value === this._password
}
}
所以在访问的时候会触发陷阱 get
对象返回无法访问的错误,所以这边只需要绑定原始对象就不会出现访问错误的错误。
Reflect
Reflect
是一个内置的对象,它提供拦截 JavaScript
操作的方法。这些方法与 proxy handlers
的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。
根据上述可以知道使用 Reflect
可以简化 Proxy
创建,当然也可以认为它是 Proxy
伴侣,之前说的 [[Get]]
、[[Set]]
内部方法仅用于规范,不能直接调用,但使用 Reflect
对象使之可能,因为它的方法是内部方法的最小包装:
obj[prop]
Reflect.get(obj, prop)
[[Get]]
obj[prop] = value
Reflect.set(obj, prop, value)
[[Set]]
delete obj[prop]
Reflect.deleteProperty(obj, prop)
[[Delete]]
new Obj(value)
Reflect.construct(Obj, value)
[[Construct]]
还有诸多操作方法,就不多举例了,使用它们的方法也很简单:
let user = {}
Reflect.set(user, 'name', 'Jaxson') // => true
console.log(user.name) // => Jaxson
在普通场景下,一般 Proxy
操作足够了:
const user = {
_name: 'Test1',
get name() {
return this._name
}
}
const userProxy = new Proxy(user, {
get(target, porp, receiver) {
return target[porp]
}
})
console.log(userProxy.name) // => Test1
在这边操作的时候建议配合 Reflect
对象更佳,在上面例子稍微复杂化就明白了:
const user = {
_name: 'Test1',
get name() {
return this._name
}
}
const userProxy = new Proxy(user, {
get(target, porp, receiver) {
return target[porp]
}
})
const userTest = {
__proto__: userProxy,
_name: 'Test2'
}
console.log(userTest.name) // => Test1
发现这边输出的内容和预想值不一样,为什么不是 Test2
,然后输出 get
的操作对象的返回值对象却是 user
而不是 userTest
:
- 在寻找
admin.name
的时候,admin
没有自己的属性,就转向原型链它的原型链 - 发现原型链是
userProxy
- 然后读取属性
name
,从get
陷阱对象获取返回对象是原始对象,从而读取到属性 - 对于
target[prop]
调用在this = target
上下文运行代码,所以结果是原始对象this._name
就是在user
对象里
所以解决这个方法就需要陷阱对象的第三个参数:receiver
,它可以正确把 this
传递给返回的对象。在普通对象里可以直接 call/apply
绑定上下文,但这边是不能被调用,而是被使用,所以就需要上面的 Reflect
的静态方法:
const user = {
_name: 'Test1',
get name() {
return this._name
}
}
const userProxy = new Proxy(user, {
get(target, porp, receiver) {
return Reflect.get(target, prop, receiver)
}
})
const userTest = {
__proto__: userProxy,
_name: 'Test2'
}
console.log(userTest.name) // => Test2
根据上面的方法进行实战,就选择最常见的表单验证的场景,例如:
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
<div class="form-group">
<label for="user">请输入用户名:</label>
<input type="text" class="form-control" id="user" name="userName">
</div>
<div class="form-group">
<label for="pwd">请输入密码:</label>
<input type="password" class="form-control" id="pwd" name="passWord">
</div>
<div class="form-group">
<label for="phone">请输入手机号码:</label>
<input type="tel" class="form-control" id="phone" name="phoneNumber">
</div>
<div class="form-group">
<label for="email">请输入邮箱:</label>
<input type="text" class="form-control" id="email" name="emailAddress">
</div>
<button type="button" class="btn btn-default">Submit</button>
</form>
</body>
针对上面进行不同的表单需要不同的验证,例如用户名长度,密码强度,手机号和邮箱的判断验证等等,如果采用最简单可以像这样:
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function() {
if (registerForm.userName.value === '') {
alert('用户名不能为空!')
return false
}
if (registerForm.userName.length < 6) {
alert('用户名长度不能少于6位!')
return false
}
if (registerForm.passWord.value === '') {
alert('密码不能为空!')
return false
}
if (registerForm.passWord.value.length < 6) {
alert('密码长度不能少于6位!')
return false
}
if (registerForm.phoneNumber.value === '') {
alert('手机号码不能为空!')
return false
}
if (!/^1(3|5|7|8|9)[0-9]{9}$/.test(registerForm.phoneNumber.value)) {
alert('手机号码格式不正确!')
return false
}
if (registerForm.emailAddress.value === '') {
alert('邮箱地址不能为空!')
return false
}
if (!/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(registerForm.emailAddress.value)) {
alert('邮箱地址格式不正确!')
return false
}
}, false)
如果随着表单随着维护增多以及复杂化,上面的各种 if-else
判断相信后期可读性非常差,所以这边需要提高代码可读性以及复用性。所以利用 Proxy
重构表单验证。
首先利用 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)
所以对比上面的 if-else
来比,不仅仅编码量少,并且表单对象和表单条件完全隔离,代码健壮性和复用性非常的强。
这边看起来比较简单,有时间再看稍微复杂的例子吧。
Recommend
-
95
一、CSS写自适应大小的正方形 代码: <style type="text/css"> 以图片为例 background 写法 .img{ width: 100%; height: 0; padding-bottom: 100%;
-
84
本片文章直接拷贝与部门wiki,作者还有renwanfeng、duyalei 写轮眼是谢益辉开发的,利用markdown语法完成的slides的 工具 , 排版非常考究,书写速度极快。非常适合工程师...
-
48
拖更很久,各位小哥哥、小姐姐别介意,今天本来会死在襁褓(草稿待了一个月)中的 不定期更新的CSS奇淫技巧(二)终于出来了,本文可能会水份居多,如有问题欢迎提议我会逐步榨干它 七、CSS 绝对底部 代码: 方案一:原理————正(padding)负(margin
-
49
ASP.NET Core 奇淫技巧之动态WebApi 接触到动态...
-
20
点击上方“ 涛哥聊Python ”,选择“星标”公众号 重磅干货,第一时间送达 来源: Python爬虫与数据挖掘 ...
-
21
Webpack5 上手 – 淮城一只猫 Webpack5 上手 Jaxson Wang / 2020-12-09 / 奇淫技巧 / 阅读量 之前很多项目都用到 Webpack
-
5
当 Vue3 遇上 TypeScript 和 TSX – 淮城一只猫 Vue3 体验 Jaxson Wang / 2020-09-17 / 编程技术 / 阅读量 3625...
-
11
NUC8 开箱体验报道 – 淮城一只猫 NUC8 开箱体验报道 Jaxson Wang / 2020-11-30 / 生活杂记 / 阅读量 148...
-
14
写了一个插件:Vue-Right-Click – 淮城一只猫 写了一个插件:Vue-Right-Click Jaxson Wang / 2020-06-09 / Vue.js / 阅读量
-
27
使用 OpenCore 引导黑苹果 – 淮城一只猫 使用 OpenCore 引导黑苹果 Jaxson Wang / 2020-04-12 / 生活杂记 / 阅读量
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK