ES6之Reflect和Proxy

一、Reflect

1. 概述

设计Reflect主要有以下四个目的(参考自阮一峰ECMAScript 6入门):

  1. 将Object上属于语言内部的方法部署到Reflect,并且以后新增的语言内部方法只会部署到Reflect上。如ES5中的Object.defineProperty方法在ES6中可以通过Reflect.defineProperty调用,并且前者在之后的版本中将不再被推荐。
  2. 规范某些Object方法的行为。如Object.defineProperty在无法定义属性时会抛出异常,而Reflect.defineProperty会返回false。
  3. 将命令式操作全部变成函数式操作,如delete obj.name,替换成了Reflect.deleteProperty(obj, name)。
  4. 为Proxy提供支持,以获取被代理的默认行为。因此Reflect上的静态方法与Proxy可以代理的操作是一一对应的(第二部分会介绍Proxy)。

总的来说,Reflect是Object上私有特性的载体,负责部署和规范Object上一些属于语言内部的方法(Reflect中文释义为“反射,表达”,在这里它就像Object的镜像,负责管理Object上的内部方法)。

2. Reflect的静态方法

Reflect共有13个静态方法,它们大都是来自于Object的同名方法,少量是由操作符如in、delete、new等演化而来:

  1. Reflect.get(target, name, receiver)
  2. Reflect.set(target, name, value, receiver)
  3. Reflect.apply(target, thisArg, args)
  4. Reflect.construct(target, args)
  5. Reflect.defineProperty(target, name, desc)
  6. Reflect.deleteProperty(target, name)
  7. Reflect.has(target, name)
  8. Reflect.ownKeys(target)
  9. Reflect.isExtensible(target)
  10. Reflect.preventExtensions(target)
  11. Reflect.getOwnPropertyDescriptor(target, name)
  12. Reflect.getPrototypeOf(target)
  13. Reflect.setPrototypeOf(target, prototype)

(1)Reflect.get(target, name, receiver)

查找target对象的name属性,如果不存在,则返回undefined。如果name属性定义了getter读取函数,则getter内的this绑定到第三个参数receiver。如:

var obj = {
  name: '张三',
  get age(){
    return this.age;
  }
}

var proxyObj = {
  age: 24
}

Reflect.get(obj, name);  // 张三
Reflect.get(obj, age, proxyObj);   // 24

获取name属性时,没有定义getter,因此直接返回属性值’张三’。获取age属性时,由于定义了getter,因此getter内部的this指向传入的第三个参数proxyObj,最终输出的是proxyObj的age属性:24。

这个特性主要用于Proxy(代理),第二部分会讲到。

(2)Reflect.set(target, name, value, receiver)

设置target对象的name属性,值为value。如果name属性设置了setter函数,则setter内部的this指向第四个参数receiver。如:

var obj = {
  name: '张三',
  set age(value){
    return this.age = value;
  }
}

var proxyObj = {
  age: 21
}

Reflect.set(obj, name, '李四');  //obj.name === 李四
Reflect.set(obj, age, 24);  //obj.age === 24

//age存在setter,并且传入了第四个参数proxyObj,
//因此setter的this被绑定到proxyObj
Reflect.set(obj, age, 22, proxyObj);
obj.age;  // 24
proxyObj.age;  // 22,这里的set只对proxyObj生效,没有改变obj的值

set的行为类似于get,receiver也是为proxy提供支持。

(3)Reflect.apply(target, thisArg, args)

等同于Function.prototype.apply.call(func, thisArg, args),也就是函数原型对象上的apply方法。

通常我们会这样调用apply方法:

function fun(args){ ... }
let obj = {}

fun.apply(obj, args);

假如fun自身又定义了apply方法(注意,函数也是对象,因此你可以给函数添加方法),为了调用其原型上的apply方法,就必须换成下面的写法:

function fun(args){ ... }
fun.apply = function(){ ... } //为fun添加实例方法apply
let obj = {}

Function.prototype.apply.call(func, thisArg, args)

使用Reflect.apply可以改成下面的规范形式:

