代理与反射

属性描述符

Property Descriptor

Property Descriptor 属性描述符 是一个普通对象,用于描述一个属性的相关信息 通过Object.getOwnPropertyDescriptor(对象, 属性名)可以得到一个对象的某个属性的属性描述符

1const desc = Object.getOwnPropertyDescriptor(obj, "a");
2console.log(desc);
  • value:属性值
  • configurable:该属性的描述符是否可以修改
  • enumerable:该属性是否可以被枚举
  • writable:该属性是否可以被重新赋值

    Object.getOwnPropertyDescriptors(对象)可以得到某个对象的所有属性描述符

如果需要为某个对象添加属性时 或 修改属性时, 配置其属性描述符,可以使用下面的代码: Object.defineProperty(对象, 属性名, 描述符);

1const obj = {
2  a: 1,
3  b: 2,
4};
5Object.defineProperty(obj, "a", {
6  value: 3,
7  configurable: false, //描述符不可以修改
8  enumerable: false, //不可枚举
9  writable: false, //不可写
10});
11console.log(obj);
12for (const prop in obj) {
13  console.log(prop); //只有b,没有a了
14}
15obj.a = 10; //不可写
16console.log(obj);
17const props = Object.keys(obj); //得到对象的所有属性
18console.log(props);
19const values = Object.values(obj); //得到对象的所有值
20console.log(values);

Object.defineProperties(对象, 多个属性的描述符)

1Object.defineProperties(obj, {
2  a: {
3    value: 3,
4    configurable: false,
5    enumerable: false,
6    writable: false,
7  },
8});

存取器属性

属性描述符中,如果配置了 get 和 set 中的任何一个,则该属性,不再是一个普通属性,而变成了存取器属性。 get 和 set 配置均为函数,如果一个属性是存取器属性,则读取该属性时,会运行 get 方法,将 get 方法得到的返回值作为属性值;如果给该属性赋值,则会运行 set 方法。 属性已经不再内存里面了,而是 get 和 set

1const obj = {
2  b: 2,
3};
4Object.defineProperty(obj, "a", {
5  get() {
6    console.log("运行了属性a的get函数");
7  },
8  set(val) {
9    console.log("运行了属性a的set函数", val);
10  },
11});
12// obj.a的时候运行get函数
13// obj.a赋值的时候运行set函数
14// 所以console.log(obj.a)//undefined,因为get函数没有返回结果
15obj.a = 1; //相当于set(20)
16console.log(obj.a); // 相当于console.log(get())
17// 运行set
18// 运行get
19// undefined
1// 演示1
2obj.a = 20 + 10; // set(20 + 10) 相当于运行set()
3console.log(obj.a); // console.log(get()) 读取属性a,相当于运行get()
4// 演示2
5obj.a = obj.a + 1; // set(obj.a + 1) 相当于读取 set(get() + 1)  undefined+1=NaN
6console.log(obj.a); //读取属性一定运行get  undefined
1const obj = {
2  b: 2,
3};
4Object.defineProperty(obj, "a", {
5  get() {
6    console.log("运行了属性a的get函数");
7    return obj._a;
8    // 错误的思路:return obj.a
9    // 这里就成了无限递归,无限读取,导致浏览器卡死,同理set
10  },
11  set(val) {
12    console.log("运行了属性a的set函数", val);
13    obj._a = val;
14  },
15});
16obj.a = 10;
17console.log(obj.a);

存取器属性最大的意义,在于可以控制属性的读取和赋值

1obj = {
2  name: "adsf",
3};
4Object.defineProperty(obj, "age", {
5  get() {
6    return obj._age;
7  },
8  set(val) {
9    if (typeof val !== "number") {
10      throw new TypeError("年龄必须是一个数字");
11    }
12    if (val < 0) {
13      val = 0;
14    } else if (val > 200) {
15      val = 200;
16    }
17    obj._age = val;
18  },
19});
20obj.age = "Asdfasasdf";
21console.log(obj.age);

应用:将对象的属性和元素关联

