[原文链接]http://feclub.cn/post/content/proxy_reflect
Proxy & Reflect
Reflect
先讲 Reflect 是因为 Proxy 要配合 Reflect 使用的
- Reflect 兼容性
- Reflect 是什么
- Reflect 对象的设计目的(为什么)
- Reflect 静态方法(怎么用)
- Reflect 操作对象与老方法的对比优势(优势对比)
- Reflect 操作对象能力扩展举个例子:(能力扩充举例)
Reflect 兼容性
caniuse 上居然没有 Reflect
的兼容性
MDN 上有有个兼容性表格,IE全跪了,enumerate 属性也全都不可用
1.Reflect 是什么
MDN: Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
阮一峰: Reflect对象是 ES6 为了操作对象而提供的新 API。
关键词: 对象, 操作对象
2.Reflect 对象的设计目的(为什么)
- (1)将
Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上(Reflect.defineProperty
)。未来新的方法将只部署在Reflect
上 - (2)修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty
在无法定义属性时,会报错
,而Reflect.defineProperty
则会返回false
。
Object.defineProperty([], 'length', { get() {return 10} })
//TypeError: Cannot redefine property: length
Reflect.defineProperty([], 'length', { get() {return 10} })
//false
兼容报错
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
- (3)让
Object
的命令式操作都变成函数行为。
'assign' in Object // true 命令式操作
Reflect.has(Object, 'assign') // true 函数行为
const obj = { name: 'cg', age: 18 }
delete obj.name //命令式操作,删除 name 属性
Reflect.deleteProperty(obj, 'age') //函数式操作,删除age属性
- (4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
举例:
const target = {}
const proxy = new Proxy(target, {
set(target, name, value, receiver) {
return Reflect.set(target, name, value, receiver)
}
})
// 因为 Proxy 的 set 方法有4个参数 (target, name, value, receiver),
// 而 Reflect 的 set 方法也有这4个参数 (target, name, value, receiver)
// 它们一一对应,所以能很完美地用 Reflect 操作 Proxy 对象
Reflect 静态方法(怎么用)
Reflect
对象一共有 13 个静态方法。
Reflect.get(target, name, receiver)
//target[name]
Reflect.set(target, name, value, receiver)
//target[name] = value
Reflect.apply(func, thisArg, args)
//Function.prototype.apply.call(func, thisArg, args)
Reflect.construct(target, args)
//new target(...args)
Reflect.defineProperty(target, name, desc)
//Object.defineProperty(target, name)
Reflect.deleteProperty(target, name)
//delete target[name]
Reflect.has(target, name)
//name in target
Reflect.ownKeys(target)
//Object.getOwnPropertyNames(target) + Object.getOwnPropertySymbols(target)
Reflect.isExtensible(target)
//Object.isExtensible 是否可扩展
Reflect.preventExtensions(target)
//Object.preventExtensions(target) 阻止扩展
Reflect.getOwnPropertyDescriptor(target, name)
//Object.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
//Object.getPrototypeOf(target) 读取 __proto__
Reflect.setPrototypeOf(target, prototype)
//Object.setPrototypeOf(target, prototype) 设置 __proto__
对于处理非法参数的错误机制,Reflect新方法和Object老方法的区别
1.defineProperty
无法定义属性时,Reflect
返回false
,Object
报错
Object.defineProperty([], 'length', { get() {return 10} })
//TypeError: Cannot redefine property: length
Reflect.defineProperty([], 'length', { get() {return 10} }) //false
2.getOwnPropertyDescriptor
如果第一个参数不是对象,Object
返回undefined
,Reflect
报错
Object.getOwnPropertyDescriptor(1, 'foo') // undefined
Reflect.getOwnPropertyDescriptor(1, 'foo')
// TypeError: Reflect.getOwnPropertyDescriptor called on non-object
3.isExtensible
(对象是否可扩展)如果参数不是对象,Object
返回false
,Reflect
报错
Object.isExtensible(1) // false
Reflect.isExtensible(1)
// TypeError: Reflect.isExtensible called on non-object
4.preventExtensions
(阻止对象扩展),如果参数不是对象,Object
在ES5环境报错,在ES6环境返回原参数,Reflect
则报错
// ES5 环境
Object.preventExtensions(1) // 报错
// ES6 环境
Object.preventExtensions(1) // 1
// 新写法
Reflect.preventExtensions(1) // 报错
Reflect操作对象与老方法的对比优势(优势对比)
Reflect
更加符合面向对象,操作对象的方法全部都挂在Reflect
Reflect操作对象 | 老方法操作对象 | |
---|---|---|
面向对象 | 全部挂在Reflect 对象上,更加符合面向对象 |
各种指令方法,= in delete |
函数式 | 所有方法都是函数 | 命令式、赋值、函数混用 |
规范报错 | defineProperty 无效返false ,后面几个方法参数非法报错 |
defineProperty 无效报错,后面几个方法参数非法不报错 |
方法扩展 | 参数receiver 指定this 指向 |
不能 |
Reflect 操作对象能力扩展举个例子:(能力扩充举例)
方法get, set中receiver参数指定this,获取、设置反射属性
//get
const Ironman = {
firstName: 'Tony',
lastName: 'Stark',
get fullName() {return `${this.firstName} ${this.lastName}`}
}
//获取自身属性,新老方法都可以实现
Reflect.get(Ironman, 'firstName') //Tony
Reflect.get(Ironman, 'lastName') //Tony
Reflect.get(Ironman, 'fullName') //Tony Stark
const Spiderman = {
firstName: 'Peter',
lastName: 'Parker'
}
//获取反射属性,只有 Reflect 可以实现
Reflect.get(Ironman, 'fullName', Spiderman) //Peter Parker
//set
const Ironman = {
hobbies: [],
set like(value) {
return this.hobbies.push(value)
}
}
//设置自身属性,新老方法都可以实现
Reflect.set(Ironman, 'like', 'money')
Reflect.set(Ironman, 'like', 'girls')
Ironman.hobbies //["money", "girls"]
const Spiderman = {
hobbies: []
}
//设置反射属性,只有 Reflect 可以实现
Reflect.set(Ironman, 'like', 'games', Spiderman)
Reflect.set(Ironman, 'like', 'animation', Spiderman)
Spiderman.hobbies //["games", "animation"]
Proxy 概述
- Proxy 的兼容性
- Proxy 是什么
- Proxy 为什么要设计
- Proxy 怎么用
- Proxy 的13种拦截方法
- Proxy 在双向绑定中比 Object.defineProperty 的优势
- 举例
Proxy 兼容性
Proxy 是什么
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
关键词:元编程
Proxy 为什么要设计
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
关键词:拦截操作、代理对象
Proxy 怎么用
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
Proxy 拦截对象obj
读取(get)和设置(set)行为:
var obj = new Proxy({}, {
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);
}
});
Proxy 与 Reflect 配合操作对象非常完美,因为它们的参数完全相同
Proxy 的用法的
var proxy = new Proxy(target, handler);
//target参数表示所要拦截的目标对象
//handler参数也是一个对象,用来定制拦截行为。
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。
举个简单例子,Proxy 拦截对属性的读取
const Ironman = {
name: 'Stark',
age: 18,
hobbies: ["money", "girls"]
}
const proxy = new Proxy(Ironman, {
get: function(target, property) {
return 'I am Ironman';
}
});
proxy.name // I am Ironman
proxy.age // I am Ironman
proxy.hobbies // I am Ironman
proxy.xxx // I am Ironman
Proxy 的 handler 方法
Proxy 拦截的操作(handler)一共有13种
get(target, propKey, receiver)
// 拦截 target[propKey]
set(target, propKey, value, receiver)
// 拦截 target[propKey] = value
has(target, propKey)
// 拦截 propKey in proxy
deleteProperty(target, propKey)
// 拦截 delete proxy[propKey]
ownKeys(target)
// 拦截 Object.getOwnPropertyNames(proxy)、
// Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in
getOwnPropertyDescriptor(target, propKey)
// 拦截 Object.getOwnPropertyDescriptor(proxy, propKey)
defineProperty(target, propKey, propDesc)
// 拦截 Object.defineProperty(proxy, propKey, propDesc)、
// Object.defineProperties(proxy, propDescs)
preventExtensions(target)
// 拦截 Object.preventExtensions(proxy)
getPrototypeOf(target)
// 拦截 Object.getPrototypeOf(proxy)
isExtensible(target)
// 拦截 Object.isExtensible(proxy)
setPrototypeOf(target, proto)
// 拦截 Object.setPrototypeOf(proxy, proto)
apply(target, object, args)
// 拦截 Function.prototype.apply.call(func, thisArg, args)
construct(target, args)
// 拦截 new target(...args)
可以看到,Proxy 的拦截操作正好和 Reflect 操作一一对应,所以 Proxy 配合 Reflect 拦截对象操作非常完美
Proxy 拦截读取、改写下划线"_"开头的属性
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {
_prop: 'hello'
};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写自定义私有属性的目的。
Tips: 对于 Proxy 拦截下划线“_”开头的属性,在内部调用是否可行的问题,做了测试,结论如下:
- 代理对象 proxy 获取 _prop 会报错, 原对象 target 可以获取 _prop
- 对于内部用 this 调用自己的属性 _prop 时候,要看 this 的指向,如果 this 指向代理对象 proxy ,就会报错; 如果 this 指向原对象 target ,则可以正常调用。
代码如下
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {
_prop: 'hello',
get a() {
console.log(this)
return this._prop
},
say() {
console.log(this)
console.log(this._prop)
}
};
const proxy = new Proxy(target, handler);
proxy._prop //报错
proxy.a //target hello
proxy.say() //proxy 报错
target._prop //hello
target.a //target hello
target.say() //target hello
计算属性
a
内的this
, 固定指向了原对象target
, 而函数say
,内部的this
需要看调用者了,如果是proxy
调用,就是从proxy
中获取_prop
,自然就报错了
实现双向绑定Proxy 比 defineproperty 优劣如何?
Vue 中实现数据双向绑定是基于 Object.defineProperty
,而Vue的作者宣称将在Vue3.0版本后加入Proxy
从而代替Object.defineProperty
const obj = {};
Object.defineProperty(obj, 'text', {
get: function() {
console.log('get val');
},
set: function(newVal) {
console.log('set val:' + newVal);
document.getElementById('input').value = newVal;
document.getElementById('p').innerHTML = newVal;
}
});
const input = document.getElementById('input');
input.addEventListener('keyup', function(e){
obj.text = e.target.value;
})
Object.defineProperty 的缺陷
1.Object.defineProperty
, 无法监听数组变化。
- 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法,
vm.lists[indexOfLists]=newValue
这种是无法检测的。
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了,以下是方法示例。
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];
aryMethods.forEach((method)=> {
// 这里是原生Array的原型方法
let original = Array.prototype[method];
// 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
// 注意:是属性而非原型属性
arrayAugmentations[method] = function () {
console.log('我被改变啦!');
// 调用对应的原生方法并返回结果
return original.apply(this, arguments);
};
});
let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d'); // 我被改变啦! 4
// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d'); // 4
由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的
2.Object.defineProperty
只能劫持对象的属性, 因此对需要双向绑定的属性需要显示地定义
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应的
vm.b = 2
// `vm.b` 是非响应的
Proxy实现的双向绑定的特点
Proxy
提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy
是Object.defineProperty
的全方位加强版
1.Proxy可以直接监听对象而非属性
const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function(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', function(e) {
newObj.text = e.target.value;
});
我们可以看到,Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。
2.Proxy可以直接监听数组的变化
const list = document.getElementById('list');
const btn = document.getElementById('btn');
// 渲染列表
const Render = {
// 初始化
init: function(arr) {
const fragment = document.createDocumentFragment();
for (let i = 0; i < arr.length; i++) {
const li = document.createElement('li');
li.textContent = arr[i];
fragment.appendChild(li);
}
list.appendChild(fragment);
},
// 我们只考虑了增加的情况,仅作为示例
change: function(val) {
const li = document.createElement('li');
li.textContent = val;
list.appendChild(li);
},
};
// 初始数组
const arr = [1, 2, 3, 4];
// 监听数组
const newArr = new Proxy(arr, {
get: function(target, key, receiver) {
console.log(key);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key !== 'length') {
Render.change(value);
}
return Reflect.set(target, key, value, receiver);
},
});
// 初始化
window.onload = function() {
Render.init(arr);
}
// push数字
btn.addEventListener('click', function() {
newArr.push(6);
});
很显然,Proxy不需要hack就可以无压力监听数组的变化,我们都知道,标准永远优先于hack。
3.Proxy的其他优势
Proxy
有多达13种拦截方法,不限于apply
、ownKeys
、deleteProperty
、has
等等是Object.defineProperty
不具备的。Proxy
返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改。Proxy
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
4.Proxy的劣势
当然,Proxy
的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。