JavaScript必备知识点之Object.defineProperty与es6Proxy代理二三事(二)
上次我们了解了 Object.defineProperty。defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。属性描述符里面有 get,和 set 方法提供 geeter 和 setter。
认识 proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。let p = new Proxy(target, handler);
参数 target:用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。参数 handler:属性是当执行一个操作时定义代理的行为函数的对象。
如果 handler 参数没有设置任何拦截,就等同于直接通向原对象:
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "a";
target.name = "a";
handler
对象用于定义拦截行为。有以下一些拦截操作:
get(target,propKey,value)
方法用于拦截(读取属性)对目标对象属性的访问请求,比如proxy.name
proxy['name']
。
let proxy = new Proxy(
{},
{
get(target, property) {
return 1;
}
}
);
proxy.name; // 1
proxy.age; // 1
无论对 proxy 添加 name 还是 age 属性,返回都是 1。
是对 Proxy 的实例进行操作,而不是{}
let obj = {
name: "cuihua"
};
let proxy = new Proxy(obj, {
get(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError(`${property} does not exist`);
}
}
});
proxy.name; // cuihua
proxy.age; //age does not exist
访问普通对象上不存在的属性会报错undefined
,使用 proxy 我们可以自定义错误信息,如上述自定义没有属性则抛出错误。
我们可以利用 proxy 实现链式操作:
let pipe = (() => {
// 立即执行
return value => {
let funcStack = [];
let oproxy = new Proxy(
{},
{
get(target, property) {
// 拦截读取事件,pipe(3).double,那么可以拦截到property==='double' --> reverseInt.get可以拦截到 property==='get'
if (property === "get") {
// 当我们调用到get方法时
return funcStack.reduce((val, fn) => {
// 累加执行函数
return fn(val);
}, value);
}
funcStack.push(window[property]); // 向funcStack数组添加get拦截到的方法
return oproxy;
}
}
);
return oproxy;
};
})();
let double = n => n * 2; //double函数 3*2
let pow = n => n * n; // pow 6*6
let reverseInt = (
n // 36转化为字符串并分割成'3''6',然后反转成 '6''3',
) =>
n
.toString()
.split("")
.reverse()
.join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
利用 get 拦截生成各种 DOM 节点的通用函数:
const dom = new Proxy(
{},
{
get(target, property) {
// 拦截读取,如:'dom.div' 'dom.a' 'dom.li'
// 返回一个函数,第一个参数存放标签的属性,第二个参数表示子节点
return (attrs = {}, ...children) => {
const el = document.createElement(property); //创建以拦截到的property('div'/'ul'/'li')值为名称的标签
for (let prop of Object.keys(attrs)) {
el.setAttribute(prop, attrs[prop]); // 循环拿到attrs(标签属性名),设置属性和属性值
}
for (let child of children) {
if (typeof child === "string") {
// 循环children(子节点),如果子节点是字符串则以文本的形式写入标签
child = document.createTextNode(child);
}
el.appendChild(child); // 添加子节点
}
return el;
};
}
}
);
const el = dom.div(
{},
"My name is ",
dom.a({ href: "//example.com" }, "Mark"), // 创建a标签,href属性值为'//example.com',文本为'Mark'
". I like:",
dom.ul({}, dom.li({}, "The web"), dom.li({}, "Food"), dom.li({}, "so on...")) // 创建ul标签及子标签li
);
document.body.appendChild(el);
set(target,propKey,value)
方法用于拦截某个属性(写入)赋值操作。比如proxy.name = 'zhangsan'
proxy.age = 21
let proxy = new Proxy(
{},
{
set(target, propKey, value) {
if (propKey === "age") {
if (!Number.isInteger(value)) {
// 如果不是整数抛出错误
throw new TypeError("The age is not an integer");
}
if (value > 200) {
// 如果大于200抛出错误
throw new RangeError("The age seems invalid");
}
}
// 存储值
target[propKey] = value;
// 成功
return true;
}
}
);
proxy.name = "zhangsan";
proxy.age = 21;
// proxy.age = 201; // The age seems invalid
console.log(proxy.age); // 21
apply(target, that, args)
方法用于拦截函数的调用、call、apply 的操作。参数分别为:目标对象、目标对象的上下文(this)、目标对象的参数数组。比如let p = new Proxy(target, handler);p();
let p = new Proxy(
() => {
return "target";
},
{
apply() {
return "proxy";
}
}
);
console.log(p()); // proxy
console.log(p.call()); // proxy
console.log(p.apply()); // proxy
has(target, key)
方法用于拦截 HasProperty 操作,判断对象是否具有某个属性时生效。比如'_prop' in proxy
。
let proxy = new Proxy(
{
_prop: "foo",
prop: "foo"
},
{
has(target, key) {
if (key[0] === "_") {
// 如果是有_下划线则返回false,否则走正常判断逻辑
return false;
}
return key in target;
}
}
);
"_proxy" in proxy;
construct(target,args,newTarget)
方法用于拦截new
操作符。参数分别为目标对象、constructor 参数列表、最初被调用的构造函数,必须返回一个对象。比如new proxy(...args) / Reflect.construct()
。
let p = new Proxy(function() {}, {
construct(target, argumentsList, newTarget) {
console.log(`called: ${argumentsList.join(", ")}`);
return { value: argumentsList[2] * 10 }; // 必须返回一个对象
// return argumentsList[2] * 10 //
}
});
console.log(new p(1, 2, 3, 4, 5).value); // called: 1, 2, 3, 4, 5
// 30
Proxy 初始化时,传给它的 target 必须具有一个有效的 constructor 供 new 操作符调用:
var p = new Proxy(
{},
{
construct: function(target, argumentsList, newTarget) {
return {};
}
}
);
new p(); // TypeError is thrown 因为Proxy的第一个参数‘target’必须是能够用new操作符实例化的构造函数
上例 Proxy 的第一个参数‘target’值是{},如果把它改为()=>{}
箭头函数也是不行的,因为箭头函数不可以当做构造函数。
defineProperty(target,key,desc)
可以拦截Object.defineProperty操作,如属性描述符,定义一个新的 property。
let p = new Proxy(function() {}, {
defineProperty(target, key, desc) {
console.log(desc);
if (desc && "value" in desc) {
// 拦截属性描述符,如果描述符是"value"
console.log(".....");
}
return target;
}
});
p.my_cookie1 = "First haha";
总结
Object.defineProperty
:
- 1.会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
- 2.通过赋值操作添加的普通属性是可枚举的。
- 3.使用 Object.defineProperty() 添加的属性值是不可修改。
Proxy
:
Proxy 的功能更加强大,包含了 defineProperty,可以说 Object.defineProperty 是 Proxy 的子集。
- 1.用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
- 2.支持数组、不需要对对象的每个属性的 key 遍历。