1<!DOCTYPE html>
2<html lang="en">
3  <head>
4    <meta charset="UTF-8" />
5    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
7    <title>Document</title>
8  </head>
9
10  <body>
11    <p>
12      <span>姓名:</span>
13      <span id="name"></span>
14    </p>
15    <p>
16      <span>年龄:</span>
17      <span id="age"></span>
18    </p>
19    <script>
20      const spanName = document.getElementById("name");
21      const spanAge = document.getElementById("age");
22      const user = {};
23      Object.defineProperties(user, {
24        name: {
25          get() {
26            return spanName.innerText;
27            //获取页面窗口中对应的元素内容
28          },
29          set(val) {
30            spanName.innerText = val;
31          },
32        },
33        age: {
34          get() {
35            return +spanAge.innerText;
36          },
37          set(val) {
38            if (typeof val !== "number") {
39              throw new TypeError("年龄必须是一个数字");
40            }
41            if (val < 0) {
42              val = 0;
43            } else if (val > 200) {
44              val = 200;
45            }
46            spanAge.innerText = val;
47          },
48        },
49      });
50    </script>
51  </body>
52</html>

属性描述符应用

1var obj = {
2  b: 2,
3};
4
5// 得到属性描述符
6// var desc = Object.getOwnPropertyDescriptor(obj, 'a');
7// console.log(desc);
8
9// 设置属性描述符
10Object.defineProperty(obj, "a", {
11  value: 10,
12  writable: false, // 不可重写
13  enumerable: false, // 不可遍历
14  configurable: false, // 不可修改描述符本身
15});
16// Object.defineProperty(obj, 'a', {
17//   writable: true,
18// });
19obj.a = "abc";
20console.log(obj.a);
21// for (var key in obj) {
22//   console.log(key);
23// }
24
25// var keys = Object.keys(obj);
26// console.log(keys);
27
28// console.log(obj);
1var obj = {};
2
3Object.defineProperty(obj, "a", {
4  get: function () {
5    return 123;
6  }, // 读取器 getter
7  set: function (val) {
8    throw new Error(
9      `兄弟,你正在给a这个属性重新赋值,你所赋的值是${val},但是,这个属性是不能复制,你再考虑考虑`
10    );
11  }, // 设置器 setter
12});
13
14console.log(obj.a);
15obj.a = "abx";
16// console.log(obj.a); // console.log(get())

购物车描述

1var aGoods = {
2  pic: ".",
3  title: "..",
4  desc: `...`,
5  sellNumber: 1,
6  favorRate: 2,
7  price: 3,
8};
9
10class UIGoods {
11  get totalPrice() {
12    return this.choose * this.data.price;
13  }
14
15  get isChoose() {
16    return this.choose > 0;
17  }
18
19  constructor(g) {
20    g = { ...g }; // 克隆一份
21    Object.freeze(g); // 冻结克隆对象
22    Object.defineProperty(this, "data", {
23      get: function () {
24        return g;
25      },
26      set: function () {
27        throw new Error("data 属性是只读的,不能重新赋值");
28      },
29      configurable: false,
30    });
31    var internalChooseValue = 0;
32    Object.defineProperty(this, "choose", {
33      configurable: false,
34      get: function () {
35        return internalChooseValue;
36      },
37      set: function (val) {
38        if (typeof val !== "number") {
39          throw new Error("choose属性必须是数字");
40        }
41        var temp = parseInt(val);
42        if (temp !== val) {
43          throw new Error("choose属性必须是整数");
44        }
45        if (val < 0) {
46          throw new Error("choose属性必须大于等于 0");
47        }
48        internalChooseValue = val;
49      },
50    });
51    this.a = 1;
52    Object.seal(this); // 密封自己 不能加属性了,其他属性还可以改
53  }
54}
55
56Object.freeze(UIGoods.prototype);
57
58var g = new UIGoods(aGoods);
59
60UIGoods.prototype.haha = "abc";
61// g.data.price = 100;
62
63console.log(g.haha);
64
65// 思考:能不能想到各种场景,提前预防

