2022-09-26JavaScript00
请注意,本文编写于 576 天前,最后修改于 576 天前,其中某些信息可能已经过时。

一、Reflect

ES6 提供了一个全新的 API-Reflect,Reflect 和 Proxy 是相对的,我们可以用 Reflect 操作对象。

1.1 Reflect 存在的意义

  • 1)将 Object 对象一些内部的方法,放到 Reflect 对象上。比如 Object.defineProperty

现阶段这些方法存在于 object 和 Reflect 对象上,未来只存在于 Reflect 对象上。

意义:也就是说,从 Reflect 对象上可以拿到语言内部的方法。

  • 2)操作对象时出现报错返回 false

比如:Object.defineProperty(obj,name,desc) 在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回 false,这样会更合理一些。

// 旧写法
try {
  Object.defineProperty(target, property, attributes);
} catch (err) {
  //failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  //success
} else {
  //failure
}
  • 3)让操作对象的编程变为函数式编程

说明:老写法有的是命令式编程,比如下面这个例子

// 老写法
"assign" in Object; // true

// 新写法
Reflect.has(Object, "assign"); //true
  • 4)保持和 Proxy 对象的方法一一对应

说明:Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。

Proxy(target, {
  set: function (target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
      console.log("property" + name + "on" + target + "set do " + value);
    }
    return success;
  },
});

这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

综上所述:Reflect 对象有 4 个意义:

  • 从 Reflect 对象上可以拿到语言内部的方法。
  • 操作对象出现报错时返回 false
  • 让操作对象都变为函数式编程
  • 保持和 proxy 对象的方法一一对应

1.2 Reflect 常用 API

静态方法

Reflect 一共有 13 个静态方法

它可以一部分是原来在 Object 上的方法,将它转移到了 Reflect 上,并做了小改动,让方法更加合理。

  • Reflect.defineProperty(target,name,desc)Object.defineProperty(target,name,desc)类似,当时对象无法定义时,Object.defineProperty 会报错,而 Reflect.defineProperty 不会,它会返回 false,成功时返回 true,如果不是对象还是会报错。
  • Reflect.getPrototypeOf(target)Object.getPrototypeOf 一样,返回指定的对象的原型。
  • Reflect.setProtoTypeOf(target,prototype)Object.setPrototypeOf(target,prototype) 一样,它将指定对象的原型设置为另一个对象。
  • Reflect.getOwnPropertyDescriptor(target,name)Object.getOwnPropertyDescriptor(target,name) 一样,如果在对象中存在,则返回给定的属性的属性描述符。
  • Reflect.isExtensible(target)Object.isExtensible(target) 类似,判断一个对象是否可扩展(是否可以在它上面添加新的属性),它们的不同点是,当参数不是对象时(原始值),Object 将它强制转换为一个对象,Reflect 直接报错。
  • Reflect.preventExtensions(target)Object.preventExtensions(target) 类似,阻止新属性添加到对象,不同点和上一条一样。
  • Reflect.apply(target,thisArg,args)Function.prototype.apply.call(fn,obj,args) 一样
  • Reflect.ownKeys(target)Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) 一样,返回一个包含所有自身属性(不包含继承属性)的数组。

另一部分是将原来操作符的功能,变成函数行为

  • Reflect.has(target,name)in 操作符一样,让判断操作都变成函数行为
  • Reflect.deleteProperty(target,name)delete 操作符一样,让删除操作变成函数行为,返回布尔值代表成功或者失败
  • Reflect.construct(target,args[,newTarget]) new 操作符一样,target 构造函数,第二参数是构造函数的参数类数组,第三个是 new.target 的值。
  • Reflect.get(target,name[,receiver])Obj[key] 一样,第三个参数是当要取值的 key 部署了 getter 时,访问其函数的 this 绑定为 receiver 对象。
  • Reflect.set(target,name,value[,receiver]) 设置 target 对象的 key 属性等于 value,第三个参数和 set 一样。返回一个布尔值。
// 老写法
"assign" in Object; // true
// 新写法
Reflect.has(Object, "assign"); // true

// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]); // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]); // 1

// 旧写法
delete myObj.foo;
// 新写法
Reflect.deleteProperty(myObj, "foo");

// new 的写法
const instance = new Greeting("张三");
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ["张三"]);

// 旧写法
Object.defineProperty(MyDate, "now", {
  value: () => Date.now(),
});
// 新写法
Reflect.defineProperty(MyDate, "now", {
  value: () => Date.now(),
});

Reflect.get(1, "foo"); // 报错
Reflect.get(false, "foo"); // 报错
Reflect.set(1, "foo", {}); // 报错
Reflect.set(false, "foo", {}); // 报错

var myObject = {
  foo: 1,
  bar: 2,
  get baz() {
    return this.foo + this.bar;
  },
};

