深入实践 ES6 Proxy Reflect
原文: https://zhuanlan.zhihu.com/p/60126477
引言
Vue中的数据绑定
Vue作为前端框架的三驾马车之一,在众多前端项目中具有极其重要的作用。
Vue中具有一个重要的功能点——“数据绑定”。使用者无需关心数据是如何绑定到dom上面,只需要关注数据本身即可。
那实现其功能的原理是什么?
阅读官方文档(v2.0),我们会发现:
把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。
关键字是Object.defineProperty
,在MDN文档找到说明如下:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
我们再仔细查询在MDN文档的说明会发现,Object.defineProperty()
存在两种属性描述符:
数据描述符(简略介绍)
- configurable:数据可改变
- enumerable:可枚举
- value:属性值
- writable:可读写
存取描述符
- get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。
- set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。
至此也就引出了getter/setter
。
ES5 getter/setter
让我们通过一个例子来测试一下。
首先,建立一个英雄(Hero)对象并赋予其一些属性:
**let** hero **=** {name**:**'赵云',hp**:** 100,sp**:** 100
}
然后使用Object.defineProperty()
来对其具有的一些属性进行修改,并且在控制台输出修改的内容:
Object.defineProperty(hero, 'hp', {set (val) {console.log(`Set hp to ${val}`);**return** val;}
})hero.hp **=** '200';
*// --> Set hp to 200*
假若把console.log('Set hp to ${val}') 改为 element.innerHTML = val,是不是就可以实现数据绑定了?
那让我们再修改一下英雄的属性,假设英雄拥有很多装备:
**let** hero **=** {name**:**'赵云',hp**:** 100,sp**:** 100,equipment**:**['马','长枪']
}
我们把“佩剑”添加到英雄的装备中,并且输出在控制台:
`Object.defineProperty(hero.equipment, 'push', {value () {**this**[**this**.length] **=** arguments[0];}
})hero.equipment.push('佩剑');
console.log(hero.equipment);*// --> [ '马','长枪', '佩剑' ]*`
由此,我们可以看到对象的属性变化可以依靠get()
和set()
方法去追踪和改变;但对于数组则需要使用value()方法实现。
显然,这不是最好的方法,那有没有更好的方法可以简化对象或数组属性变化呢?
答案是肯定的。
概念
Proxy
Proxy意思为“代理”,即在访问对象之前建立一道“拦截”,任何访问该对象的操作之前都会通过这道“拦截”,即执行Proxy里面定义的方法。
基本用法:
let pro = new Proxy(target,handler);
- new Proxy()表示生成一个Proxy实例
- target参数表示所要拦截的目标对象
- handler参数也是一个对象,用来定制拦截行为。
handler
Proxy支持13种拦截行为(handle),针对解决上一节的问题,简单介绍下其中2种拦截行为,get与set。
get
get(target, propKey, receiver)
用于拦截某个属性的读取操作,可以接受三个参数:
- target:目标对象
- propKey:属性名
- receiver(可选):proxy 实例本身(严格地说,是操作行为所针对的对象)
set
set(target, propKey, value, receiver)
用于拦截某个属性的赋值操作,可以接受四个参数:
- target:目标对象
- propKey:属性名
- value:属性值
- receiver(可选):Proxy 实例本身
实例
在解决上一节问题之前,先一同看几个实例。
实例1
let hero = {name: "赵云",age: 25
}let handler = {}let heroProxy = new Proxy(hero, handler);console.log(heroProxy.name);
// --> 赵云
heroProxy.name = "黄忠";
console.log(heroProxy.name);
// --> 黄忠
解析:
创建hero对象为所要拦截的对象;
拦截操作对象handler为空,未对拦截对象设定拦截方法;
该情况下heroProxy直接指向原对象target,访问heroProxy等同于访问target,所以结果为target中的结果。
实例2
let hero = {name: "赵云",age: 25
}let handler = {get: (hero, name, ) => {const heroName =`英雄名是${hero.name}`;return heroName;},set:(hero,name,value)=>{console.log(`${hero.name} change to ${value}`);hero[name] = value;return true;}
}let heroProxy = new Proxy(hero, handler);console.log(heroProxy.name);
heroProxy.name = '黄忠';
console.log(heroProxy.name);// --> 英雄名是赵云
// --> 赵云 change to 黄忠
// --> 英雄名是黄忠
解析:
创建hero对象为所要拦截的对象;
handler对象为拦截对象后执行的操作,这里get方法为读取操作,即用户想要读取heroProxy中的属性时执行的拦截操作。
最后创建一个Proxy实例,当读取heroProxy中的属性时,结果打印出来的总是“黄忠”字符串。
实例3
Proxy也可以作为其他对象的原型对象使用。
let hero = {name: "赵云",age: 25
}let handler = {get: (hero, name, ) => {const heroName =`英雄名是${hero.name}`;return heroName;},set:(hero,name,value)=>{console.log(`${hero.name} change to ${value}`);hero[name] = value;return true;}
}let heroProxy = new Proxy(hero, handler);
let obj = Object.create(heroProxy);console.log(obj.name);
obj.name = '黄忠';
console.log(obj.name);// --> 英雄名是赵云
// --> 赵云 change to 黄忠
// --> 英雄名是黄忠
解析:
在实例2的基础上,将heroProxy作为obj的原型对象使用。
虽然obj本身没有name这个属性,但是根据原型链,会在heroProxy上读取到name属性,之后会执行相对应的拦截操作。
解决数据绑定问题
在我们对Proxy有了一定了解后,可以尝试解决上一节的问题。
首先,还是定义一个英雄:
let hero = {name:'赵云',hp: 100,sp: 100,equipment:['马','长枪']
接着,定义一个handler:
let handler = {set(target, property, value) {console.log(`hero's ${property} change to ${value}`);target[property] = value;return true;}
}
然后,修改英雄的hp值
let heroProxy = new Proxy(hero, handler);
heroProxy.hp = 200;
// --> hero's hp change to 200
console.log(hero.hp);
// --> 200
最后,同样把“佩剑”添加到英雄的装备中
let heroProxy = new Proxy(hero.equipment, handler);
heroProxy.push('佩剑');
// --> hero's 2 change to 佩剑
// --> hero's length change to 3
console.log(hero.equipment);
// --> ["马", "长枪", "佩剑"]
可以发现,heroProxy.push('佩剑');
触发了两次set
,原因是push
即修改了hero.equipment的内容,又修改了hero.equipment的length
。
Reflect
在了解了Proxy
之后,细心的我们一定发现,若需要在Proxy
内部调用对象的默认行为,该如何实现?
Reflect
正是ES6 为了操作对象而提供的新 API。
基本特点
- 只要
Proxy
对象具有的代理方法,Reflect
对象全部具有,以静态方法的形式存在。这些方法能够执行默认行为,无论Proxy
怎么修改默认行为,总是可以通过Reflect
对应的方法获取默认行为。 - 修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。 - 让
Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。
静态方法
Reflect
对象一共有 13 个静态方法(匹配Proxy
的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
对象的方法是一一对应的。
实例
下面通过3个实例来对比Object
对象方法与Reflect
对象方法。
实例1
//Object对象方法
try {Object.defineProperty(target, name, property);
} catch (e) {console.log("error");
}//Reflect对象方法
if (Reflect(target, name, property)) {console.log("success");
} else {console.log("error")
解析:
由于Reflect(target, name, property)
返回的是boolean,代码语义性更好。
实例2
let hero = {name: '赵云',hp: 100,sp: 100,equipment: ['马', '长枪']
}//Object对象方法
console.log('name' in hero);
// --> true//Reflect对象方法
console.log(Reflect.has(hero,'name'));
// --> true
解析:
Object
操作是命令式,而Reflect
让它们变成了函数行为
实例3
let hero = {name: '赵云',hp: 100,sp: 100,equipment: ['马', '长枪']
}let handler = {get(target, name, receiver) {if (name === "name") {console.log("success");} else {console.log("failure");}return Reflect.get(target, name, receiver);}
}let heroProxy = new Proxy(hero, handler);
console.log(heroProxy.name);
// --> success
// --> 赵云
解析:
Reflect
对象的操作和Proxy
对象的操作一一对应,在Proxy
的拦截操作中,可以直接利用Reflect
对象直接获取Proxy
的默认值。
结合实践
掌握了Proxy
与Reflect
的知识点后,除了解决文章开头的数据绑定问题之外,挑选日常编码中容易遇见的两种情况进行编码实践。
观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦数据有变化,函数就会自动执行。
let hero = {name: '赵云',hp: 100,sp: 100,equipment: ['马', '长枪']
}const handler = {set(target, key, value, receiver) {//内部调用对应的 Reflect 方法const result = Reflect.set(target, key, value, receiver);//执行观察者队列observableArray.forEach(item => item());return result;}
}//初始化Proxy对象,设置拦截操作
const createProxy = (obj) => new Proxy(obj, handler);//初始化观察者队列
const observableArray = new Set();const heroProxy = createProxy(hero);//将监听函数加入队列
observableArray.add(() => {console.log(heroProxy.name);
});heroProxy.name = "黄忠";
// --> 黄忠
该实例在set
拦截行为中加入了监听函数的执行,使每一次值的改变均能被监听。
对象多重继承
实现对象间的单继承,比如obj2继承obj1,可以使用Object.setPrototypeOf
方法,但是没法实现多继承。
const people = {name: 'people',run() {console.log('people.run:', this.name);}
};const powerMan = {name: 'powerMan',run() {console.log('powerMan.run:', this.name);},fight() {console.log('powerMan.fight:', this.name);}
};const handler = {get(target, name, receiver) {if (Reflect.has(target, name)) {return Reflect.get(target, name, receiver);}else {for (let P of target[Symbol.for('[[Prototype]]')]) {if (Reflect.has(P, name)) {return Reflect.get(P, name, receiver);}}}}
};const hero = new Proxy({name: 'hero',strike() {this.run();this.fight();}
}, handler);hero[Symbol.for('[[Prototype]]')] = [people, powerMan];
hero.strike();
// --> people.run:hero
// --> powerMan.fight:hero
用了一个自定义的属性Symbol.for("[[Prototype]]")
来表示要继承的多个父对象。
然后用Proxy
来拦截所有hero
中的get
请求,使用Reflect.has
方法检查hero
中是否存在相应的属性或者方法。
如果存在,则直接转发;如果不存在,则遍历父对象列表,在父对象中逐个检查是否存在相应的属性或者方法。
若存在则调用。若不存在,则相当于get
返回undefined
。
深入实践 ES6 Proxy Reflect相关推荐
- es6利用Reflect实现观察者模式,并详解Reflect对象
es6利用Reflect实现观察者模式,并详解Reflect对象 字面意思:反映;映出(影像) 先看一个简易的观察者模式: 上述观察者模式代码运行后输出: // 李四 20: // 哈哈 10: 分析 ...
- ES6 Proxy 性能之我见
ES6 Proxy 性能之我见 本文翻译自https://thecodebarbarian.com/thoughts-on-es6-proxies-performance Proxy是ES6的一个强力 ...
- ES6 Proxy 和 Reflect 的理解
Vue中的数据绑定 ps:观察者模式 (下面有重点) Vue作为前端框架的三驾马车之一,在众多前端项目中具有极其重要的作用. Vue中具有一个重要的功能点--"数据绑定".使用者无 ...
- ES6 之Reflect 与 Proxy概述
Proxy 与 Reflect 是 ES6 为了操作对象引入的 API .Proxy 可以对目标对象的读取.函数调用等操作进行拦截,然后进行操作处理. 概述 Proxy 与 Reflect 是 ES6 ...
- ES6 Proxy和Reflect (上)
Proxy概述 Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种"元编程"(meta programming),即对编程语言进行编程. Proxy可以理 ...
- ES6 Proxy和Reflect
目录 Proxy 概述 基本用法 Proxy 实例方法 1.get(target, propKey, receiver) 2.set(target, propKey, value, receiver) ...
- ES6:Reflect
Reflect 概述. Relflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新的API.Reflect对象的设计目的有几个. (1)将Object对象的一些明显属于语言内部的方法( ...
- es6 --- Proxy的属性(get、set除外)
apply(): 拦截函数的调用.call和apply操作 var target = function () { return 'I am the target';}; var handler = { ...
- es6 --- Proxy实例的get方法
写一个拦截函数,访问目标对象不存在属性时,会抛出不存在该属性的错误 如果存在该属性时,就返回其值. var person = {name: "张三" };var proxy = n ...
最新文章
- android开发教程
- 微软正在改进 Windows 命令行:为 Linux 用户敞开大门
- eprime经典程序案例_小程序经典案例!写字楼里的小店铺如何利用小程序增加人流量!...
- c语言方向变量,C语言,变量与内存
- 利用Lucene.net搭建站内搜索(3)---创建索引
- android布局添加布局,Android中添加布局和初始化布局总结
- JQ实现单击按钮 倒计时获取验证码
- iOS 绝对值方法
- 拓端tecdat|R语言:用R语言填补缺失的数据
- 天气 android 源码,android 天气预报app源码
- python 基于金字塔的图像融合
- USB接口WiFi驱动浅析
- 戴尔服务器板载系统raid管理,如何在 Dell 系统的统一可扩展固件接口(UEFI)配置中管理您的板载 LSI 3008 RAID 控制器...
- 计算机链接局域网,window7连接局域网的方法
- ubuntu mysql 默认安装路径_ubuntu安装mysql并修改默认目录
- spring mvc
- 山石岩读丨一文读懂区块链安全:区块链会带来哪些冲击?
- 解决python ping测试
- python微信群聊机器人_Python + itchat 实现微信机器人聊天(支持自动回复指定群聊)...
- 百度的链接打不开解决办法