背景

需求

image.png
比如Vue,如下,一个标签里面有个插值的变量name,Vue的响应式需要监听这个变量的变化,如果发生变化,就执行对应的操作,更新到页面上

  1. <template>
  2. <div>{{name}}</div>
  3. </template>

Object.defineProperty

image.png
Vue2 就是使用这种方式。
https://www.yuque.com/yejielin/mypn47/mkkfc6#y0Swp
image.png
通过设置对象属性特性的方式,添加设置值时执行(set)和读取值时执行(get),让变量改变时自动调用对应的方法
image.png

image.png

Proxy

image.png
Proxy的原理是,用一个类(中介)去代理对象,然后用户可以直接操作这个类,就相当于操作原对象,同时可以设定,什么操作触发会什么函数
image.png
Vue3 开始采用 Proxy 的方式监听对象属性的变化

===================

操作

1、创建代理

image.png
第二个参数是捕获器对象,就是你希望什么操作,给你执行什么函数,见下

2、操作代理

image.png image.png源对象被修改

3、创建并设置捕获器(Trap)

捕获器(Trap)会监听目标对象(target)的某些操作(get读取时、set设置值时,等等),执行一些方法(handler)
image.png
image.png image.png

编写捕获器,推荐使用Reflect反射,反射的方法正好与捕获器的方法一一对应。

4、receiver 参数

image.png
image.png(原本对象里面的this,指的是对象本身)

当我定义好一个Proxy代理,用户访问objProxy.name时,会正常触发get代理操作1次,而不是访问name触发1次同时_name触发1次,一共2次。
image.png

此时如果用到了 receiver 参数,这个参数是指代理对象(例子中的objProxy)本身,把obj里面的this改成了代理对象,配合Reflect反射,这样访问 objProxy.name 时,会触发2次get。
image.png

==================

Proxy的13种捕获器

编写捕获器,推荐使用Reflect反射,反射的方法正好与捕获器的方法一一对应。


说明 new Proxy( obj , { 捕获器名 }) 对代理后对象 objProxy 的操作
1 获取值 get( target , key ) objProxy.属性名
2 设置值 或 新增属性 set( target , key , newValue ) objProxy.属性名 = 新值
objProxy.新属性名 = 新值
3 是否有某个属性 has( target , key ) 属性名 in objProxy,返回布尔值
4 删除属性 deleteProperty( target, key ) delete objProxy.属性名
5 获取对象的key ownKeys( target ) Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys( objProxy )
Reflect.ownKeys( )
6 函数调用操作 apply( target , 被调用时的 this 对象 , 被调用时的参数数组 ) functionProxy( )
Function.prototype.apply( “functionProxy”, [ 参数1 , 参数2 ] )
Function.prototype.call( “functionProxy”, 参数1 , 参数2 )
Reflect.apply( functionProxy , this对象 , [ 参数1 , 参数2 ] )
7 函数被 new 时 construct( target, 参数列表 ) new functionProxy( 参数1 , 参数2 )
8 获取对象的原型 proto getPrototypeOf( target ) Object.getPrototypeOf( objProxy )
9 设置对象的原型 proto setPrototypeOf( target , newPrototype ) Object.setPrototypeOf( objProxy )
10 是否可以添加新属性 isExtensible( target ) Object.isExtensible( objProxy ),返回布尔值
11 禁止对象添加新属性 preventExtensions( target ) Object.preventExtensions( objProxy )
12 获取对象属性的描述符(属性特性) getOwnPropertyDescriptor( target , 属性的描述 ) Object.getOwnPropertyDescriptor( objProxy )
13 设置对象属性的描述符(属性特性) defineProperty( target , 属性的描述 ) Object.defineProperty( objProxy , prop , descriptor)

==================

示例

设置默认值

  1. const handler = {
  2. get: function(obj, prop) {
  3. return prop in obj ? obj[prop] : 37;
  4. }
  5. };
  6. const p = new Proxy({}, handler);
  7. p.a = 1;
  8. p.b = undefined;
  9. console.log(p.a, p.b); // 1, undefined
  10. console.log('c' in p, p.c); // false, 37

无操作转发代理

  1. let target = {};
  2. let p = new Proxy(target, {});
  3. p.a = 37; // 操作转发到目标
  4. console.log(target.a); // 37. 操作已经被正确地转发

验证属性的值

  1. //设置验证器对象,定义一个set属性,相当于让proxy拦截设置属性的方法,然后判断设置的属性有没有问题
  2. let validator = {
  3. set: function(obj, prop, value) {
  4. if (prop === 'age') {
  5. if (!Number.isInteger(value)) {
  6. throw new TypeError('The age is not an integer');
  7. }
  8. if (value > 200) {
  9. throw new RangeError('The age seems invalid');
  10. }
  11. }
  12. // The default behavior to store the value
  13. obj[prop] = value;
  14. // 表示成功
  15. return true;
  16. }
  17. };
  18. let person = new Proxy({}, validator);
  19. person.age = 100;
  20. console.log(person.age);
  21. // 100
  22. person.age = 'young';
  23. // 抛出异常: Uncaught TypeError: The age is not an integer
  24. person.age = 300;
  25. // 抛出异常: Uncaught RangeError: The age seems invalid

