共计 5317 个字符,预计需要花费 14 分钟才能阅读完成。
零、引言
参考文献
- 计算姬 的博客文章:《JavaScript 的 Proxy 详解》
- 阮一峰的《ECMAScript 6 入门》- 15.Proxy
纠正了一些谬误并重新整理编辑排版,仅供集体学习应用。
问题的起源
vue3.0 开始 Proxy 代替 Object.defineProperty,产生了一些列纳闷。
- Proxy 是什么?
- Proxy 能干什么?
- Vue 用 Object.defineProperty 干了什么?
- 为什么用 Proxy 代替 Object.defineProperty?
一、Proxy 是什么?
1.1 了解 Proxy
MDN 定义:Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
艰深的讲 Proxy 是一个对象操作的拦截器,拦挡对指标对象的操作,进行一些自定义的行为,一种分层的思维有点相似 spring 的 AOP。
1.2 Proxy 怎么用
let p = new Proxy(target, handler);
语法非常简单, 只有两个参数,很好了解
target
用 Proxy 包装的指标对象(能够是任何类型的对象,包含原生数组,函数,甚至另一个代理)。
handler
一个对象,其属性是当执行一个操作时定义代理的行为的函数。
var proxy = new Proxy({}, {get: function(target, property) {return 35;}
});
let obj = Object.create(proxy);
obj.time // 35
无操作转发代理
代理对象 p 会将所有利用到它的操作转发到指标对象 target 上,能够间接对代理对象进行操作,操作会转发到指标对象上。
let target = {};
let p = new Proxy(target, {});
p.a = 37; // 操作转发到指标
console.log(target.a); // 37. 操作曾经被正确地转发
1.3 能够拦挡的操作
一共有 13 种可代理操作,每种操作的代号(属性名 / 办法名)和触发这种操作的形式列举如下。留神,如果没有定义某种操作,那么这种操作会被转发到指标对象身上。
-
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()
当指标对象为函数,且被调用时触发。
-
handler.construct()
在给一个指标对象为构造函数的代理对象结构实例时触发该操作,比方在执行 new proxy() 时。
1.4 this 的指向
须要特地留神代理对象和拦挡操作中的 this 指向问题
一旦对象被代理之后,他的 this 就指向了代理对象
const target = {m: function () {console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true。指标函数的 this 指向 proxy
handler 定义的拦挡操作中 this 指向 handler,receiver 指向的才是 proxy
const target = {m: 100};
const handler = {get(target, property, receiver) {console.log(this === handler) // true。拦挡操作中的 this 指向 handler
console.log(receiver === proxy) // true。receiver 指向 代理对象 proxy
return target[property]
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.m)
二、Proxy 能干什么?
Proxy 各种罕用的拦挡都能干什么,罕用的四种操作
2.1 get()
get 办法用于拦挡某个属性的读取操作, 能够承受三个参数,顺次为指标对象、属性名和 proxy 实例自身。所有的属性调用都会进入同一个 get 即便是没有的属性。
- 能够发明一些原本没有的属性
- 能够在取数据时对数据进行验证和转换
- 能够自定义一些语法糖操作
- get 返回一个函数的话能够把一个属性转换成办法
var base = {
a: 100,
small: "hello world!!"
};
var proxy = new Proxy(base, {get(target, property, receiver) {
// 属性转换成函数
if ("fn" === property) {return function(value) {console.log(value);
};
}
if ("ghost" === property) {return "ghost";}
if ("fn" === property) {return function(value) {console.log(value);
};
}
// 自定义语法糖
if (property.includes("_")) {const direct = property.split("_")[1];
const propertyBase = property.split("_")[0];
switch (direct) {
case "Big":
return receiver[propertyBase].toLocaleUpperCase();
default:
break;
}
}
if (!(property in target)) {throw new ReferenceError('Property"' + property + '"does not exist.'); // 验证属性值
}
return target[property];
}
});
console.log(proxy.a)// 输入 100 失常拜访
proxy.fn("fn")// 输入 fn 属性转换为办法
console.log(proxy.small_Big)// 输入 HELLO WORLD!! 自定义语法糖
console.log(proxy.ghost)// 输入 ghost 创立属性
console.log(proxy.ghost_Big)// 输入 GHOST receiver 的应用
console.log(proxy.b)// 数据验证 抛出谬误
2.2 set()
set 办法用来拦挡某个属性的赋值操作,能够承受四个参数,顺次为指标对象、属性名、属性值和 Proxy 实例自身,其中最初一个参数可选。
- 能够用来验证属性是否符合要求
- 能够用来更改数据格式
- 能够用来监听数据更改事件
- 屏蔽一些赋值操作比方以 ”_” 结尾的公有变量
var base = {
a: 100,
small: "hello world!!"
};
const A_Change_Event = "aChangeEvent";
window.addEventListener(A_Change_Event, e => {console.log(e.data.value);
});
var proxy = new Proxy(base, {set(obj, property, value, receiver) {if ("a" === property) {if (!Number.isInteger(value)) {throw new TypeError("The" + property + "is not an integer");
}
if (value > 100) {throw new RangeError("The" + property + "seems invalid");
}
obj[property] = value;
// 事件
const dataChangeEvent = new Event(A_Change_Event); // 创立一个事件
dataChangeEvent.data = {value};
window.dispatchEvent(dataChangeEvent);
}
}
});
proxy.a = 80;
console.log(proxy.a);
proxy.a = 120;
2.3 apply()
函数对象也能够作为代理的指标对象
apply 办法拦挡函数的调用、call 和 apply 操作。
apply 办法能够承受三个参数,别离是指标对象、指标对象的上下文对象(this)和指标对象的参数数组。
最简略的例子
var target = function () { return 'I am the target';};
var handler = {apply: function () {return 'I am the proxy';}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
2.4 construct ()
construct 拦挡的是 new 操作
承受三个参数
- target:指标对象
- args:构造函数的参数对象
- newTarget:发明实例对象时,new 命令作用的构造函数
三、Vue 中的 Proxy 和 Object.defineProperty
3.1 Object.defineProperty 实现 observe
递归遍历所有属性,应用 Object.defineProperty 挨个定义监听
var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变动了 kindeng --> dmq
function observe(data) {if (!data || typeof data !== 'object') {return;}
// 取出所有属性遍历
Object.keys(data).forEach(function(key) {defineReactive(data, key, data[key]);
});
};
function defineReactive(data, key, val) {observe(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再 define
get: function() {return val;},
set: function(newVal) {console.log('哈哈哈,监听到值变动了', val, '-->', newVal);
val = newVal;
}
});
}
3.2 Proxy 实现 observe
不必提前挨个遍历绑定绑定。每次依据 key 来判断
observe(data) {
const that = this;
let handler = {get(target, property) {return target[property];
},
set(target, key, value) {let res = Reflect.set(target, key, value);
that.subscribe[key].map(item => {item.update();
});
return res;
}
}
this.$data = new Proxy(data, handler);
}
四、为什么用 Proxy 代替 Object.defineProperty?
vue3.0 应用了 Proxy 替换了原先遍历对象应用 Object.defineProperty 办法给属性增加 set,get 拜访器的蠢笨做法。也就是说不应遍历了,而是间接监控 data 对象了。