var myReceiverObject = {
  foo: 4,
  bar: 4,
};

Reflect.get(myObject, "baz", myReceiverObject); // 8

ES6 为 new 命令引入了一个new.target属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或Reflect.construct()调用的,new.target会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error("必须使用 new 命令生成实例");
  }
}

二、Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 就像在目标对象之间的一个代理,任何对目标的操作都要经过代理。代理就可以对外界的操作进行过滤和改写。

Proxy 是构造函数,它有两个参数 targethandler

target是用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

handler 是一个对象,其属性是当执行一个操作时定义代理的行为函数。

var obj = new Proxy(
  {},
  {
    get: function (target, key, receiver) {
      console.log(`getting ${key}`);
      return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
      console.log(`setting ${key}`);
      return Reflect.set(target, key, value, receiver);
    },
  }
);
obj.count = 1; // setting count
++obj.count; // getting count setting count 2

Proxy 只有一个静态方法revocable(target, handler)可以用来创建一个可撤销的代理对象。两个参数和构造函数的相同。它返回一个包含了所生成的代理对象本身以及该代理对象的撤销方法的对象。

一旦某个代理对象被撤销,它将变的几乎完全不可用,在它身上执行任何的可代理操作都会抛出 TypeError 异常(注意,可代理操作一共有 14 种,执行这 14 种操作以外的操作不会抛出异常)。一旦被撤销,这个代理对象永远不可能恢复到原来的状态,同时和它关联的目标对象以及处理器对象将有可能被垃圾回收掉。调用撤销方法多次将不会有任何效果,当然,也不会报错。

var revocable = Proxy.revocable(
  {},
  {
    get(target, name) {
      return "[[" + name + "]]";
    },
  }
);
// revocable => {"proxy":proxy,"revoke":revoke}

var proxy = revocable.proxy;
proxy.foo; // "[[foo]]"
revocable.revoke(); // 执行撤销方法

proxy.foo; // TypeError
proxy.foo = 1; // TypeError
delete proxy.foo; // TypeError
typeof proxy; // "object" 因为typeof 不属于代理操作

handler 参数是代理函数对象,它一共支持 13 种拦截函数。和 Reflect 的相同。如果没有定义某种操作,那么这种操作会被转发到目标对象身上。

const proxy = new Proxy(
  {},
  {
    get: function (target, property, receiver) {
      return receiver;
      // receiver 总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。
    },
  }
);
proxy.getReceiver === proxy; // true

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。

const target = Object.defineProperties(
  {},
  {
    foo: {
      value: 123,
      writable: false,
      configurable: false,
    },
  }
);

const handler = {
  get(target, propKey) {
    return "abc";
  },
};

const proxy = new Proxy(target, handler);

proxy.foo;
// TypeError: Invariant check failed

apply 方法拦截函数的调用、call 和 apply 操作。

var target = function () {
  return "I am the target";
};
var handler = {
  apply: function () {
    return "I am the proxy";
  },
};

var p = new Proxy(target, handler);

p();
// "I am the proxy"

defineProperty 方法拦截了 Object.defineProperty 操作。

var handler = {
  defineProperty(target, key, descriptor) {
    return false;
  },
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = "bar"; // 不会生效
// defineProperty 方法返回 false,导致添加新属性总是无效。

注意,如果目标对象不可扩展(non-extensible),则 defineProperty 不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则 defineProperty 方法不得改变这两个设置。

getPrototypeOf方法主要用来拦截获取对象原型,会拦截以下这些操作:

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof

ownKeys 方法用来拦截对象自身属性读取的操作,会拦截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in

通过代理,你可以轻松地验证向一个对象的传值。

let validator = {
  set: function (obj, prop, value) {
    if (prop === "age") {
      if (!Number.isInteger(value)) {
        throw new Error("The age is not an integer");
      }
      if (value > 200) {
        throw new Error("The age seems invalid");
      }
    }
    // The default behavior to store the value
    obj[prop] = value;
  },
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100

person.age = "young";
// 抛出异常: Uncaught TypeError: The age is not an integer
person.age = 1000;
// 抛出异常: Uncaught RangeError: The age seems invalid

this 指向

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的 this 关键字会指向 Proxy 代理。

const target = {
  m: function () {
    console.log(this === proxy);
  },
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m(); // false
proxy.m(); //true
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

// getDate 方法只能在Date对象实例上面拿到,
// 如果this不是Date对象实例就会报错。
// 这时,this绑定原始对象,就可以解决这个问题
const target = new Date("2021-07-28");
const handler = {
  get(target, prop) {
    if (prop === "getDate") {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  },
};
const proxy = new Proxy(target, handler);
proxy.getDate(); // 28

本文作者:前端小毛

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!