扩展构造函数

  1. function extend(sup, base) {
  2. var descriptor = Object.getOwnPropertyDescriptor(
  3. base.prototype, "constructor"
  4. );
  5. base.prototype = Object.create(sup.prototype);
  6. var handler = {
  7. construct: function(target, args) {
  8. var obj = Object.create(base.prototype);
  9. this.apply(target, obj, args);
  10. return obj;
  11. },
  12. apply: function(target, that, args) {
  13. sup.apply(that, args);
  14. base.apply(that, args);
  15. }
  16. };
  17. var proxy = new Proxy(base, handler);
  18. descriptor.value = proxy;
  19. Object.defineProperty(base.prototype, "constructor", descriptor);
  20. return proxy;
  21. }
  22. var Person = function (name) {
  23. this.name = name
  24. };
  25. var Boy = extend(Person, function (name, age) {
  26. this.age = age;
  27. });
  28. Boy.prototype.sex = "M";
  29. var Peter = new Boy("Peter", 13);
  30. console.log(Peter.sex); // "M"
  31. console.log(Peter.name); // "Peter"
  32. console.log(Peter.age); // 13

操作 DOM 节点

  1. let view = new Proxy({
  2. selected: null
  3. }, {
  4. set: function(obj, prop, newval) {
  5. let oldval = obj[prop];
  6. if (prop === 'selected') {
  7. if (oldval) {
  8. oldval.setAttribute('aria-selected', 'false');
  9. }
  10. if (newval) {
  11. newval.setAttribute('aria-selected', 'true');
  12. }
  13. }
  14. // 默认行为是存储被传入 setter 函数的属性值
  15. obj[prop] = newval;
  16. // 表示操作成功
  17. return true;
  18. }
  19. });
  20. let i1 = view.selected = document.getElementById('item-1');
  21. console.log(i1.getAttribute('aria-selected')); // 'true'
  22. let i2 = view.selected = document.getElementById('item-2');
  23. console.log(i1.getAttribute('aria-selected')); // 'false'
  24. console.log(i2.getAttribute('aria-selected')); // 'true'

值修正及附加属性

以下products代理会计算传值并根据需要转换为数组。
这个代理对象同时支持一个叫做 latestBrowser的附加属性,这个属性可以同时作为 getter 和 setter。

  1. let products = new Proxy({
  2. browsers: ['Internet Explorer', 'Netscape']
  3. }, {
  4. get: function(obj, prop) {
  5. // 附加一个属性
  6. if (prop === 'latestBrowser') {
  7. return obj.browsers[obj.browsers.length - 1];
  8. }
  9. // 默认行为是返回属性值
  10. return obj[prop];
  11. },
  12. set: function(obj, prop, value) {
  13. // 附加属性
  14. if (prop === 'latestBrowser') {
  15. obj.browsers.push(value);
  16. return;
  17. }
  18. // 如果不是数组,则进行转换
  19. if (typeof value === 'string') {
  20. value = [value];
  21. }
  22. // 默认行为是保存属性值
  23. obj[prop] = value;
  24. // 表示成功
  25. return true;
  26. }
  27. });
  28. console.log(products.browsers); // ['Internet Explorer', 'Netscape']
  29. products.browsers = 'Firefox'; // 如果不小心传入了一个字符串
  30. console.log(products.browsers); // ['Firefox'] <- 也没问题, 得到的依旧是一个数组
  31. products.latestBrowser = 'Chrome';
  32. console.log(products.browsers); // ['Firefox', 'Chrome']
  33. console.log(products.latestBrowser); // 'Chrome'

通过属性查找数组中的特定对象

  1. let products = new Proxy([
  2. { name: 'Firefox' , type: 'browser' },
  3. { name: 'SeaMonkey' , type: 'browser' },
  4. { name: 'Thunderbird', type: 'mailer' }
  5. ], {
  6. get: function(obj, prop) {
  7. // 默认行为是返回属性值, prop ?通常是一个整数
  8. if (prop in obj) {
  9. return obj[prop];
  10. }
  11. // 获取 products 的 number; 它是 products.length 的别名
  12. if (prop === 'number') {
  13. return obj.length;
  14. }
  15. let result, types = {};
  16. for (let product of obj) {
  17. if (product.name === prop) {
  18. result = product;
  19. }
  20. if (types[product.type]) {
  21. types[product.type].push(product);
  22. } else {
  23. types[product.type] = [product];
  24. }
  25. }
  26. // 通过 name 获取 product
  27. if (result) {
  28. return result;
  29. }
  30. // 通过 type 获取 products
  31. if (prop in types) {
  32. return types[prop];
  33. }
  34. // 获取 product type
  35. if (prop === 'types') {
  36. return Object.keys(types);
  37. }
  38. return undefined;
  39. }
  40. });
  41. console.log(products[0]); // { name: 'Firefox', type: 'browser' }
  42. console.log(products['Firefox']); // { name: 'Firefox', type: 'browser' }
  43. console.log(products['Chrome']); // undefined
  44. console.log(products.browser); // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
  45. console.log(products.types); // ['browser', 'mailer']
  46. console.log(products.number); // 3

拦截函数执行

image.png