Reflect.apply(fun, obj, args);

由于ES6新增了扩展运算符,所以call方法可以用apply来替代,因此Reflect上就没有提供对应的call方法。

(4)Reflect.construct(target, args)

new关键字的等价形式。即Reflect.construct方法等同于new target(…args)。

如下面的new关键字可以用Reflect.construct来代替:

function Student(name, age){
  this.name = name;
  this.age = age;
}

let student = new Student('张三', 24);
//等价于
let student = Reflect.construct(Student, ['张三'24]);

如果Reflect.construct接收的第一个参数不是函数就会报错。

(5)Reflect.defineProperty(target, name, desc)

源于Object.defineProperty,两者基本等价。不同的是,当被定义的属性无法被修改时(如配置了configurable为false),Object.defineProperty会抛出错误,而Reflect.defineProperty会返回false。

defineProperty允许修改的属性参数为:

Reflect.defineProperty({}, name, {
  configurable: true,  //是否可配置
  enumerable: true,   //是否可遍历
  writable: true,    //是否可写
  value: null,      //属性值
  get: function(){},  //getter方法
  set: function(){},  //setter方法
})

关于这些参数的含义,属于ES5的范畴,这里不再详述,感兴趣的请自行查阅。

(6)Reflect.deleteProperty(obj, name)

等价于delete关键字。Reflect.deleteProperty将其规范成了函数调用的形式。

如ES5的delete语句,可以写成下面的形式:

const obj = { name: '张三' }

delete obj.name;   // ES5写法

Reflect.deleteProperty(obj, name); // ES6写法

删除成功返回true,删除失败返回false。如果第一个参数不是对象会报错。

(7)Reflect.has(obj, name)

等价于ES5的in操作符。如:

const obj = { name: '张三' }

name in obj;   //true

Reflect.has(obj, name);  //true

如果has的第一个参数不是对象就会报错。

(8)Reflect.ownKeys(target)

等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。获取对象属性集合的几个方法如下:
在这里插入图片描述
使用Reflect.ownKeys可以获取对象自身所有的普通属性和Symbol属性(包括不可枚举属性)。如:

var myObject = {
  name: '张三',
  [Symbol.for('age')]: 24,
};

Reflect.ownKeys(myObject)
// ['name', Symbol(age)]

(9)Reflect.isExtensible(target)

基本等价于Object.isExtensible方法,判断对象是否可扩展(即是否可添加属性)。不同的是,如果传入的不是对象,Object.isExtensible会返回false,而Reflect.isExtensible则会报错。如:

const myObject = {};

Object.isExtensible(myObject) // true
Reflect.isExtensible(myObject) // true

Object.isExtensible(1) // false
Reflect.isExtensible(1) // 报错

(10)Reflect.preventExtensions(target)

基本等价于Object.preventExtensions,让一个对象变得不可扩展。如果参数不是对象,Object.preventExtensions在ES5环境会报错,在ES6环境会返回传入的参数,而Reflect.preventExtensions会报错。如:

var myObject = {};

// ES5写法
Object.preventExtensions(myObject) // Object {}
// ES6写法
Reflect.preventExtensions(myObject) // true

// ES5 环境
Object.preventExtensions(1) // 报错
// ES6 环境
Object.preventExtensions(1) // 1
// 新写法
Reflect.preventExtensions(1) // 报错

(11)Reflect.getOwnPropertyDescriptor(target, name)

基本等同于Object.getOwnPropertyDescriptor,用于获取对象属性的描述符对象,它包含上面defineProperty方法允许定义的那六个属性(configurable、writable、value等)。当传入的第一个参数不是对象时,使用Object调用会返回undefined,而使用Reflect调用则会抛出异常。

var myObject = {};
Object.defineProperty(myObject, 'hidden', {
  value: true,
  enumerable: false,
});

// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');

// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');

(12)Reflect.getPrototypeOf(target)

基本等同于Object.getPrototypeOf,用于获取对象的原型对象,类似于__proto__,不过该方法是标准方法,而__proto__不属于ES规范。当传入的参数不是对象时,Object.getPrototypeOf会先将其转化为对象的包装对象,再获取原型,而Reflect.getPrototypeOf会报错。

