(深入JavaScript 八)defineProperty、Proxy、Reflect使用详细

283 阅读7分钟

defineProperty

作用

Object.defineProperty(obj,prop,descriptor)方法是在对象中定义或者修改一个属性,并且对该属性进行描述。它可以接收三个参数:

  1. obj:需要定义或修改属性的目标对象
  2. prop:需要定义或修改的属性名
  3. descriptor:需要定义和修改的属性描述符

属性描述符分类

属性描述符的类型分为两种:

  1. 数据属性(Data Properties)描述符(Descriptor)
  2. 存取属性(Accessor Properties)描述符(Descriptor)

image.png

数据属性描述符

  • Configurable:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符。当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true,当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false。
  • Enumerable:表示是否可以通过for-in或者Object.keys()遍历时返回该属性。当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true,当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false。
  • Writable:表示是否可以修改属性的值。当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true,当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false。
  • value:读取属性时会返回该值,修改属性时,会对其进行修改,默认情况下这个值是undefined。
let obj = {
  name: "aaa",
};

Object.defineProperty(obj, "num", {
  value: 10,
  writable: false,//不可修改
  enumerable: false,//不可遍历
  configurable: false,//不可删除
});

console.log(obj);//{ name: 'aaa' }

console.log(obj.num);//10
delete obj.num;
console.log(obj.num);//10
obj.num = 20;
console.log(obj.num);//10

存取属性描述符

在存取属性描述符中,也有Configurable和Enumerable属性,且与数据属性描述符一致,但存取属性描述符没有Writable和value属性,只有get和set属性:

  • get:获取属性时会执行的函数。默认为undefined
  • set:设置属性时会执行的函数。默认为undefined
let obj = {
  name: "aaa",
};

let num = 10;
Object.defineProperty(obj, "num", {
  enumerable: false,
  configurable: false,
  get: function () {
    return num;
  },
  set: function (newValue) {
    num = newValue;
  },
});

console.log(obj);//{ name: 'aaa' }

console.log(obj.num);//10
delete obj.num;
console.log(obj.num);//10
obj.num = 20;
console.log(obj.num);//20

注意:,数据属性描述符和存取属性描述符不能一起使用,即存在get或者set的时候不能使用value或者writable,不然会报错。

同时定义多个属性Object.defineProperties()

通过上面我们发现,Object.defineProperty()只能为对象定义一个属性,但js还为我们提供了一个方法Object.defineProperties()来同时定义或修改多个属性:

let obj = {
  _age: 10,
};

Object.defineProperties(obj, {
  name: {
    configurable: true,
    enumerable: true,
    writable: true,
    value: "aaa",
  },
  age: {
    configurable: true,
    enumerable: true,
    get: function () {
      return this._age;
    },
    set: function (value) {
      this._age = value;
    },
  },
});

obj.age = 20;
console.log(obj.age);//20
console.log(obj);//{ _age: 20, name: 'aaa', age: [Getter/Setter] }

Proxy

Proxy是一个类,用来创建一个代理对象。而代理对象顾名思义,就是对一个对象进行代理,以后我们对这个对象的操作就不用直接对该对象进行操作了(比如修改属性、新增属性、删除属性等),而是通过操作它的代理对象来间接的操作它本身属性。并且,代理对象还能监听原对象进行了哪些操作。

const objProxy = new Proxy(obj,trap)

如上所示,Proxy传入两个参数,第一个参数表示需要代理的对象,第二个参数表示需要添加的捕获器。而捕捉器的作用就是我们需要捕捉我们操作代理对象的某些操作。

Proxy的常用捕获器

const obj = {
  name: "aaa",
};
const objProxy = new Proxy(obj, {
  get: function (target, property, receiver) {
    console.log("获取属性捕获器");
    return target[property];
  },
  set: function (target, property, value, receiver) {
    console.log("修改属性捕获器");
    target[property] = value;
  },
  has: function (target, property) {
    console.log("存在属性捕获器");
    return property in target;
  },
  deleteProperty: function (target, property) {
    console.log("删除属性捕获器");
    delete target[property];
  },
});

console.log(objProxy.name);
objProxy.name = "bbb";
console.log("name" in objProxy);
delete objProxy.name;