Reflect

Reflect 是什么?

Reflect 是一个内置的 JS 对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能 由于它类似于其他语言的反射,因此取名为 Reflect

它可以做什么?

使用 Reflect 可以实现诸如 属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中   等等功能

这些功能不是已经存在了吗?为什么还需要用 Reflect 实现一次?

有一个重要的理念,在 ES5 就被提出:减少魔法、让代码更加纯粹,这种理念很大程度上是受到函数式编程的影响

ES6 进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的 API,并高度聚合到某个对象中,于是,就造就了 Reflect 对象

因此,你可以看到 Reflect 对象中有很多的 API 都可以使用过去的某种语法或其他 API 实现。

它里面到底提供了哪些 API 呢?

  • Reflect.set(target, propertyKey, value): 设置对象 target 的属性 propertyKey 的值为 value,等同于给对象的属性赋值
1const obj = {
2  a: 1,
3  b: 2,
4};
5// obj.a = 10;
6Reflect.set(obj, "a", 10); //给obj的a赋值为10
7console.log(Reflect.get(obj, "a"));
  • Reflect.get(target, propertyKey): 读取对象 target 的属性 propertyKey,等同于读取对象的属性值
  • Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定 this 和参数列表。等同于函数调用
  • Reflect.deleteProperty(target, propertyKey):删除一个对象的属性
  • Reflect.defineProperty(target, propertyKey, attributes):类似于 Object.defineProperty,不同的是如果配置出现问题,返回 false 而不是报错
  • Reflect.construct(target, argumentsList):用构造函数的方式创建一个对象
  • Reflect.has(target, propertyKey): 判断一个对象是否拥有一个属性
  • 其他 API
1function method(a, b) {
2  console.log("method", a, b);
3}
4// method(3, 4);
5Reflect.apply(method, null, [3, 4]);
6
7const obj = {
8  a: 1,
9  b: 2,
10};
11// delete obj.a;
12Reflect.deleteProperty(obj, "a");
13console.log(obj);
14
15function Test(a, b) {
16  this.a = a;
17  this.b = b;
18}
19
20// const t = new Test(1, 3);
21const t = Reflect.construct(Test, [1, 3]);
22console.log(t);
1const obj = {
2  a: 1,
3  b: 2,
4};
5
6// console.log("a" in obj);
7console.log(Reflect.has(obj, "a"));

Proxy

提供了修改底层实现的方式

1//代理一个目标对象
2//target:目标对象
3//handler:是一个普通对象,其中可以重写底层实现(可以重写反射里面所有的api)
4//返回一个代理对象
5new Proxy(target, handler);
1const obj = {
2  a: 1,
3  b: 2,
4};
5
6const proxy = new Proxy(obj, {
7  set(target, propertyKey, value) {
8    // console.log(target, propertyKey, value);
9    // target[propertyKey] = value;修改属性的值。重写了底层实现
10
11    // 等价于
12    Reflect.set(target, propertyKey, value);
13  },
14  get(target, propertyKey) {
15    if (Reflect.has(target, propertyKey)) {
16      return Reflect.get(target, propertyKey);
17    } else {
18      return -1;
19    }
20  },
21  has(target, propertyKey) {
22    return false; //代理说没有就没有
23  },
24});
25// console.log(proxy);
26// proxy.a = 10;
27// console.log(proxy.a);
28
29console.log(proxy.d); //运行get
30console.log("a" in proxy);

应用

观察者模式

有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事。

vue2

