如果你平时有关注 Vue 的进展的话,可能已经知道了在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作
Proxy它是一个构造函数,返回一个代理对象Proxy,主要用于从外部控制对对象内部的访问。
使用 Proxy 的好处是:对象只需关注于核心逻辑,一些非核心的逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)可以让 Proxy 来做。从而达到关注点分离,降级对象复杂度的目的。
代理允许你拦截在目标对象上的底层操作(Object下的许多原型方法),而这原本是 JS 引擎的内部能力。拦截行为使用了 一个能够响应特定操作的函数(被称为陷阱)
外部如何去影响对象内部的访问呢,先创建一个简单的代理
//目标对象 句柄对象
var target = {}, handler = {};
var proxy = new Proxy(target, handler);
那么Proxy、target、handler这三者之间有什么关系呢?Proxy的行为很简单:将Proxy的所有内部方法转发至target 。即调用Proxy的方法就会调用target上对应的方法。那么handler又是用来干嘛的?handler的方法可以覆写任意代理的内部方法(一个或多个陷阱函数的对象)。 外界每次通过Proxy访问target 对象的属性时,就会经过 handler 对象,因此,我们可以通过重写handler对象中的一些方法来做一些拦截的操作。
//最简单粗暴的proxy
let target={}
let proxy=new Proxy(target,{})
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
console.log(proxy,target)//Proxy {name: "target"} {name: "target"}
之前说了哈Proxy返回一个Proxy对象
该例中的 proxy 对象将所有操作直接传递给 target 对象,当然没有陷阱函数的代理没什么用
使用 set 陷阱函数验证属性值
var a={
_year:2019,
get year(){
return this._year
},
set year(newValue){
this._year=newValue
}
}
可以区分一下写法,ES5 set支持一个参数,代理中的set支持四个参数
trapTarget :将接收属性的对象(即代理的目标对象);
key :需要写入的属性的键(字符串类型或符号类型);
value :将被写入属性的值;
receiver :操作发生的对象(通常是代理对象)//很多时候可以省略
简单的demo
let target={
name:'lwj'
}
let proxy=new Proxy(target,{
set(trapTarget, key, value, receiver){
if(!trapTarget.hasOwnProperty(key)){ //如果已存在的属性,绕过
if(isNaN(value)){
throw new TypeError("Property must be a number.");
}
}
//添加属性
return Reflect.set(trapTarget, key, value, receiver)
//这里也可以改写 不使用上方的代码 函数还是尽量return一下 当然建议使用上方的方法
//trapTarget[key]=value
// return true
}
})
proxy.count=1
console.log(proxy.count) //1
console.log(target.count) //1
proxy.name='lwj'
console.log(proxy.name)//lwj
console.log(target.name) //lwj
proxy.anotherName = "proxy"; //报错
set 代理陷阱允许你在写入属性值的时候进行拦截,而 get 代理陷阱则允许你在读取属性 值的时候进行拦截。
get陷阱函数参数借鉴了 set 陷阱函数的参数,只有一个明显的不同,也就是没有使用 value
let target={
name:'lwj'
}
console.log(target.age)//undefined
由于js语言的特殊,属性是不会报错的,然而有时候我们不希望存在这个问题
let proxy=new Proxy({},{
get(tarpTarget,key,receiver){
//不存在此属性
if(!(key in receiver)){
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(tarpTarget,key,receiver);
}
})
proxy.name='lwj'
console.log(proxy.name)
console.log(proxy.names) //报错 Property names doesn't exist.
这里使用receiver是因为receiver自带has陷阱函数,使用 trapTarget 会跳过 has 陷阱函数
写has方法之前写一个数据监听,如果你能看懂的话很棒,看不懂还是再看看前面!
let onWatch=(obj,sets,gets)=>{
let handler={
get(tarpTarget,key,receiver){
gets(tarpTarget,key)
return Reflect.get(tarpTarget,key,receiver)
},
set(tarpTarget,key,value,receiver){
sets(value,key)
return Reflect.set(tarpTarget,key,value,receiver)
}
}
return new Proxy(obj, handler)
}
就不打注释了,自己理解的好
let obj={a:1}
let p=onWatch(obj,
(v,property)=>{
console.log(`监听到属性${property}改变为${v}`)
},
(target,property)=>{
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了
get(target,key,receiver){s
gets(target, key)
// 这句判断代码是新增的
if (typeof tarpTarget[key] === 'object' && tarpTarget[key] !== null) {
return new Proxy(tarpTarget[key],handler);
} else {
return Reflect.get(tarpTarget,key);
}
}
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象(陷阱函数)的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。比如Math对象
has陷阱函数
trapTarget :需要读取属性的对象(即代理的目标对象);
key :需要检查的属性的键(字符串类型或符号类型)。
let target={
value:10,
name:'lwj'
}
let proxy=new Proxy(target,{
has(trapTarget,key){
if(key==='value'){
return false
}else{
return Reflect.has(trapTarget,key)
}
}
})
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true
使用 deleteProperty 陷阱函数避免属性被删除
delete 运算符能够从指定对象上删除一个属性,在删除成功时返回 true ,否则返回 false
var obj={
name:'lwj'
}
Object.defineProperty(obj,'age',{
value:18,
configurable:false
})
console.log("name" in obj) //true
console.log('age' in obj) //true
let result= delete obj.name
console.log(result) //true
console.log("name" in obj) //false
let results= delete obj.age
console.log(results) // false
console.log('age' in obj) //true
deleteProperty 陷阱函数会在使用 delete 运算符去删除对象属性时下被调用,并且会被传 入两个参数:
trapTarget :需要删除属性的对象(即代理的目标对象);
key :需要删除的属性的键(字符串类型或符号类型)。
let target={
name:'lwj',
age:18
}
let proxy=new Proxy(target,{
deleteProperty(trapTarget,key){
if(key==="age"){
return false
}else{
return Reflect.deleteProperty(trapTarget,key)
}
}
})
console.log("age" in proxy); // true
let result1 = delete proxy.age
console.log(result1); // false
console.log("age" in proxy); // true
console.log("name" in proxy); // true
let result2 = delete proxy.name
console.log(result2); // true
console.log("name" in proxy); // false
写下个陷阱函数之前先弥补两个对象下的方法getPrototypeOf(),setPrototypeOf(),获取原型设置原型
let person={
getting(){
return 'Hello'
}
}
let dog={
getting(){
return 'Hi'
}
}
let friend = Object.create(person)
console.log(friend.getting());//Hello
console.log(Object.getPrototypeOf(friend) === person); // true
//原型设置为dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getting());//Hi
console.log(Object.getPrototypeOf(friend) === dog); // true
寄生组合继承就是利用了Object.create方法,将父类的原型赋值给了子类,既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
function Person(age){
this.age=age
}
function Child(age){
Person.call(this,age)
}
Child.prototype=Object.create(Person.prototype,{
constructor:{
value:Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child=new Child(18)
原型代理的陷阱函数setPrototypeOf() getPrototypeOf()
原型代理的陷阱函数类似上方,也有两个,同样也可以利用Reflect反射,参数:
trapTarget :需要设置原型的对象(即代理的目标对象);
proto :需用被用作原型的对象。
getPrototypeOf 陷阱函数只接受 trapTarget 参数
这些陷阱函数受到一些限制。首先, getPrototypeOf 陷阱函数的返回值必须是一个对象或者 null ,其他任何类型的返回值都会引发“运行时”错误。对于返回值的检测确保了 Object.getPrototypeOf() 会返回预期的结果。类似的, setPrototypeOf 必须在操作没有成 功的情况下返回 false ,这样会让 Object.setPrototypeOf() 抛出错误;而若 setPrototypeOf 的返回值不是 false ,则 Object.setPrototypeOf() 就会认为操作已成 功。
先看代码再看上面文字容易理解
let target = {};
let proxy=new Proxy(target,{
getPrototypeOf(trapTarget){
return null
},
setPrototypeOf(trapTarget,proto){
return false
}
})
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
Object.setPrototypeOf(target, {}); //成功
Object.setPrototypeOf(proxy, {}); //报错
使用 target 对象作为参数调用 Object.getPrototypeOf() 会返回一个对象值;而使用 proxy 对象调用该方法则会返回 null ,因为 getPrototypeOf 陷阱函数被调用了。类似的,使用 target 去调用 Object.setPrototypeOf() 会成功;而由于 setPrototypeOf 陷阱函数的存在,使用 proxy 则会引发错误。
let target = {};
let proxy=new Proxy(target,{
getPrototypeOf(trapTarget){
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget,proto){
return Reflect.setPrototypeOf(trapTarget, proto);
}
})
如此就可以了,俩者方法看起来非常相似但是也有差异,有兴趣可以百度,这里简单过一下
首先, Object.getPrototypeOf() 与 Object.setPrototypeOf() 属于高级操作,而 Reflect.getPrototypeOf() 与 Reflect.setPrototypeOf() 属于底层 操作,允许开发者访问 [[GetPrototypeOf]] 与 [[SetPrototypeOf]]
Reflect.getPrototypeOf() 方法在接收到的参数不是一个对象时会抛出错误,而 Object.getPrototypeOf() 则会在操作之前先将参数值转换为一个对象。
let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype); // true
Reflect.getPrototypeOf(1); //报错
对象可扩展性的陷阱函数
对象是否是可扩展的(是否可以在它上面添加新的属性)
Object.isExtensible(obj); // === true
大多数基本对象都是可扩展的
// ...可以变的不可扩展.
Object.preventExtensions(obj);
Object.isExtensible(obj); // === false
密封对象与冻结对象也不可扩展
Object.seal({}); Object.freeze({});
let target={}
let proxy=new Proxy(target,{
isExtensible(trapTarget){
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
//return false 即不会改变代理对象的可扩展性
return Reflect.preventExtensions(trapTarget);
}
})
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
属性描述符的陷阱函数
defineProperty(trapTarget, key, descriptor)
getOwnPropertyDescriptor(trapTarget, key)
descriptor :为该属性准备的描述符对象
ES5 最重要的特征之一就是引入了 Object.defineProperty() 方法用于定义属性的特性。在 JS 之前的版本中,没有方法可以定义一个访问器属性,也不能让属性变成只读或是不可枚 举。而这些特性都能够利用 Object.defineProperty() 方法来实现,并且你还可以利用 Object.getOwnPropertyDescriptor() 方法来检索这些特性。
var obj={
name:'lwj'
}
Object.defineProperty(obj,'age',{
value:18,
enumerable:true,
writable:true,
configurable:true
})
console.log(Object.getOwnPropertyDescriptor(obj,'name')) ===> descriptor
descriptor : {value: "lwj", writable: true, enumerable: true, configurable: true}
let proxy=new Proxy({},{
defineProperty(trapTarget,key,descriptor){
//此处可以做拦截 比如:
// if (typeof key === "symbol") { return false; }
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget,key){
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
})
Object.defineProperty(proxy,'age',{
value:18
})
console.log(proxy.age) //18
let descriptor = Object.getOwnPropertyDescriptor(proxy, "age");
console.log(descriptor.value);//18
ownKeys 陷阱函数
这个函数比较大哥,相当于调用了Object.keys() 方法、 Object.getOwnPropertyNames() 方法、 Object.getOwnPropertySymbols() 方法与 Object.assign() 方法
let proxy=new Proxy({},{
ownKeys(trapTarget){
//返回一个数组
return Reflect.ownKeys(trapTarget).filter( key =>{ //这里的key是返回的每一项
return typeof key!=='string'||key[0]!=='_' //过滤 带下划线的属性
});
}
})
let nameSymbol = Symbol("name");
//增加属性
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
//获取返回数组
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names) //name
console.log(keys) //name
console.log(symbols)//[Symbol(name)]
使用 apply 与 construct 陷阱函数的函数代理
在所有的代理陷阱中,只有 apply 与 construct 要求代理目标对象必须是一个函数
函数被调用是执行 apply 陷阱函数
trapTarget :被执行的函数(即代理的目标对象);
thisArg :调用过程中函数内部的 this 值;
argumentsList :被传递给函数的参数数组。
当使用 new 去执行函数时, construct 陷阱函数会被调用并接收到下列两个参数:
trapTarget :被执行的函数(即代理的目标对象);
argumentsList :被传递给函数的参数数组。
let target=function(){return 1}
let proxy=new Proxy(target,{
apply(trapTarget,thisArg,argumentList){
return Reflect.apply(trapTarget,thisArg,argumentList);
},
construct(trapTarget,argumentList){
return Reflect.construct(trapTarget,argumentList)
}
})
// 使用了函数的代理,其目标对象会被视为函数
console.log(typeof proxy); // "function"
console.log(proxy()); // 1
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
apply 与 construct 陷阱函数在函数的执行方式上开启了很多的可能性。例如,假设你想要 保证所有参数都是某个特定类型的,可使用 apply 陷阱函数来进行验证:
function sum(...values){
return values.reduce((prev,current)=>prev+current)
}
let sumProxy=new Proxy(sum,{
apply(trapTarget,thisArg,argumentList){
argumentList.forEach(el=> {
//参数必须为num
if(typeof el!=='number'){
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget,thisArg,argumentList);
},
construct(trapTarget,argumentList){ //不允许创建实例对象
throw new TypeError("This function can't be called with new.");
}
})
console.log(sumProxy(1, 2, 3, 4)); // 10
console.log(sumProxy(1, "2", 3, 4));//报错
let result = new sumProxy();//报错
同样可以再construc陷阱函数中对argumentList进行限制
ES6为我们提供了一个元属性(元属性指的是“非对象”(例如 new ) 上的一个属性) new.target,主要可以限制构造函数必须使用new
function Person(name){
if(typeof new.target!=='undefined'){
this.name=name //使用了new
}else{
throw new Error("You must use new with Person.")
}
}
var person = new Person("lwj");
var notAPerson = Person('age') //报错
一般情况下构造函数是必须带new的
在new一个实例的过程中,其实是执行了如下的步骤
1、声明一个中间对象
2、将该中间对象的原型指向构造函数的原型
3、将构造函数中的this指向该中间对象
4、返回该中间对象,即返回实例对象
现在可以利用apply与construct陷阱函数使其无需new也可以
function Num(...values){ //构造函数首字母大写
if(typeof new.target==='undefined'){
throw new TypeError("This function must be called with new.");
}
this.values=values
}
let NumbersProxy=new Proxy(Num,{
apply(trapTarget,thisArg,argumentList){
return Reflect.construct(trapTarget,argumentList)//在函数调用的时候执行 construct函数
}
})
let instance = NumbersProxy(1, 2, 3, 4); //未报错
console.log(instance.values); // [1,2,3,4]
同样的道理可以return new trapTarget(...argumentList);
可被撤销的代理
Proxy.revocable() 方法来创建一个可被撤销的代理,该方法接受的参数与 Proxy 构造器的相同:一个目标对象、一个代理处理器,而返回值是包含下列属性的一个对 象:
proxy :可被撤销的代理对象;
revoke :用于撤销代理的函数。
revoke() 函数被调用后,就不能再对该 proxy 对象进行更多操作
let target={
name:'lwj'
}
//解构赋值操作
let{proxy,revoke}=Proxy.revocable(target,{})
console.log(proxy.name); // "target"
revoke();
console.log(proxy.name);//报错
解决一个数组的小问题
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
当 colors[3] 被赋值时, length 属性被自动增加到 4 ;
当 length 属性被设置为 2 时,数组的最后两个元素被自动移除了。
当想要重现内置数组的工作方式时,仅需模拟这两个行为即可。
检测数组的索引
数组最多可以包含43亿左右的项(4 294 967 295),然而在JavaScript中数组的索引不能超过一个数,也就是2的32次方-1,只有小于等于这个数才能作为数组的索引(最大的安全整数是2的53次方-1,也就是Number.MAX_SAFE_INTEGER)
//检索是否符合数组索引 可以则 isArrayIndex(key)返回true
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2,32)
}
function isArrayIndex(key) {
let numerickey=toUint32(key)
return String(numerickey)==key && numerickey<(Math.pow(2,32)-1)
}
模拟类似的操作
//检索是否符合数组索引
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2,32)
}
function isArrayIndex(key) {
let numerickey=toUint32(key)
return String(numerickey)==key && numerickey<(Math.pow(2,32)-1)
}
function createMyArray(length=0){
//缩写{length:length}
return new Proxy({length},{
set(trapTarget,key,value){
let currentLength = Reflect.get(trapTarget, "length");
//特殊情况
if (isArrayIndex(key)){
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1); //未做其他处理,只是递增
}
}
return Reflect.set(trapTarget, key, value);
}
})
}
let colors = createMyArray(3); //length:3
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
console.log(colors); // Proxy {0: "red", 1: "green", 2: "blue", length: 3}
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
其他情况可以自己模拟一下!!!
网友评论