控制台打印:

image.png

Proxy中apply和construct捕捉器

apply和construct捕捉器较为特殊,因为它们是作用于函数的捕捉器。注意,apply捕捉器并不是指捕捉调用函数的apply方法,而是代理函数一被调用就会触发apply捕捉器。而construct捕捉器是对于构造函数被new关键字调用时触发的。

function Foo(num1) {
  this.num = num1;
  console.log("foo");
}
const FooProxy = new Proxy(Foo, {
  //target:foo函数本身,thisArg:绑定的this对象,otherArgs,传入的函数参数
  apply: function (target, thisArg, otherArgs) {
    console.log("apply捕捉器");
  },
  //target:foo函数本身,argArray:传入的函数参数,newTarget:最初被调用的构造函数,就上面的例子而言是FooProxy
  construct: function (target, argArray, newTarget) {
    console.log("construct捕捉器");
    return new target(...argArray);
  },
});

FooProxy(10);
FooProxy.call(111, 10);

const obj = new FooProxy(100);
console.log(obj);

Proxy所有的捕获器

  • handler.getPrototypeOf():Object.getPrototypeOf 方法的捕捉器。
  • handler.setPrototypeOf():Object.setPrototypeOf 方法的捕捉器。
  • handler.isExtensible():Object.isExtensible 方法的捕捉器。
  • handler.preventExtensions():Object.preventExtensions 方法的捕捉器。
  • handler.getOwnPropertyDescriptor():Object.getOwnPropertyDescriptor 方法的捕捉器。
  • handler.defineProperty():Object.defineProperty 方法的捕捉器。
  • handler.ownKeys():Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols 方法的捕捉器。
  • handler.has():in 操作符的捕捉器。
  • handler.get():属性读取操作的捕捉器。
  • handler.set():属性设置操作的捕捉器。
  • handler.deleteProperty():delete 操作符的捕捉器。
  • handler.apply():函数调用操作的捕捉器。
  • handler.construct():new 操作符的捕捉器

Reflect

Reflect不像Proxy那样作为一个构造函数使用,它是一个对象,它本身的作用是提供操作JS对象来使用的,有点类似Object的方法。事实上,Reflect也包含了很多与Object一样的方法,像getPrototypeOf或者getOwnPropertyDescriptor等方法。

既然Object对象提供了这些方法,那么为什么还需要Reflect对象呢?这是因为在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面,但是Object作为一个构造函数,这些操作实际上放到它身上并不合适,另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪。所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上。总而言之,出现Reflect的原因就是因为有了更好的规范,对比Proxy内提供的13种触发器,你也会发现与Reflect中提供的方法一一对应,这样也会让我们开发者更方便的记忆和使用。

Proxy与Reflect结合使用

在上面介绍Proxy的get和set触发器时,可以发现,我们实际上返回和修改值的时候也是直接作用于对象本身的,这样不是很妥当,我们可以使用Relfect中的get和set来操作原对象:

const obj = {
  _name: "aaa",
  get name() {
    return this._name;
  },
  set name(newValue) {
    this._name = newValue;
  },
};

const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    console.log("get触发器", key, receiver);
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, newValue, receiver) {
    console.log("set触发器", key);
    Reflect.set(target, key, newValue, receiver);
  },
});

objProxy.name = "bbb";

上面我们还用到了receiver属性,在这里,receiver指代理对象本身。实际上,receiver的值是,如果target对象中指定了getterreceiver则为getter调用时的this值。因为这里是objProxy.name进行调用的,调用时触发get函数,这时因为隐式绑定,this指向objProxy,所以receiver就是objProxy。那么为什么这里我们这样使用呢Reflect.get(target, key, receiver)。这时为了将obj内的get方法的this指向我们的代理对象objProxy。我们上面虽然只访问了objProxy.name属性,但是看代码,我们实际上给的值是obj._name的值,obj._name又何尝不是获取属性应该触发get呢?但是this刚开始绑定的是obj,直接访问obj对象的属性是不会触发get触发器的,只有代理对象才能触发get触发器,所以我们需要将obj内的this指向为代理对象,使访问_name的时候是通过getter访问的代理对象的_name的值,那么就会触发get属性。

Reflect全部方法