es6-快速掌握Proxy、Reflect

更新日期: 2021-05-07阅读: 1.7k标签: es6

前言

ES6新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

Proxy (代理)

代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。缺少其中任何一个参数都会抛出 TypeError。

创建空代理

如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同
就是代码中操作的是代理对象。

const target = { 
 id: 'target' 
}; 
const handler = {}; 
const proxy = new Proxy(target, handler); 
// id 属性会访问同一个值
console.log(target.id); // target 
console.log(proxy.id); // target

// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo'; 
console.log(target.id); // foo 
console.log(proxy.id); // foo

// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar'; 
console.log(target.id); // bar 
console.log(proxy.id); // bar

定义捕获器

捕获器可以理解为处理程序对象中定义的用来直接或间接在代理对象上使用的一种“拦截器”,每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

const target = { 
 foo: 'bar' 
};
const handler = { 
 // 捕获器在处理程序对象中以方法名为键
 get() { 
 return 'handler override'; 
 } 
};
const proxy = new Proxy(target, handler); 
console.log(target.foo); // bar 
console.log(proxy.foo); // handler override

get() 捕获器会接收到目标对象,要查询的属性和代理对象三个参数。我们可以对上述代码进行如下改造

const target = { 
 foo: 'bar' 
};
const handler = { 
 // 捕获器在处理程序对象中以方法名为键
 get(trapTarget, property, receiver) { 
 console.log(trapTarget === target); 
 console.log(property); 
 console.log(receiver === proxy); 
 return trapTarget[property]
 } 
};
const proxy = new Proxy(target, handler); 
proxy.foo; 
// true 
// foo 
// true
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)api 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。因此,使用反射 API 也可以像下面这样定义出空代理对象:

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
     // 第一种写法
     return Reflect.get(...arguments); 
     // 第二种写法
     return Reflect.get
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar 
console.log(target.foo); // bar

我们也可以以此,来对将要访问的属性的返回值进行修饰。

const target = { 
 foo: 'bar', 
 baz: 'qux' 
}; 
const handler = { 
 get(trapTarget, property, receiver) { 
 let decoration = ''; 
 if (property === 'foo') { 
 decoration = ' I love you'; 
 } 
 return Reflect.get(...arguments) + decoration; 
 } 
}; 
const proxy = new Proxy(target, handler); 
console.log(proxy.foo); // bar I love you 
console.log(target.foo); // bar 
console.log(proxy.baz); // qux 
console.log(target.baz); // qux

可撤销代理

有时候可能需要中断代理对象与目标对象之间的联系。对于使用 new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直持续存在。Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。而且,撤销函数(revoke())是幂等的,调用多少次的结果都一样。撤销代理之后再调用代理会抛出 TypeError。

const target = { 
 foo: 'bar' 
}; 
const handler = { 
 get() { 
 return 'intercepted'; 
 } 
}; 
const { proxy, revoke } = Proxy.revocable(target, handler); 
console.log(proxy.foo); // intercepted 
console.log(target.foo); // bar 
revoke(); 
console.log(proxy.foo); // TypeError

代理另一个代理

代理可以拦截反射 API 的操作,而这意味着完全可以创建一个代理,通过它去代理另一个代理。这样就可以在一个目标对象之上构建多层拦截网:

const target = { 
 foo: 'bar' 
}; 
const firstProxy = new Proxy(target, { 
 get() { 
 console.log('first proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
const secondProxy = new Proxy(firstProxy, { 
 get() { 
 console.log('second proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
console.log(secondProxy.foo); 
// second proxy 
// first proxy 
// bar

代理的问题与不足

1. 代理中的this

const target = { 
 thisValEqualsProxy() { 
 return this === proxy; 
 } 
} 
const proxy = new Proxy(target, {}); 
console.log(target.thisValEqualsProxy()); // false 
console.log(proxy.thisValEqualsProxy()); // true

这样看起来并没有什么问题,this指向调用者。但是如果目标对象依赖于对象标识,那就可能碰到意料之外的问题。

const wm = new WeakMap(); 
class User { 
 constructor(userId) { 
     wm.set(this, userId); 
 } 
 set id(userId) { 
     wm.set(this, userId); 
 } 
 get id() { 
     return wm.get(this); 
 } 
}
const user = new User(123); 
console.log(user.id); // 123 
const userInstanceProxy = new Proxy(user, {}); 
console.log(userInstanceProxy.id); // undefined

这是因为 User 实例一开始使用目标对象作为 WeakMap 的键,代理对象却尝试从自身取得这个实
例。要解决这个问题,就需要重新配置代理,把代理 User 实例改为代理 User 类本身。之后再创建代
理的实例就会以代理实例作为 WeakMap 的键了:

const UserClassProxy = new Proxy(User, {}); 
const proxyUser = new UserClassProxy(456); 
console.log(proxyUser.id);

2. 代理与内部槽位

在代理Date类型时:根据 ECMAScript 规范,Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get()和 set()操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:

const target = new Date(); 
const proxy = new Proxy(target, {}); 
console.log(proxy instanceof Date); // true 
proxy.getDate(); // TypeError: 'this' is not a Date object

Reflect(反射)

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect的设计目的:

  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。
  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  3. 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

代理与反射API

get()

接收参数:

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。
  • receiver:代理对象或继承代理对象的对象。
    返回:
  • 返回值无限制
    get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 get(target, property, receiver) { 
 console.log('get()'); 
 return Reflect.get(...arguments) 
 } 
}); 
proxy.foo; 
// get()

set()

接收参数:

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。
  • value:要赋给属性的值。
  • receiver:接收最初赋值的对象。
    返回:
  • 返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。

set()捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 set(target, property, value, receiver) { 
 console.log('set()'); 
 return Reflect.set(...arguments) 
 } 
}); 
proxy.foo = 'bar'; 
// set()

has()

接收参数:

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。

返回:

  • has()必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。

has()捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。

const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 has(target, property) { 
 console.log('has()'); 
 return Reflect.has(...arguments) 
 } 
}); 
'foo' in proxy; 
// has()

defineProperty()

Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。

接收参数:

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。
  • descriptor:包含可选的 enumerable、configurable、writable、value、get 和 set定义的对象。

返回:

  • defineProperty()必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 defineProperty(target, property, descriptor) { 
 console.log('defineProperty()'); 
 return Reflect.defineProperty(...arguments) 
 } 
}); 
Object.defineProperty(proxy, 'foo', { value: 'bar' }); 
// defineProperty()

getOwnPropertyDescriptor()

Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象。

接收参数:

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。

返回:

  • getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回 undefined。
const myTarget = {}; 
const proxy = new Proxy(myTarget, { 
 getOwnPropertyDescriptor(target, property) { 
 console.log('getOwnPropertyDescriptor()'); 
 return Reflect.getOwnPropertyDescriptor(...arguments) 
 } 
}); 
Object.getOwnPropertyDescriptor(proxy, 'foo'); 
// getOwnPropertyDescriptor()

deleteProperty()

Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性。

接收参数:

  • target:目标对象。
  • property:引用的目标对象上的字符串键属性。

返回:

  • deleteProperty()必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。

ownKeys()

Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。

接收参数:

  • target:目标对象。

返回:

  • ownKeys()必须返回包含字符串或符号的可枚举对象。

getPrototypeOf()

Reflect.getPrototypeOf方法用于读取对象的__proto__属性

接收参数:

  • target:目标对象。

返回:

  • getPrototypeOf()必须返回对象或 null。

等等。。

代理模式

跟踪属性访问

通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过:

const user = { 
 name: 'Jake' 
}; 
const proxy = new Proxy(user, { 
 get(target, property, receiver) { 
 console.log(`Getting ${property}`); 
 return Reflect.get(...arguments); 
 }, 
 set(target, property, value, receiver) { 
 console.log(`Setting ${property}=${value}`); 
 return Reflect.set(...arguments); 
 } 
}); 
proxy.name; // Getting name 
proxy.age = 27; // Setting age=27

隐藏属性

代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。

const hiddenProperties = ['foo', 'bar']; 
const targetObject = { 
 foo: 1, 
 bar: 2, 
 baz: 3 
}; 
const proxy = new Proxy(targetObject, { 
 get(target, property) { 
 if (hiddenProperties.includes(property)) { 
 return undefined; 
 } else { 
 return Reflect.get(...arguments); 
 } 
 }, 
 has(target, property) {
  if (hiddenProperties.includes(property)) { 
 return false; 
 } else { 
 return Reflect.has(...arguments); 
 } 
 } 
}); 
// get() 
console.log(proxy.foo); // undefined 
console.log(proxy.bar); // undefined 
console.log(proxy.baz); // 3 
// has() 
console.log('foo' in proxy); // false 
console.log('bar' in proxy); // false 
console.log('baz' in proxy); // true

属性验证

因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:

const target = { 
 onlyNumbersGoHere: 0 
}; 
const proxy = new Proxy(target, { 
 set(target, property, value) { 
 if (typeof value !== 'number') { 
 return false; 
 } else { 
 return Reflect.set(...arguments); 
 } 
 } 
}); 
proxy.onlyNumbersGoHere = 1; 
console.log(proxy.onlyNumbersGoHere); // 1 
proxy.onlyNumbersGoHere = '2'; 
console.log(proxy.onlyNumbersGoHere); // 1

函数与构造函数参数验证

跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值:

function median(...nums) { 
     return nums.sort()[Math.floor(nums.length / 2)]; 
} 
const proxy = new Proxy(median, { 
     apply(target, thisArg, argumentsList) { 
         for (const arg of argumentsList) { 
             if (typeof arg !== 'number') { 
                 throw 'Non-number argument provided'; 
             } 
         }
  return Reflect.apply(...arguments); 
 } 
}); 
console.log(proxy(4, 7, 1)); // 4 
console.log(proxy(4, '7', 1)); 
// Error: Non-number argument provided 
类似地,可以要求实例化时必须给构造函数传参:
class User { 
 constructor(id) { 
     this.id_ = id; 
 } 
} 
const proxy = new Proxy(User, { 
 construct(target, argumentsList, newTarget) { 
     if (argumentsList[0] === undefined) { 
         throw 'User cannot be instantiated without id'; 
     } else { 
         return Reflect.construct(...arguments); 
     } 
 } 
}); 
new proxy(1); 
new proxy(); 
// Error: User cannot be instantiated without id

数据绑定与可观察对象

通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:

const userList = []; 
class User { 
 constructor(name) { 
 this.name_ = name; 
 } 
} 
const proxy = new Proxy(User, { 
 construct() { 
 const newUser = Reflect.construct(...arguments); 
 userList.push(newUser); 
 return newUser; 
 } 
}); 
new proxy('John'); 
new proxy('Jacob'); 
new proxy('Jingleheimerschmidt'); 
console.log(userList); // [User {}, User {}, User{}]

另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:

const userList = []; 
function emit(newValue) { 
 console.log(newValue); 
} 
const proxy = new Proxy(userList, { 
 set(target, property, value, receiver) { 
 const result = Reflect.set(...arguments); 
 if (result) { 
 emit(Reflect.get(target, property, receiver)); 
 } 
 return result; 
 } 
}); 
proxy.push('John'); 
// John 
proxy.push('Jacob'); 
// Jacob

使用 Proxy 实现观察者模式

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

const person = observable({
  name: '张三',
  age: 20
});

function print() {
  console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';
// 输出
// 李四, 20

结尾

本文主要参考阮一峰es6教程、js红宝书第四版

来自:https://segmentfault.com/a/1190000039956559


链接: https://fly63.com/article/detial/10266

es6 箭头函数的使用总结,带你深入理解js中的箭头函数

箭头函数是ES6中非常重要的性特性。它最显著的作用就是:更简短的函数,并且不绑定this,arguments等属性,它的this永远指向其上下文的 this。它最适合用于非方法函数,并且它们不能用作构造函数。

详解JavaScript模块化开发require.js

js模块化的开发并不是随心所欲的,为了便于他人的使用和交流,需要遵循一定的规范。目前,通行的js模块规范主要有两种:CommonJS和AMD

js解构赋值,关于es6中的解构赋值的用途总结

ES6中添加了一个新属性解构,允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。用途:交换变量的值、从函数返回多个值、函数参数的定义、提取JSON数据、函数参数的默认值...

ES6中let变量的特点,使用let声明总汇

ES6中let变量的特点:1.let声明变量存在块级作用域,2.let不能先使用再声明3.暂时性死区,在代码块内使用let命令声明变量之前,该变量都是不可用的,4.不允许重复声明

ES6的7个实用技巧

ES6的7个实用技巧包括:1交换元素,2 调试,3 单条语句,4 数组拼接,5 制作副本,6 命名参数,7 Async/Await结合数组解构

ES6 Decorator_js中的装饰器函数

ES6装饰器(Decorator)是一个函数,用来修改类的行为 在设计阶段可以对类和属性进行注释和修改。从本质上上讲,装饰器的最大作用是修改预定义好的逻辑,或者给各种结构添加一些元数据。

基于ES6的tinyJquery

Query作为曾经Web前端的必备利器,随着MVVM框架的兴起,如今已稍显没落。用ES6写了一个基于class简化版的jQuery,包含基础DOM操作,支持链式操作...

ES6 中的一些技巧,使你的代码更清晰,更简短,更易读!

ES6 中的一些技巧:模版字符串、块级作用域、Let、Const、块级作用域函数问题、扩展运算符、函数默认参数、解构、对象字面量和简明参数、动态属性名称、箭头函数、for … of 循环、数字字面量。

Rest/Spread 属性_探索 ES2018 和 ES2019

Rest/Spread 属性:rest操作符在对象解构中的使用。目前,该操作符仅适用于数组解构和参数定义。spread操作符在对象字面量中的使用。目前,这个操作符只能在数组字面量和函数以及方法调用中使用。

使用ES6让你的React代码提升到一个新档次

ES6使您的代码更具表现力和可读性。而且它与React完美配合!现在您已了解更多基础知识:现在是时候将你的ES6技能提升到一个新的水平!嵌套props解构、 传下所有props、props解构、作为参数的函数、列表解构

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!