const myObj = new FancyThing();
// ES5写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;
// ES6写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;

Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 报错

(13)Reflect.setPrototypeOf(obj, newProto)

基本等价于Object.setPrototypeOf,用于设置对象的原型,它返回一个布尔值,表示是否设置成功。如:

const myObj = {};

// ES5写法
Object.setPrototypeOf(myObj, Array.prototype);
// ES6写法
Reflect.setPrototypeOf(myObj, Array.prototype);
myObj.length // 0

如果无法设置目标原型,该方法返回false。如果第一个参数不是对象,Object.setPrototypeOf会返回该参数,而Reflect.setPrototypeOf会报错。如果第一个参数是null或undefined,则两个方法都会报错。

二、Proxy

1. 基本原理

Proxy中文释义为“代理”,它的作用是在访问对象之前架设一层“拦截”,将对原对象的访问变成对代理对象的访问,代理对象可以根据需求自定义如何访问原对象。

下图是proxy的大致访问过程:
在这里插入图片描述
基于Proxy可以实现订阅者模式。即通过Proxy代理原对象的get和set方法,在get方法被触发时,收集订阅。之后如果触发了set方法(即值发生变化),就通知订阅者,从而执行某些操作(如更新视图)。这是Vue实现响应式系统的核心机制。在Vue 2.xx版本中这是借助Object.defineProperty实现的,但由于存在一定的性能问题,所以在3.0版本中替换为了Proxy实现。下面是一个订阅者模式的简单例子:

const person = observable({  //person得到的是该对象的代理对象
  name: '张三',
  age: 20
})

const queuedObservers = new Set();  //订阅者集合
const observe = fn => queuedObservers.add(fn);  //添加订阅者的方法

//将对象变为可观察的,思路为将其包装成一个proxy对象,并代理其set方法
function observable(obj){   
  const handler = {
    set: function(target, key, value, receiver){
      const result = Reflect.set(target, key, value, receiver);
      queuedObservers.forEach(observer => observer());
      return result;
    }
  }
  return new Proxy(obj, handler);
}

observe(function(){  //将该函数添加到订阅者集合,它会在person.set触发时被执行
  console.log(`${person.name}, ${person.age}`);
})
person.name = '李四';  //这会触发person的set方法,因此上述函数被触发
//李四,24

这里的person对象并不是原始对象{name:'张三',age:24},它是observable函数的返回值,即const person = new Proxy(obj, handler),因此它是原始对象的代理对象。

我们在handler中只配置了一个set方法,也就是只拦截了原始对象的set行为。当通过person.name = '李四'去修改对象的属性值时,就会触发这个set方法。在set方法中,我们首先通过Reflect.set(target, key, value, receiver)调用原始的set方法,完成默认的赋值行为,然后再依次调用订阅者集合中的函数,最后返回属性值。

注意:返回属性值不是必需的,但当你返回属性值时,它会作为person.name = '李四'这个表达式的值,这是set方法默认的行为,否则该表达式的值将为undefined。因此如果你没有返回属性值,它将不能用于连等式:

//没有返回属性值的情况
const name = person.name = "李四";
name;  // undefined,因为set方法没有返回值

如果你写了返回值,那么上面的连等式就没有问题,变量name的值会被正确设置为“李四”。

2. 语法介绍

Proxy总共支持13种拦截操作,与Reflect的13个静态方法一一对应。

在大多数情况下,我们配置拦截行为是为了在触发这些原生方法时执行某些额外的操作,而不希望改变他们原来的行为。但当我们拦截了某个方法时,默认方法就不会被执行。这时我们就可以在拦截器中通过Reflect手动调用默认方法,完成默认行为(如上面的Reflect.set就是原生的set方法,它可以完成默认的赋值行为)。

下面我们分别来看这13种拦截操作及常见的用法。

(1)get()

拦截对象的取值行为。它可以接收三个参数:目标对象、属性名和代理对象(也就是proxy本身,可选)。一个简单的例子如下:

const obj = {
  num: 123
}

const proxy = new Proxy(obj, {
  get: function(target, key, receiver){
    if(key in target){
      return target[key]
    } else {
      throw new ReferenceError(key + " not exist");
    }
  }
})

proxy.height;
// ReferenceError: height not exist

我们为obj配置的代理对象proxy拦截了取值行为,当访问不存在属性时就抛出错误。如果没有配置这样的拦截,访问不存在的属性时只会返回undefined。

拦截get行为不仅可以实现如此简单的功能,还可以实现如添加订阅者、链式调用等,详见阮一峰 ECMAScript 6入门

注意:只有通过proxy访问原对象,拦截行为才会生效。

(2)set()

set拦截对象属性的赋值行为,如proxy基本原理中所介绍的就是set的常见用法。set可以接受四个参数:目标对象、属性名、属性值和proxy实例本身,最后一个参数可选。下面的例子通过拦截set来验证属性值:

const validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200 || value < 0) {
        throw new RangeError('The age seems invalid');
      }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
}

const author = new Proxy({}, validator);
author.age = "carter"; // TypeError: The age is not an integer,年龄不是数字
author.age = 300; // RangeError: The age seems invalid,年龄无效
author.age = 24;  // ok

(3)apply()

拦截函数调用,包括通过call和apply的调用。由于ES6提供了扩展运算符,普通的函数调用和call方法的参数列表都可以很容易转换为数组,因此它们可以统一由apply来拦截。apply方法接收三个参数,分别是目标对象(即被调用的函数)、上下文对象(即this指向的对象)和参数数组。

下面的集中调用方式都可以被apply拦截:

function fun(arg1){
  console.log(arg1, this.num);
}

const proxyFun = new Proxy(fun, {
  apply: function(target, thisArg, args){
    console.log('代理函数触发!')
    return Reflect.apply(target, thisArg, args);  //参数列表可以简写为...arguments
  }
})

const obj = { num: 123 }

proxyFun(1);  
// 代理函数触发!
// 1, undefined,此时fun是在全局对象上调用的,而全局对象没有num属性,因此输出undefined

proxyFun.apply(obj, [1]);
// 代理函数触发!
// 1, 123

proxyFun.call(obj, 1);
// 代理函数触发!
// 1, 123

在这里,尽管apply传入的是参数数组,而普通函数调用和call传入的是参数列表,但代理对象的apply方法对三者的处理没有任何差别,因为它们都会被规范成参数数组传入代理对象。

(4)construct()

construct拦截通过new关键字构造对象的行为。它接收三个参数:构造函数、参数数组和代理对象本身(即proxy,由于此时的proxy实际上是函数对象,所以它是个代理函数)。如:

function Student(name, age){}

const proxyStu = new Proxy(Student, {
  construct: function(target, args, proxy){
    return {msg: "构造行为被拦截!"}
  }
})

const stu = new proxyStu('张三', 24);
stu;  // {msg: "构造行为被拦截!"}

可以看到,使用new关键字构造对象被拦截。如果你希望得到默认行为,可以通过Reflect.construct实现:

const proxyStu = new Proxy(Student, {
  construct: function(target, args, proxy){
    console.log('这里定义了执行new前的一些操作');
    return Reflect.construct(...arguments);
  }
})

(5)defineProperty()

拦截了defineProperty方法。借助该方法,你可以修改原对象某些属性的描述符,从而修改该属性的数据属性或访问器属性。但是如果该属性原本就不可写(writable:false)或不可配置(configurable:false),那么代理对象也无法改变这两个设置。它接收三个参数:目标对象、属性名和原描述符对象。如:

const obj = {name: '张三', age: 24};
const proxy = new Proxy(obj, {
  defineProperty: function(target, key, descriptor){
    if( key === 'name' ){  //将name属性置为不可写
      return Reflect.defineProperty(target, key, {
        writable: false
      })
    }
    return Reflect.defineProperty(...arguments);  //其余属性不变
  }
})

