title: ES6 - Proxy
date: 2020.12.13
top:

categories:

tags:


概述
Proxy 实例的方法
Proxy.revocable()
this 问题
实例:Web 服务的客户端

2020.12.13 星期日 17:33

1 概述

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});


注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果handler没有设置任何拦截,那就等同于直接通向原对象。
一个技巧是将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用。var object = { proxy: new Proxy(target, handler) };
Proxy 实例也可以作为其他对象的原型对象。let obj = Object.create(proxy);

同一个拦截器函数,可以设置拦截多个操作。
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

2 Proxy 实例的方法

get()

get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

get方法可以继承。
上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。

下面的例子使用get拦截,实现数组读取负数的索引。
利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。
下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

set()

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。
利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。
结合get和set方法,就可以做到防止这些内部属性被外部读写。
上面代码中,set方法的第四个参数receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身,请看下面的例子。

注意,如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用。
注意,严格模式下,set代理如果没有返回true,就会报错。

apply()

apply方法拦截函数的调用、call和apply操作。
apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

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"

// 另外,直接调用Reflect.apply方法,也会被拦截。
Reflect.apply(proxy, null, [9, 10]) // 38

has()

has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
has()方法可以接受两个参数,分别是目标对象、需查询的属性名。

下面的例子使用has()方法隐藏某些属性,不被in运算符发现。

另外,虽然for…in循环也用到了in运算符,但是has()拦截对for…in循环不生效。

construct()

construct()方法用于拦截new命令,下面是拦截对象的写法。

另外,由于construct()拦截的是构造函数,所以它的目标对象必须是函数,否则就会报错。

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

defineProperty()

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

getOwnPropertyDescriptor()

getOwnPropertyDescriptor()方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。

getPrototypeOf() § ⇧

getPrototypeOf()方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。

Object.prototype.proto
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof

isExtensible()

isExtensible()方法拦截Object.isExtensible()操作。

ownKeys()

ownKeys()方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。

Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for…in循环

for…in循环也受到ownKeys()方法的拦截。

注意,使用Object.keys()方法时,有三类属性会被ownKeys()方法自动过滤,不会返回。
目标对象上不存在的属性
属性名为 Symbol 值
不可遍历(enumerable)的属性

另外,如果目标对象是不可扩展的(non-extensible),这时ownKeys()方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。

preventExtensions()

preventExtensions()方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。

这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)为false),proxy.preventExtensions才能返回true,否则会报错。

setPrototypeOf()

setPrototypeOf()方法主要用来拦截Object.setPrototypeOf()方法。

注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型。

3 Proxy.revocable()

Proxy.revocable()方法返回一个可取消的 Proxy 实例。

Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

4 this 问题

在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。
此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

javascript 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 handler = { get(target, prop) { if (prop === 'getDate') { return target.getDate.bind(target); } return Reflect.get(target, prop); } };

另外,Proxy 拦截函数内部的this,指向的是handler对象。

## 5 实例:Web 服务的客户端
javascript const service = createWebService('http://example.com/data'); service.employees().then(json => { const employees = JSON.parse(json); // ··· }); function createWebService(baseUrl) { return new Proxy({}, { get(target, propKey, receiver) { return () => httpGet(baseUrl + '/' + propKey); } }); }
上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。


同理,Proxy 也可以用来实现数据库的 ORM 层。






# Object.create()

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。 (请打开浏览器控制台以查看运行结果。)
Object.create(proto,[propertiesObject])

function MyClass() {
     SuperClass.call(this);
     OtherSuperClass.call(this);
}

// 继承一个类
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 重新指定constructor
MyClass.prototype.constructor = MyClass;

MyClass.prototype.myMethod = function() {
     // do a thing
};

18:02