1<div id="container"></div>
2<script>
3  //创建一个观察者
4  function observer(target) {
5    //通过观察目标元素的变化,把这些属性渲染到页面上
6    const div = document.getElementById("container");
7    const ob = {};
8    const props = Object.keys(target); // 拿到所有属性名
9    for (const prop of props) {
10      Object.defineProperty(ob, prop, {
11        get() {
12          // 获取的时候
13          return target[prop];
14        },
15        set(val) {
16          // 设置的时候
17          target[prop] = val;
18          render();
19        },
20        enumerable: true, //这两个属性默认不能被枚举
21      });
22    }
23    render();
24    function render() {
25      let html = "";
26      for (const prop of Object.keys(ob)) {
27        //拼接属性名属性值
28        html += `
29<p><span>${prop}:</span><span>${ob[prop]}</span></p>
30`;
31      }
32      div.innerHTML = html;
33    }
34    return ob;
35  }
36  const target = {
37    a: 1,
38    b: 2,
39  };
40  const obj = observer(target);
41</script>

缺陷:搞出来了两个对象,占用了内存,代理不占用内存

vue3

1<div id="container"></div>
2
3<script>
4  //创建一个观察者
5  function observer(target) {
6    const div = document.getElementById("container");
7    const proxy = new Proxy(target, {
8      set(target, prop, value) {
9        // 重写底层实现
10        Reflect.set(target, prop, value);
11        render();
12      },
13      get(target, prop) {
14        return Reflect.get(target, prop);
15      },
16    });
17    render();
18    function render() {
19      let html = "";
20      for (const prop of Object.keys(target)) {
21        html += `
22<p><span>${prop}:</span><span>${target[prop]}</span></p>
23`;
24      }
25      div.innerHTML = html;
26    }
27    return proxy;
28  }
29  const target = {
30    a: 1,
31    b: 2,
32  };
33  const obj = observer(target);
34</script>

偷懒的构造函数

恶心的类创建

1class User(){
2  constructor(firstName, lastName, age) {
3    this.firstName = firstName;
4    this.lastName = lastName;
5    this.age = age;
6  }
7}

自动赋值

1class User {}
2function ConstructorProxy(Class, ...propNames) {
3  return new Proxy(Class, {
4    construct(target, argumentsList) {
5      const obj = Reflect.construct(target, argumentsList);
6      propNames.forEach((name, i) => {
7        obj[name] = argumentsList[i];
8      });
9      return obj;
10    },
11  });
12}
13const UserProxy = ConstructorProxy(User, "firstName", "lastName", "age");
14const obj = new UserProxy("付", "志强", 18);
15console.log(obj);
16class Monster {}
17const MonsterProxy = ConstructorProxy(
18  Monster,
19  "attack",
20  "defence",
21  "hp",
22  "rate",
23  "name"
24);
25const m = new MonsterProxy(10, 20, 100, 30, "怪物");
26console.log(m);

可验证的函数参数

1function sum(a, b) {
2  return a + b;
3}
4
5function validatorFunction(func, ...types) {
6  //并不占据内存空间,只是个代理
7  const proxy = new Proxy(func, {
8    apply(target, thisArgument, argumentsList) {
9      types.forEach((t, i) => {
10        // 进行验证
11        const arg = argumentsList[i];
12        if (typeof arg !== t) {
13          throw new TypeError(
14            `第${i + 1}个参数${argumentsList[i]}不满足类型${t}`
15          );
16        }
17      });
18      return Reflect.apply(target, thisArgument, argumentsList);
19    },
20  });
21  return proxy;
22}
23
24const sumProxy = validatorFunction(sum, "number", "number");
25console.log(sumProxy(1, 2));

Proxy:如果没有我

1function sum(a, b) {
2  return a + b;
3}
4
5function validatorFunction(func, ...types) {
6  return function (...argumentsList) {
7    // 新的函数占用内存
8    types.forEach((t, i) => {
9      const arg = argumentsList[i];
10      if (typeof arg !== t) {
11        throw new TypeError(
12          `第${i + 1}个参数${argumentsList[i]}不满足类型${t}`
13        );
14      }
15    });
16    return func(...argumentsList); //运行这个函数
17  };
18  return proxy;
19}
20
21const sumProxy = validatorFunction(sum, "number", "number");
22console.log(sumProxy(1, 2));

其他 proxy 资料:

https://cloud.tencent.com/developer/article/1890562

https://juejin.cn/post/6844904101218631694