proxy.name = '李四';
proxy.age = 25;

proxy.name;  // 张三
proxy.age;   // 25

我们拦截了name属性的描述符,将其设置为{ writable: false },结果尝试修改name属性的值时发现它的值没有变化。而没有拦截的age属性则可以修改。

(6)deleteProperty()

拦截delete命令。如果该方法抛出错误或返回false,那么该属性将不会被删除。它接收两个参数:目标对象和目标属性。如:

const proxy = new Proxy({ _name: '张三', age: 24 }, {
  deleteProperty: function(target, key){
    if(key[0] === '_'){
      throw new Error("内部属性不可删除!");
    } else {
      delete target[key];
      return true;
    }
  }
})

delete proxy._name; // Error: 内部属性不可删除!
delete proxy.age;  // ok

这里我们为代理对象配置了deleteProperty拦截,如果属性是以“_”开头,则认为是内部属性,不允许删除;否则允许删除。

(7)has()

主要拦截in操作符。通过配置has拦截,可以让某些属性不被in操作符遍历到,如:

const proxy = new Proxy({ _name: '张三', age: 24 }, {
  has: function(target, key){
    if(key[0] === "_"){
      return false;
    } else {
      return true;
    }
  }
})

'_name' in proxy;  // false
'age' in proxy;   // true

我们在检测到属性以"_"开头时,就返回false,保证这样的属性不被in操作符遍历到。

(8)ownKeys()

ownKeys接收目标对象为参数,可以拦截获取对象属性的操作,包括:

  1. Object.getOwnPropertyNames()
  2. Object.getOwnPropertySymbols()
  3. Object.keys()
  4. for…in循环

如:

const obj = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.for('secret')]: '4',
}

const proxy = new Proxy(obj, {
  ownKeys: function(target){
    return ['a', 'b', 'd', Symbol.for('secret')];
  }
})

Object.getOwnPropertyNames(proxy); // ['a', 'b', 'd']
Object.getOwnPropertySymbols(proxy); // [Symbol.for('secret')]
Object.keys(proxy);  // ['a', 'b']
for(let prop in proxy){
  console.log(prop); // a b
}

需要特别注意的是,keys方法和for … in循环有一个自动过滤机制,他们不会返回ownKeys返回值的以下三类属性:

  1. 目标对象不存在的属性
  2. Symbol类型的属性
  3. 不可遍历的属性

比如上面的例子中,虽然ownKeys返回的列表包含四个属性名,但是d属性不存在于原对象,而Symbol.for('secret')是Symbol类型的,所以keys方法和for … in循环都没有输出它们。

另外,Object.getOwnPropertyNames只会输出列表中的字符串属性名,Object.getOwnPropertySymbols只会输出Symbol类型的属性名,而不管它们是不是真的存在于原对象上。

当然了,调用Reflect.ownKeys也会被拦截(其实调用自身应该算不上拦截,所以没有写到上述列表中)。如:

Reflect.ownKeys(proxy); // ['a', 'b', 'd', Symbol.for('secret')]

(9)isExtensible()

拦截object.isExtensible,输出拦截之后的值,接收的参数为被拦截对象。如:

var proxy = new Proxy({}, {
  isExtensible: function(target) {
    console.log("调用了isExtensible");
    return true;
  }
});

Object.isExtensible(proxy)
// 调用了isExtensible
// true

(10)preventExtensions()

拦截object.preventExtensions,接收的参数为被拦截对象。如:

var proxy = new Proxy({}, {
  preventExtensions: function(target) {
    console.log("该对象不可禁止扩展!");
    return false;
  }
});

Object.preventExtensions(proxy);
// 该对象不可禁止扩展!
// false

(11)getOwnPropertyDescriptor()

拦截Object.getOwnPropertyDescriptor,返回一个描述符对象或undefined。接收的参数为目标对象和属性名。

const proxy = new Proxy({_name: '张三', age: 24}, {
  getOwnPropertyDescriptor: function(target, key){
    if(key[0] === '_'){
      return;
    }
    return Reflect.getOwnPropertyDescriptor(target, key);
  }
})

Object.getOwnPropertyDescriptor(proxy, '_name'); // undefined
Object.getOwnPropertyDescriptor(proxy, 'age'); // {value: 24, writable: true, enumerable: true, configurable: true}

(12)getPrototypeOf()

拦截获取目标对象原型的行为,包括:

  1. Object.prototype.proto
  2. Object.prototype.isPrototypeOf()
  3. Object.getPrototypeOf()
  4. Reflect.getPrototypeOf()
  5. instanceof

如:

var proto = {};
var p = new Proxy({}, {
  getPrototypeOf(target) {
    return proto;
  }
});
Object.getPrototypeOf(p) === proto // true
p instanceof proto;  // true

该方法的返回值必须是对象或null,否则报错。如果目标对象不可扩展,该方法必须返回其原型对象,否则也会报错。

(13)setPrototypeOf()

setPrototypeOf拦截设置目标对象原型的操作。它接收两个参数:目标对象和目标原型对象。如:

const proxy = new Proxy({}, {
  setPrototypeOf: function(target, proto){
    console.log("将目标属性的原型设置为:" + JSON.stringify(proto))
    return Reflect.setPrototypeOf(target, proto);
  }
})

Object.setPrototypeOf(proxy, {}); 
// 将目标属性的原型设置为:{}
// true

3. 关于Proxy的其他问题

(1)Proxy.revocable()

该方法返回一个可取消的Proxy实例。

let {proxy, revoke} = Proxy.revocable({}, {});

proxy.name = '张三'; // ok

revoke();
peoxy.name = '李四'; // TypeError: Revoked

该方法返回一个对象,它有两个属性:proxy为代理对象,revoke为取消代理的方法。该方法主要用于取消代理权。

(2)this指向问题

在不使用代理的情况下,如果调用原对象上的方法(假设调用者即为该对象),方法内的this会指向该对象本身。

但是在使用代理的情况下,由于调用者发生了变化,这些方法内的this都会指向代理对象。这也就意味着,即使不对原对象作任何拦截,访问原对象和访问代理对象也不是完全等价的。如:

const target = {
  fun(){
    console.log(this === proxy);
  }
}

const proxy = new Proxy(target, {});

proxy.fun();  // true
target.fun();  // false

上例中,用原对象target调用fun时,this指向target;而用proxy调用时,this指向proxy。

某些情况下,这种差别会造成一定影响。如:

const _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxy = new Proxy(jane, {});
proxy.name // undefined

这里原对象Person的行为是,使用构造出的实例作为WeakMap的键,将传入的参数作为他的值。如执行const jane = new Person('Jane')后变量_name(WeakMap类型,可以以对象作为键)的值为:{{}: 'Jane'}。这里的键是一个Person实例。此时jane.name返回的是 _name.get(jane),也就是字符串’Jane’。

之后用上述Person实例生成一个代理对象,此时调用proxy.name会触发get函数,返回_name.get(this)。然而此时的this并不指向原对象jane,而是指向代理对象,所以get方法返回的实际上是_name(proxy),结果是undefined。

proxy虽然是变量jane的代理,但两者并不相等,所以以jane作为WeakMap的键存储的值,通过proxy并不能得到。

另外,一些原生对象也不能直接使用代理,如Date。Date实例对象的某些属性必须靠正确的this才能得到,使用代理对象无法获得。如:

const target = new Date();
const proxy = new Proxy(target, {});

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

这里proxy不是Date对象,而在内部调用getDate方法时发现this不是Date对象就会报错。这个问题可以通过绑定this来解决。即:

const proxy = new Proxy(target, {
  get: function(target, prop){
    if(prop === 'getDate'){
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
})

注意,调用getDate方法实际上就是先获取对象的getDate方法对象,然后再调用它,因此会触发对象的get拦截。这里检测到要访问的属性时getDate时,就返回已经绑定到原对象(target)的getDate,这样在调用时this就可以正确指向原对象。

发布了37 篇原创文章 · 获赞 90 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_41694291/article/details/103706088