10

ES6 中的 Proxy 的一些奇淫技巧 – 淮城一只猫

 3 years ago
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.
ES6 中的 Proxy 的一些奇淫技巧 – 淮城一只猫
ES6 中的 Proxy 的一些奇淫技巧
Jaxson Wang / 2020-04-15 / 编程技术 / 阅读量 484

老早之前就使用它处理一些业务,不过后来很少接触复杂的业务,几乎忘了它的存在,正好进来因为 Vue3 特性让我想起来这个对象,花点时间深入了解这个对象使用和一些方法的技巧。

Proxy 翻译中文叫 代理 ,联想可以认为它可以在 JavaScript 给代理人进行一些操作。

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

在语法上,也没有很多复杂的东西:

Loading...
载入代码中...
const proxy = new Proxy(target, handler)
javascript

Target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler: 处理器对象,作为代理配置,包涵 traps(陷阱对象) 对象,也就是拦截操作的方法。常见的陷阱对象有:getset

没有配置任何陷阱对象代理函数如下:

Loading...
载入代码中...
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 }
javascript

因为上面没有设置代理配置,也就是陷阱对象,所有的操作都转发给 target

  • 写入操作 proxy.test 值设置给 target
  • 读取操作 proxy.testtarget 获取返回值
  • 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() 一样是常见的陷阱对象,从上述文档得知语法如下:

Loading...
载入代码中...
get(target, property, receiver)
javascript
  • target:目标对象,作为第一个 参数传递给新的 Proxy
  • property:属性名称
  • receiverProxy 或者继承 Proxy 的对象

返回值:返回访问任何值

在平时开发项目中,要获取一个对象的属性,如果不存在的属性系统会返回一个 undefined 还需要额外判断等操作给予默认值:

Loading...
载入代码中...
const num1 = [1, 2, 3] console.log(num1[2]) // => 3 console.log(num1[4]) // => undefined
javascript

但如果使用 Proxy 方法将上面进一步提高操作可行性:

Loading...
载入代码中...
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
javascript
Loading...
载入代码中...
set(target, property, value, receiver)
javasc
  • target:目标对象,作为第一个 参数传递给新的 Proxy
  • property:将被设置的属性名或 Symbol
  • value:新属性值
  • receiver:最初被调用的对象。通常是 Proxy 本身,但 handlerset 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 Proxy 本身)

返回值:返回 Boolean

这边使用场景的情景做个案例,比如表单验证,填入年龄是 number 类型,需要根据用户输入判断输入类型来决定是否进行下一步操作:

Loading...
载入代码中...
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'
javascript

对于 set 它必须用有个 true 的返回值,否则告知写入失败。

Loading...
载入代码中...
has(target, property)
javascript
  • target:目标对象
  • property:需要检查是否存在的属性

返回值:Boolean 属性值

一般来说是在 in 运算符触发陷阱对象:

Loading...
载入代码中...
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
javascript

apply

Loading...
载入代码中...
apply(target, thisArg, argumentsList)
javascript
  • target:目标对象
  • thisArg:被调用时的上下文对象
  • argumentsList:被调用时的参数数组

返回值:返回任何值

使用 Proxy 实现简单的函数转发功能函数:

Loading...
载入代码中...
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!
javascript

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 遍历一组用户对象,这个对象包括用户的信息,然后忽略特定的对象属性:

Loading...
载入代码中...
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, '男']
javascript

如果是对象不包含任何键值, Object.keys 不会列出该键值:

Loading...
载入代码中...
let user = {} user = new Proxy(user, { ownKeys(target) { return ['user1', 'user2', 'user3'] } }) console.log(Object.keys(user)) // => 返回空
javascript

因为 Object.keys 仅返回带有可枚举标识的属性,在遍历对象它为每个属性调用内部属性 [[GetOwnProperty]] 来获取它描述符,如果没有属性,其描述符为空则跳过。

描述符 是描述对象属性的属性 , 对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符. 可以通过 Object.getOwnPropertyDescriptor() 函数来获取某个对象下指定属性的对应的 描述符 .

