开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
大家都已经知道了,Vue3就是通过Proxy实现数据响应式原理的,那么proxy为什么可以实现数据响应式?经常和Proxy一起出现的Reflect又是用来干嘛的?Proxy还有一些什么运用场景呢?
Proxy是什么?
Proxy
是JS的一个原生对象,它可以用于创建一个对象的代理,从而实现基本操作的拦截和自定义行为
-
原生对象
Proxy
是ES6新增的,也就是2015年才出现的,Vue2是2016年发布使用的,所以你知道Vue2为什么不是使用Proxy而是使用Object.definePropety
了吧;听到ES6不知道你会不会或多或少的想到兼容性问题,由于ES5的限制,Proxy
无法被转译成ES5,所以在一些老旧的浏览器上(手动@某E),Proxy
是无法使用的,所以Vue3在Proxy无法使用的时候,就会进行降级,还是采用Object.definePropety
的方式
-
创建一个对象的代理
看到创建一个对象,就莫名的想到
new
(毕竟单身的你,时常被同事调侃自己new一个对象),细心的小伙伴早已经发现了,Proxy的首字母是大写的,还记得那个不成文的规定么,构造函数的首字母一般是大写的,使用new
操作符进行调用,是的没错,Proxy就是一个构造器,也是用new
去创建一个对象的代理,所以被创建的对象的原型就是Proxy
对象啦Proxy
是创建的是一个对象的代理,而Object.definePropety
是在一个对象上定义新的属性或修改一个已存在的属性;所以是对一整个对象代理效率高,还是一个属性一个属性定义效率高?结果不言而喻。任何类型的对象都能被代理,包括内置的数组,函数,甚至另一个
Proxy
对象;所以你知道Vue2为什么监听不到数组的变化,而Vue3可以了吧 -
基本操作
Proxy可以拦截以下13种操作
apply
-拦截函数调用construct
-拦截new
操作符defineProperty
-拦截对象的Object.definePropety
操作delete
-拦截对象属性的删除操作get
-拦截对象的读取属性操作getOwnPropertyDescriotor
-拦截对象Object.getOwnPropertyDescriptor
操作getPrototypeOf
-拦截读取代理对象的原型操作has
-拦截in
操作符isExtensible
-拦截对象的Object.isExtensible
操作ownKeys
-拦截Object.getOwnPropertyNames
和Object.getOwnPropertySymbols
操作preventExtensions
-拦截Object-preventExtensions
操作set
-拦截设置属性值操作setPrototypeOf
-拦截Object.setPrototypeOf
操作
-
自定义行为
通过拦截上面的13种操作,可以自定义操作,这样你创建的代理对象进行上面的13种操作,都会触发你自定义的行为;你想让它往东它就往东,你没让它干嘛他就按照默认的行为干嘛。
Proxy使用
new Proxy(target,handler)
-
参数
target
-它可以是任何类型的对象,包括内置数组,函数甚至是另一个Proxy对象handler
-它也是一个对象,他的属性提供了某些操作(上面的13中操作)发生是所对应的处理函数
this的指向
const person = {
name: 'George',
getThis: function () {
console.log(this);
}
}
const personProxy = new Proxy(person, {
get: function (target, property, receiver) {
console.log(this); // 打印的是handler对象,handler中的this指向handler
return target[property]
}
})
person.getThis() // 谁最后调用,this就指向谁,打印的是person对象
personProxy.getThis() // 打印的personProxy对象
当然如果你用匿名函数,那他们的this都指向window了
const person = {
name: 'George',
getThis: () => {
console.log(this);
}
}
const personProxy = new Proxy(person, {
get: (target, property, receiver) => { // 访问回调
console.log(this); // 打印的是Window对象
console.log(receiver); // 打印的是personProxy对象
return target[property]
}
})
person.getThis() // 打印的是Window对象
personProxy.getThis() // 打印的是Window对象
常见的几种拦截属性
handler.get
get可以创造一些本来没有的属性的返回值,可以在取值的时候对数据进行加工操作等,get 方法可以返回任何值。
let Moutai = {
price: 888,
degrees: 52
}
const MaotaiDealer = new Proxy(Moutai, {
get: (target, property, receiver) => { // 访问回调
if (property === 'price') {
return 1599
}
if (property === 'address') {
return '贵州省仁怀市茅台镇'
}
return target[property]
},
set: () => { }
})
console.log(Moutai.price); // 888
console.log(Moutai.degrees); // 52
console.log(Moutai.address); // undefined
console.log(MaotaiDealer.price); // 1599
console.log(MaotaiDealer.degrees); // 52
console.log(MaotaiDealer.address); // 贵州省仁怀市茅台镇
target
--目标对象property
--被获取的属性名receiver
--Proxy实例
如果把对象的某个属性配置修改成不可配置,不可写,又自定了handler.get
的函数返回不同的值,那么去获取该属性时就会报错,如果要访问的目标舒心是不可写以及不可配置的,则返回的值必须与该目标属性的值相同
let Moutai = {
price: 888,
degrees: 52
}
Object.defineProperty(Moutai, 'price', {
configurable: false,
writable: false
})
const MaotaiDealer = new Proxy(Moutai, {
get: (target, property, receiver) => { // 访问回调
if (property === 'price') {
return 1599
}
return target[property]
}
})
console.log(MaotaiDealer.price); //TypeError: 'get' on proxy: property 'price' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '888' but got '1599')
console.log(Moutai.price); // 888
console.log(MaotaiDealer.degrees); // 52
handler.set
set操作一般用于对于要赋值的数进行过滤,加工或是权限设置,set()
方法应当返回一个布尔值。
- 返回
true
代表属性设置成功。 - 在严格模式下,如果
set()
方法返回false
,那么会抛出一个TypeError
异常。
let Moutai = {
price: 888,
degrees: 52
}
const MoutaiManufacturer = new Proxy(Moutai, {
get: (target, property, receiver) => {
if (property === 'address') {
return '贵州省仁怀市茅台镇'
}
return target[property]
},
set: (target, property, value, receiver) => {
if (property === 'address') {
return false
}
target[property] = value
return true
}
})
MoutaiManufacturer.price = 1299
console.log(MoutaiManufacturer.price); // 1299
console.log(Moutai.price);// 1299
MoutaiManufacturer.address = '广东省深圳市'
console.log(MoutaiManufacturer.address); // 贵州省仁怀市茅台镇
target
--目标对象property
--被设置的属性名value
--新属性的值receiver
--Proxy实例
handler.set()
方法用于拦截设置属性值的操作,如果违背以下的约束条件,proxy 会抛出一个 TypeError
异常:
- 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值。
- 如果目标属性没有配置存储方法,即
[[Set]]
属性的是undefined
,则不能设置它的值。 - 在严格模式下,如果
set()
方法返回false
,那么也会抛出一个TypeError
异常。
handler.deleteProperty
handler.deleteProperty()
方法用于拦截删除属性的操作,deleteProperty
必须返回一个 Boolean
类型的值,表示了该属性是否被成功删除。如果目标对象的属性是不可配置的,那么该属性不能被删除。
let Moutai = {
price: 888,
degrees: 52
}
let MaotaiDealer = new Proxy(Moutai, {
deleteProperty: (target, property) => {
if (property === 'price') {
return false
}
delete target[property]
return true
}
})
delete MaotaiDealer.degrees // true
target
--目标对象property
--被设置的属性名
handler.apply
handler.apply()
方法用于拦截函数的调用,apply
方法可以返回任何值。target
必须是可被调用的。也就是说,它必须是一个函数对象。
function sum(a, b) {
return a + b
}
const proxyFu = new Proxy(sum, {
apply: (target, thisArg, argumentsList) => {
return target(...argumentsList) * 3
}
})
sum(3, 5) // 8
proxyFu(3, 5) // 24
target
--目标对象thisArg
--被调用时的上下文对象argumentsList
--被调用时的参数数组
Reflect是什么?
Reflect
是一个内置的对象,字面意思是“反射”,它提供拦截 JavaScript 操作的方法。这些拦截方法和Proxy中的13中拦截方法命名相同。
-
内置的对象
Reflect
与大多数全局对象不同,Reflect
不是一个函数对象,并非一个构造函数,所以他是不可构造的,即不能通过new操作符调用,或者将Reflect
对象作为函数来调用。Reflect的所有属性和方法都是静态的(就像Math
对象)
-
操作方法
Reflect
对象提供了以下静态方法,这些方法与Proxy
拦截方法的命名相同,其中有些方法和Object
相同Reflect.apply(target,thisArgument,argumentsList)
Reflect.construct(target,argumentsList[, newTarget])
Reflect.defineProperty(target, propertyKey, attributes)
Reflect.deleteProperty(target, propertyKey)
Reflect.get(target, propertyKey[, receiver])
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Reflect.getPrototypeOf(target)
Reflect.has(target, propertyKey)
Reflect.isExtensible(target)
Reflect.ownKeys(target)
Reflect.preventExtensions(target)
Reflect.set(target, propertyKey, value[, receiver])
Reflect.setPrototypeOf(target, prototype)
-
命名相同
看完上面的命名,发现确实一样,那他们之间对应着什么样联系呢?其实
Reflect
的方法是用来操作对象的,可以看做是Object
的升级版,增加了兼用性,对浏览器和用户友好;这是因为在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更规范,所以将这些API放到了Object
上面,但是Object
作为一个构造函数,这些操作实际上放到它身上并不合适;另外还包含一些类似于in、delete操作符,让JS看起来会有一些奇怪,所以在ES6增加了Relect
,把这些操作都集中到Relect
对象上。简单理解就是Object
这个函数上的属性太杂了,大概有20种左右,虽然其中包含了对象接口。但是这不太好,我们需要一个专门的对象来做这个事情。显然不可能重新设计Object,毕竟兼容性才是大哥。于是Reflect应运而生,取名为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 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。
-
Relect使用
Reflect.apply(target,thisArgument,argumentsList)
通过指定的参数列表发起对目标 (target) 函数的调用;该方法和ES5中的Function.prototype.apply()
方法类似:调用一个方法并显示地指定this变量和参数列表(arguments),参数列表可以是数组或类数组的对象
Reflect.apply("".charAt, "ponies", [3]) // i
// 相当于 'ponies'.charAt(3)
Reflect.apply(Math.floor, undefined, [1.75]); // 1
// 相当于 Math.floor(1.75)
target
--目标函数thisArgument
--target函数调用时绑定的this对象argumentsList
--target函数调用时传入的实参列表,该参数是一个数组或类数组的对象
Reflect.construct(target,argumentsList[, newTarget])
该方法有点像new
操作符构造函数,相当于new target(...args)
function Person(name) {
this.name=name
}
const zhansanObj= Reflect.construct(Person,['张三'])
// 相当于 const zhansanObj = new Person('张三')
-
target
--被运行的目标构造函数(必须要是构造函数,如果不是会抛出异常) -
argumentsList
--类数组,目标构造函数调用时的参数 -
newTarget
--可选,作为新创建对象的原型对象的constructor
属性(必须要是构造函数,如果不是会抛出异常)function Person(name) { this.name=name } function NewTarget(name){ this.name=name } const zhansanObj= Reflect.construct(Person,['张三'],NewTarget) // 相当于 const zhansanObj = new NewTarget('张三')
Reflect.defineProperty(target, propertyKey, attributes)
基本等同于 Object.defineProperty()
方法,唯一不同是Object.defineProperty()
返回的是 Boolean
值;Object.defineProperty
方法,如果成功则返回一个对象,否则抛出一个 TypeError
。如果target
不是 Object
,抛出一个TypeError
。
let obj = {}
Reflect.defineProperty(obj,'x',{value:10})
Object.defineProperty(obj,'y',{value:20})
console.log(obj); // {x:10,y:20}
target
--目标对象propertyKey
--要定义或修改的属性的名称attributes
--要定义或修改的属性的描述。
Reflect.deleteProperty(target, propertyKey)
用于删除属性,它很像delete obj.prop
,返回值为Boolean
表明该属性是否被成功删除。如果target
不是 Object
,抛出一个 TypeError
。
let Moutai = {
price: 888,
degrees: 52
}
Reflect.deleteProperty(Moutai,price) // true
// 相当于
delete Moutai.price // true
target
--删除属性的目标对象。propertyKey
--需要删除的属性的名称。
Reflect.get(target, propertyKey[, receiver])
Reflect.get
方法允许你从一个对象中取属性值。就如同属性访问器语法,但却是通过函数调用来实现。如果target
不是 Object
,抛出一个 TypeError
。
let Moutai = {
price: 888,
degrees: 52
}
Reflect.get(Moutai,'price') // 888
Reflect.get(["zero", "one"], 1); // "one"
target
--需要取值的目标对象propertyKey
--需要获取的值的键值receiver
--如果target
对象中制定了getter
,receiver
则为getter
调用时的this
值
Reflect.getOwnPropertyDescriptor(target, propertyKey)
静态方法 Reflect.getOwnPropertyDescriptor()
与 Object.getOwnPropertyDescriptor()
方法相似。如果在对象中存在,则返回给定的属性的属性描述符。否则返回 undefined
。唯一不同在于如何处理非对象目标,如果该方法的第一个参数不是一个对象,Reflect.getOwnPropertyDescriptor()
会报错,Object.getOwnPropertyDescriptor()
将参数强制转换为一个对象处理。
Reflect.getOwnPropertyDescriptor({x: "hello"}, "x");
// {value: "hello", writable: true, enumerable: true, configurable: true}
// 相当于 Object.getOwnPropertyDescriptor({x: "hello"}, "x");
Reflect.getOwnPropertyDescriptor("foo", 0); // TypeError: "foo" is not non-null object
Object.getOwnPropertyDescriptor("foo", 0);// { value: "f", writable: false, enumerable: true, configurable: false }
target
--需要取值的目标对象propertyKey
--获取自己的属性描述符的属性的名称
Reflect.getPrototypeOf(target)
Reflect.getPrototypeOf()
与 Object.getPrototypeOf()
方法几乎是一样的。都是返回指定对象的原型(即内部的 [[Prototype]]
属性的值);如果target
不是 Object
,抛出一个 TypeError
。Reflect
抛异常,Object
强制类型转换;
Reflect.getPrototypeOf({}); // Object.prototype
Reflect.getPrototypeOf(Object.prototype); // null
Reflect.getPrototypeOf(Object.create(null)); // null
target
--获取原型的目标对象
Reflect.has(target, propertyKey)
Reflect.has()
作用与 in
操作符相同,返回值Boolean
, 如果target
不是 Object
,抛出一个 TypeError
。
Reflect.has({x: 0}, "x"); // true
Reflect.has({x: 0}, "y"); // false
// 如果该属性存在于原型链中,返回 true
Reflect.has({x: 0}, "toString");
target
--目标对象propertyKey
--属性名,需要检查目标对象是否存在此属性。
Reflect.isExtensible(target)
Reflect.isExtensible()
判断一个对象是否可扩展(即是否能够添加新的属性),返回值Boolean
, ,它 Object.isExtensible()
方法相似,但有一些不同,如果该方法的第一个参数不是一个对象,Reflect.isExtensible()
会报错,Object.isExtensible()
将参数强制转换为一个对象处理。
let empty = {};
Reflect.isExtensible(empty); // === true
Reflect.preventExtensions(empty);
Reflect.isExtensible(empty); // === false
Reflect.isExtensible(1);
// TypeError: 1 is not an object
Object.isExtensible(1);
// false
target
--检查是否可扩展的目标对象。
Reflect.ownKeys(target)
Reflect.ownKeys()
返回一个由目标对象自身的属性键组成的数组。它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
。如果target
不是 Object
,抛出一个 TypeError
。
Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]
target
--获取自身属性键的目标对象
Reflect.preventExtensions(target)
Reflect.preventExtensions()
方法阻止新属性添加到对象 (例如:防止将来对对象的扩展被添加到对象中)。返回值Boolean
, 该方法与 Object.preventExtensions()
相似,但有一些不同点,如果该方法的第一个参数不是一个对象,Reflect.preventExtensions()
会报错,Object.preventExtensions()
将参数强制转换为一个对象处理。
// Objects are extensible by default.
let empty = {};
Reflect.isExtensible(empty); // === true
// ...but that can be changed.
Reflect.preventExtensions(empty);
Reflect.isExtensible(empty); // === false
target
--阻止扩展的目标对象。
Reflect.set(target, propertyKey, value[, receiver])
Reflect.set
方法允许你在对象上设置属性。它的作用是给属性赋值并且就像obj.prop = value
语法一样,但是它是以函数的方式。返回一个 Boolean
值表明是否成功设置属性。如果target
不是 Object
,抛出一个 TypeError
。
// Object
let obj = {};
Reflect.set(obj, "prop", "value"); // true
obj.prop; // "value"
// Array
let arr = ["duck", "duck", "duck"];
Reflect.set(arr, 2, "goose"); // true
arr[2]; // "goose"
// 它可以截断数组。
Reflect.set(arr, "length", 1); // true
arr; // ["duck"];
// 只有一个参数,propertyKey和value“未定义”。
var obj = {};
Reflect.set(obj); // true
Reflect.getOwnPropertyDescriptor(obj, "undefined");
// { value: undefined, writable: true, enumerable: true, configurable: true }
target
--设置属性的目标对象。propertyKey
--设置的属性的名称。value
--设置的值。receiver
--如果遇到setter
,receiver
则为setter
调用时的this
值。
Reflect.setPrototypeOf(target, prototype)
Reflect.setPrototypeOf()
与 Object.setPrototypeOf()
方法是一样的。它可设置对象的原型(即内部的 [[Prototype]]
属性)为另一个对象或 null
,如果操作成功返回 true
,否则返回 false
。如果target
不是 Object
,抛出一个 TypeError
。
Reflect.setPrototypeOf({}, Object.prototype); // true
// 它可以将对象的[[Prototype]]更改为null
Reflect.setPrototypeOf({}, null); // true
// 如果目标不可扩展,则返回false。
Reflect.setPrototypeOf(Object.freeze({}), null); // false
// 如果导致原型链循环,则返回false。
let target = {};
let proto = Object.create(target);
Reflect.setPrototypeOf(target, proto); // false
target
--设置原型的目标对象propertyKey
--对象的新原型(一个对象或null
)。
了解了Relect
怎么使用,我们就可以把之前的写法改一下,比如target[property]
改成Reflect.get(target,property)
;比如target[property] = value
改成Reflect.set(target,property,value)
;Relect
一般是和Proxy
同时使用的
Proxy使用例子
统计函数被调用
const countExecute = (fn) => {
let count = 0
return new Proxy(fn, {
apply(target, ctx, args) {
++count
console.log('ctx上下文:', ctx);
console.log(`第${count} 次 调用${fn.name} `);
return Reflect.apply(target, ctx, args)
}
})
}
const getSum = (...args) => {
return args.reduce((pre, cur) => pre + cur, 0)
}
const useSum = countExecute(getSum)
useSum(1, 2, 3) // ctx上下文: undefined , 第1 次 调用getSum
useSum.apply(window, [4, 5, 6]) //ctx上下文:Window , 第2 次 调用getSum
let obj = {}
useSum.apply(obj, [7,8,9]) //ctx上下文: {}, 第3 次 调用getSum
实现一个节流功能
const throttleByProxy = (fn, time) => {
let lastTime = 0
return new Proxy(fn, {
apply(target, ctx, args) {
const nowTime = Date.now()
if (nowTime - lastTime > time) {
lastTime = nowTime
Reflect.apply(target, ctx, args)
}
}
})
}
const longTimeStamp = () => console.log(Date.now());
window.addEventListener('scroll', throttleByProxy(longTimeStamp, 300));
实现观察者模式
const list = new Set()
const observe = (fn) => list.add(fn)
const observable = (obj) => {
return new Proxy(obj, {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
list.forEach((observe) => observe())
return result
}
})
}
const person = observable({name:'George',age:20}) // 使用Proxy创建代理对象
const App = () =>{
console.log(`App -> name: ${person.name}, age: ${person.age}`);
}
observe(App); // 加入观察者
person.name='XXX' // person属性变化,执行观察者 App -> name: XXX, age: 20
最后
伴随着Reflect,Proxy降世,为js带来了元编程!下次一定!
如果你看到了这里,烦请大佬点个赞,鼓励一下小弟学习。