Libx

Proxy & Rreflect

Word count: 1,947Reading time: 8 min
2018/09/15 Share

Proxy:更好的对象处理

Proxy概述

Proxy 用来修改某些操作的默认行为,等同于在语言层面进行修改,属于一种元编程的:对编程语言进行编程。

顾名思义,Proxy的原意即是代理,Proxy 可以理解为在目标对象前架设一个拦截层,外界对该对象的访问都经过这层拦截,因此提供了一种机制来对外界的访问进行过滤和改写。
先来看一个示例:

let obj = new Proxy({},{
get:function(target,key,receiver){
console.log(`getting ${key}`)
return Reflect.get(target,key,receiver)
},
set:function(target,key,receiver){
console.log(`setting ${key}`)
return Reflect.set(target,key,value,receiver)
}
})
// 以上代码对一个空对象进行了一层拦截,重新定义了属性的get,set方法,

Proxy实际上重载了点运算符,用自己的定义覆盖了语言的原始定义。

基本语法与API

原生Javascript 对象所提供的Proxy的原生构造函数let p = new Proxy(target, handler);其中有几个概念,traps表示提供属性访问的方法,handler为包含traps的占位符对象(即属性是当执行一个操作时定义代理的行为的函数),target 用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
API

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() 时。

Proxy能带来的

因为在使用了Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。

  1. 拦截和监视外部对对象的访问
  2. 降低函数或类的复杂度
  3. 在复杂操作前对操作进行校验或对所需资源进行管理
    下面来看一下使用Proxy能给我们带来什么改变

    原生JS的枚举:

    直接使用JS对象来代替枚举类型通常会不安全,我们所希望的枚举类型通常需要包含至少以下的特点:
  • 如果不存在的话,报错。
  • 不允许动态设置,否则报错。
  • 不允许删除,否则报错。
    当然TS的出现为我们解决了这个问题,我们这里单纯使用Proxy来编写一下简单的类型检测(当然使用原本的Object.definePerpoty也可实现),
    export default function ENUM(object) {
    return new Proxy(object, {
    get(target, prop) {
    if (target[prop]) {
    return Reflect.get(target, prop)
    } else {
    throw new ReferenceError(`Unknown enum '${prop}'`)
    }
    },
    set() {
    throw new TypeError('Enum is readonly')
    },
    deleteProperty() {
    throw new TypeError('Enum is readonly')
    }
    })
    }

使用Peoxy包装Fetch

Fetch是一个非常常用的原生API了,我们可以使用Proxy来简单的包装一下,使其变得更加的易用

const handlers = {
get (target, property) {
if (!target.init) {
['GET', 'POST'].forEach(method => {
target[method] = (url, params = {}) => {
return fetch(url, {
headers: {
'content-type': 'application/json'
},
mode: 'cors',
credentials: 'same-origin',
method,
...params
}).then(res => res.json())
}
})
}
return target[property]
}
}
let FetchX = new Proxy({}, handlers)
await FetchX.GET('XXX')
await FetchX.POST('XXX', {
body: JSON.stringify({...})
})

断言

const assert = new Proxy({},{
set(target,message,value){
if(!value) console.error(message)
}
})
assert['I am wuyanzu'] = false // Error: I am not wuyanzu

函数的链式调用

let pipe = (function () {
return function (value) {
let funcStack = [];
let oproxy = new Proxy({} , {
get(target,fnName) {
if (fnName === 'get') {
return funcStack.reduce((val, fn) => fn(val) ,value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
let double = n => n * 2;
let pow = n => n * n;
let reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63

一个简单的数据响应

<main>
<p>请输入:</p>
<input type="text" id="input">
<p id="p"></p>
</main>
const input = document.getElementById('input');
const p = document.getElementById('p');
const newObj = new Proxy({}, {
get(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key === 'text') {
input.value = value;
p.innerHTML = value;
}
return Reflect.set(target, key, value, receiver);
},
});
input.addEventListener('keyup', (e)=> {
newObj.text = e.target.value;
});

相比Object.definePeproty只能代理一个属性,Proxy可以直接代理对象,这也带来了极大的方便,比如Vue由于使用的是Object.definePeproty来实现的数据劫持,所以对数组的监听,又额外进行了一些处理。之前尤雨溪发了一条微博:
这两天验证了一些 Vue 3.0 的设计,证明一些思路是可行的... Proxy 是个好东西 [doge] ​​​​
相信会有更多的惊喜的吧。

Reflect:Proxy的另一面

首先我们要了解一下,为什么会新添加这么一个全局对象?如果你看过Reflect的一些函数,你就会发现,这个对象上的方法基本上都可以从Object上面找到,找不到的那些,也是可以通过对对象命令式的操作去实现的;那么为什么还要新添加一个呢?

  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  3. 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

API

Reflect对象一共有 13 个静态方法。

Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。
这里关于Reflect有一篇很好的文章:ES6 中的元编程:第二部分 —— 反射(Reflect)

CATALOG
  1. 1. Proxy:更好的对象处理
    1. 1.1. Proxy概述
    2. 1.2. 基本语法与API
    3. 1.3. Proxy能带来的
      1. 1.3.1. 原生JS的枚举:
    4. 1.4. 使用Peoxy包装Fetch
    5. 1.5. 断言
    6. 1.6. 函数的链式调用
    7. 1.7. 一个简单的数据响应
  2. 2. Reflect:Proxy的另一面
    1. 2.1. API