要谈两者区别,我们先来了解一下 Objet.defineProperty()
和 Proxy
。
Objet.defineProperty()
Objet.defineProperty()
可以定义一个对象的属性或修改对象上已存在的属性,并返回这个对象。语法如下
Object.defineProperty(obj, prop, descriptor);
我们来了解一下第三个参数 descriptor
。它是属性描述符,MDN 中描述如下:
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。
数据描述符和存取描述符共享两个可选键值:
configurable
:当值为 true
时,该属性的描述符才能被改变,默认为 false
enumerable
:当值为 true
时,该属性才会出现在对象的枚举属性中,默认为 false
除上面两个属性外,数据描述符具有下面两个可选键值:
value
:该属性对应的值。默认为 undefined
writable
: 当值为 true 时,上面的 value
才能被修改,默认为 false
存取描述符具有下面两个可选键值:
get
:属性的 getter
函数,当访问该属性时,会调用此函数。如无 getter
,则为 undefined
set
:属性的 setter
函数,但属性值被修改时,会调用该函数。如无 setter
,则为 undefined
总的来说,数据描述符和存取描述符可拥有的键值概括如下:
拦截对象属性
此处,descriptor
我们使用 get
和 set
方法拦截对象的属性:
let o = {};
let value = "value";
Object.defineProperty(o, "b", {
get() {
// 获取 o.b 的值
console.log(`get ${value}`);
return value;
},
set(newValue) {
// 设置 o.b 的值
console.log(`set ${newValue}`);
value = newValue;
},
enumberable: true,
configurable: true
});
// 获取对象 o 的属性 b 的值
console.log(o.b);
// 依次输出:
// "get value"
// "value";
// 设置对象 o 的属性 b 的值为 value1
o.b = "value1"; // "set value1"
// 重新获取对象 o 的属性 b 的值
console.log(o.b);
// 依次输出:
// "get value1"
// "value1"
delete o.b; // 删除属性, 未触发 get、set 操作
console.log(o.b); // undefined
从上面的代码可以看出,我们通过 get
和 set
方法,可以检测到对象属性的变化,从而实现对象属性的监听。然而删除属性操作却并未触发 get
和 set
方法。
监听对象上的多个属性
如果我们监听对象上的多个属性的变化,就需要遍历对象:
let list = [1, 2, 3];
list.map((elem, index) => {
Object.defineProperty(list, index, {
get: function () {
console.log("get index:" + index);
return elem;
},
set: function (val) {
console.log("set index:" + index);
elem = val;
}
});
});
list[2] = 6; // 输出 "set index:2"
console.log(list[1]); // 依次输出: "get index:1" 2
list.push(4); // A 无输出
list[3] = 5; // B 无输出
console.log(list[3]); // 输出 5
list.length = 10; // C 无输出
虽然我们监听到了数组的变化,但是代码 A、B、C 处理想情况下,我们希望它触发 set 操作,但此处并没有输出。因此,操作数组的 push
方法、修改数组长度 length
都无法监听到正确结果。
数组的方法我们可以通过劫持 Array.prototype
上的方法做到:
const arrayMethods = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
const arrayProto = Object.create(Array.prototype);
arrayMethods.forEach((method) => {
const origin = Array.prototype[method];
arrayProto[method] = function () {
console.log("run method", method);
return origin.apply(this, arguments);
};
});
const list = [];
list.__proto__ = arrayProto;
list.push(2); // "run method" "push"
list.shift(3); // "run method" "shift"
Object.defineProperty
的缺陷
由上可知,Object.defineProperty
在劫持对象和数组时的缺陷:
Proxy
MDN 对 Proxy
的定义如下:
Proxy
对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
它的语法如下:
cosnt p = new Proxy(target, handler)
它的两个参数:
target
:需要代理的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理handler
:一个以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。这里列举几个 handler:
handler.get(target, property, receiver):
设置属性的捕捉器target
:是目标对象
property
:是目标属性名
receiver
: 如果目标属性是一个 getter
访问器属性,则 receiver
就是本次读取属性所在的 this 对象。
handler.set(target, property, value, receiver):
读取属性的捕捉器,如果写入成功,返回 true
,否则返回 false
target
:目标对象
property
:目标对象的属性
value
:目标对象属性的值
receiver
:同 get 捕捉器
handler.defineProperty()
:方法的捕捉器handler.has()
:in
操作符的捕捉器hander.apply()
:函数调用的捕捉器上文我们使用 Object.defineProperty
拦截对象的属性和方法,这里也可以使用 Proxy
来实现:
let o = {};
let value = 'value';
// 使用 Proxy 创建 对象 o 的代理对象 p
let p = new Proxy(o, {
get(target, property) {
// 获取对象 o 上的属性,有则返回该属性的值,如果没有则返回变量 value
return property in target ? target[property] : value;
},
set(target, property, value) {
// 设置对象 p 的 value
target[property] = value;
return true;
},
deleteProperty: function (target, propKey) {
// 删除对象 p 上的属性
console.log(`delete ${propKey}!`);
delete target[propKey];
return true;
}
});
// 获取对象 p 的属性 value 的 value
console.log(p.value); // "value";
// 设置对象 p 的属性 b 的值为 value1
p.b = 'value1'; // "value1"
console.log(p.b); // "value1"
// 对象 o 的方法也被改变了
console.log(o.value); // undefined
console.log(o.b); // "value1"
delete p.b; // "delete b!"
console.log(o.b); // "undefined"
可以看到,Proxy
直接代理了 target
整个对象,并且返回了一个新的对象;能监听到属性的增加、删除操作。
Proxy
还能代理数组:
let numbers = [];
let p = new Proxy(numbers, {
set(target, prop, val) { // 拦截写入属性操作
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
console.log("Length is: " + p.length, numbers.length); // Length is: 2 2
numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)
console.log("p is: " + p); // p is: 1,2,test
console.log("numbers is: " + numbers); // numbers is: 1,2,test
我们可以在控制台看到代理对象 p
:
数组的长度变化以及方法 push
都能够被监听到,而且除了除了常用的 get
、set
操作外,Proxy
更是支持 13 种拦截操作,上面只列出了部分,剩余的可以去 MDN 上查阅。
Proxy 中的 this
Proxy
中虽然完成了对目标对象的代理,但是即使 handler
为空对象,它代理的对象中的 this
指向的是代理对象,而不是目标对象。
let target = {
m() {
// 检查 this 的指向是不是 proxyObj
console.log(this === proxyObj)
}
}
let handler = {}
let proxyObj = new Proxy(target, handler)
proxyObj.m() // 输出: true
target.m() //输出: false
如果想要获取目标对象的 this
,可以使用 Reflect
,下文详细讲解。
总结
Reflect
是一个内置的对象,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy
的 handler
的方法相同。Reflect
的所有属性和方法都是静态的(就像 Math 对象)。
Reflect 有什么用?
Reflect
对象上挂载了很多静态方法,它有一些对应的函数或功能符,如下表:
我们可以使用 Reflect
操作数组:
const arr1 = [];
Reflect.set(arr1, 0, 'first');
Reflect.set(arr1, 1, 'second');
Reflect.set(arr1, 2, 'third');
console.log(arr1); // ["first","second","third"]
我们还可以搭配 Proxy
使用:
let obj = {};
const proxy = new Proxy(obj, {
get(target) {
console.log("get name");
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log("delete" + name);
return Reflect.deleteProperty(target, name);
}
});
proxy.name = "Joe";
console.log(proxy.name);
delete proxy.name;
执行结果如下:
返回值
如果我们把 obj
的 name
改为可读,configurable
默认是 false
,因此只设置 get
方法:
let obj = {}
Object.defineProperty(obj, name, {
get () {
return "Joe"
},
// configurable: false
});
console.log(obj.name = "Lily1"); // "Lily1"
console.log(Reflect.set(obj, name, "Lily")) // false
可以看到,Relfect
可以知道属性是否设置成功,而 Object.defineProperty
则不可以。
Reflect
的第三个参数 receiver
receiver
是接收者的意思,表示调用对应属性或方法的主体对象,通常情况下,receiver
无需传递,但是如果发生了继承,则需要明确调用主体,需要使用 receiver
。
let cat = {
_name: '中华田园猫',
get name () {
return this._name
}
}
let baiMao = new Proxy(cat, {
get (target, prop) {
return target[prop];
}
})
let xiaoBai = {
__proto__: baiMao,
_name: "小白"
}
console.log(xiaoBai.name); // "中华田园猫"
此处,我们希望输出结果是 "小白",应该传递 receiver
let cat = {
_name: '中华田园猫',
get name () {
return this._name
}
}
let baiMao = new Proxy(cat, {
get (target, prop, receiver) {
return Reflect.get(target, prop, receiver); // A
}
})
let xiaoBai = {
__proto__: baiMao,
_name: "小白"
}
console.log(xiaoBai.name); // "小白"
注意代码 A 处,我们使用 Reflect
并且传递了 receiver
。
Reflect
与传统方法的对比
那么,Reflect
与传统方法的优势在哪里呢?
应用
我们可以使用 Reflect
实现一个最简单的观察者模式。
观察者模式指得是函数自动观察数据对象,一旦对象变化,函数就会自动执行。
const person = observerable({
name: 'Zhange San',
age: 47,
});
function print() {
console.log(`${person.name}, ${person.age}`);
}
observe(print);
person.name = 'Li Si';
// 放回目标结果:
// Li Si, 47
const observe = (fn) => {
// 待实现
// ....
};
const observable = (obj) => {
// 待实现
// ...
};
上面代码中,person
是观察对象,函数 print 是观察者,一旦数据发生变化,print
就会自动执行。我们需要实现观察者模式,即实现 observe
和 observable
函数。
下面,我们用 queuedObservers
存放观察者们,observable
用于代理目标对象,监听并拦截 set
方法,一旦目标对象发生变化,则依次调用观察者们 queuedObservers
中的函数。
// 创建观察者队列
const queuedObservers = new Set();
// 添加观察对象时需要执行的函数
const observe = (fn) => queuedObservers.add(fn);
// 监听对象
const observable = (obj) => new Proxy(obj, { set });
// 监听对象的 set 操作
function set(target, key, value, receiver) {
// 设置对象属性的值
const result = Reflect.set(target, key, value, receiver);
// 观察对象改变时,执行添加的函数
queuedObservers.forEach((observer) => observer());
return result;
}
完整代码如下:
// 创建观察者队列
const queuedObservers = new Set();
// 添加观察对象时需要执行的函数
const observe = (fn) => queuedObservers.add(fn);
// 监听对象
const observable = (obj) => new Proxy(obj, { set });
// 监听对象的 set 操作
function set(target, key, value, receiver) {
// 设置对象属性的值
const result = Reflect.set(target, key, value, receiver);
// 观察对象改变时,执行添加的函数
queuedObservers.forEach((observer) => observer());
return result;
}
const person = observable({
name: "Zhange San",
age: 47
});
function print() {
console.log(`${person.name}, ${person.age}`);
}
observe(print);
person.name = "Li Si";
// Li Si, 47
小练习
如何实现一个简单的观察者模式?
答案:见上文。
阅读量:2013
点赞量:0
收藏量:0