第八章: Proxy & Reflection-灵析社区

懒人学前端

一、谈谈 Object.defineProperty 与 Proxy 的区别?

要谈两者区别,我们先来了解一下 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 都能够被监听到,而且除了除了常用的 getset 操作外,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 有什么用?

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 就会自动执行。我们需要实现观察者模式,即实现 observeobservable函数。

下面,我们用 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