如果想要在上面例子返回一个属性,那么需要将它存在带有可枚举标识的对象里或者拦截对 [[GetOwnProperty]] 的调用,而这个陷阱对象就是 getOwnPropertyDescriptor 来处理,并返回一个描述符 enumerable: true

Loading...
载入代码中...
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']
javascript

deleteProperty

继续拿上面的用户对象来作为例子,希望所有带 _ 开头的属性对此进行保护:

  • get 访问属性时报错告知无法访问
  • set 设置属性时报错告知无法设置
  • deleteProperty 删除该属性时报错告知无法删除
  • ownKeys 排除所有的 _ 开头属性操作
Loading...
载入代码中...
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
javascript

在上述有一段代码:

Loading...
载入代码中...
return (typeof value === 'function') ? value.bind(target) : value
javascript

如果对象包含一个检查这个属性的方法:

Loading...
载入代码中...
let user = { name: 'Jaxson', age: 25, sex: '男', _password: '****', // 密码选项不得公开展示并且不可修改 checkPassword(value) { return value === this._password } }
javascript

所以在访问的时候会触发陷阱 get 对象返回无法访问的错误,所以这边只需要绑定原始对象就不会出现访问错误的错误。

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

根据上述可以知道使用 Reflect 可以简化 Proxy 创建,当然也可以认为它是 Proxy 伴侣,之前说的 [[Get]][[Set]] 内部方法仅用于规范,不能直接调用,但使用 Reflect 对象使之可能,因为它的方法是内部方法的最小包装:

操作 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]]

还有诸多操作方法,就不多举例了,使用它们的方法也很简单:

Loading...
载入代码中...
let user = {} Reflect.set(user, 'name', 'Jaxson') // => true console.log(user.name) // => Jaxson
javascript

在普通场景下,一般 Proxy 操作足够了:

Loading...
载入代码中...
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
javascript

在这边操作的时候建议配合 Reflect 对象更佳,在上面例子稍微复杂化就明白了:

Loading...
载入代码中...
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
javascript

发现这边输出的内容和预想值不一样,为什么不是 Test2 ,然后输出 get 的操作对象的返回值对象却是 user 而不是 userTest

  • 在寻找 admin.name 的时候, admin 没有自己的属性,就转向原型链它的原型链
  • 发现原型链是 userProxy
  • 然后读取属性 name ,从 get 陷阱对象获取返回对象是原始对象,从而读取到属性
  • 对于 target[prop] 调用在 this = target 上下文运行代码,所以结果是原始对象 this._name 就是在 user 对象里

所以解决这个方法就需要陷阱对象的第三个参数:receiver ,它可以正确把 this 传递给返回的对象。在普通对象里可以直接 call/apply 绑定上下文,但这边是不能被调用,而是被使用,所以就需要上面的 Reflect 的静态方法:

Loading...
载入代码中...
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
javascript

根据上面的方法进行实战,就选择最常见的表单验证的场景,例如:

Loading...
载入代码中...
<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>

针对上面进行不同的表单需要不同的验证,例如用户名长度,密码强度,手机号和邮箱的判断验证等等,如果采用最简单可以像这样:

Loading...
载入代码中...
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)
javascript

如果随着表单随着维护增多以及复杂化,上面的各种 if-else 判断相信后期可读性非常差,所以这边需要提高代码可读性以及复用性。所以利用 Proxy 重构表单验证。

首先利用 Proxy 拦截一些不符合要求的数据:

Loading...
载入代码中...
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 } } }) }
javascript

负责校验的逻辑代码:

Loading...
载入代码中...
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) } }
javascript

整合俩者:

Loading...
载入代码中...
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)
javascript

所以对比上面的 if-else 来比,不仅仅编码量少,并且表单对象和表单条件完全隔离,代码健壮性和复用性非常的强。

这边看起来比较简单,有时间再看稍微复杂的例子吧。

Specification Proxy

MDN Proxy 文档

MDN JSPropertyDesciptor 参考文档

MDN in 运算符文档

MDN Reflect 文档

MDN 元编程文档


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK