ES6学习笔记10 Proxy

Proxy简介

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

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

ES6 原生提供 Proxy 构造函数,用来生成 实例

var proxy = new Proxy(target, handler);
//其中 target表示所要拦截的目标对象,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

上述代码对一个空对象假设了一层拦截,重新定义了属性的getset操作,返回一个Proxy,其中参数target为拦截对象,key为接受到的键名,value为接受到的键值,receiver为一个可选的对象。因此,当对返回的Proxy读取和修改属性值时,就会调用相应的自定义方法,在控制台输出字符串。

同一个hander对象内可以设置多个拦截操作,对于没有设置的操作,遵从原先的方式产生结果。注意,要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作,否则定义的拦截操作不会起作用。

Proxy支持的拦截操作包括

(1)get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']

var person = {
  name: "张三"
};

var proxy = new Proxy(person, {
  get: function(target, property) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError("Property \"" + property + "\" does not exist.");
    }
  }
});

proxy.name // "张三"
proxy.age // 抛出一个错误

上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined

get方法可以继承

let proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET ' + propertyKey);
    return target[propertyKey];
  }
});

let obj = Object.create(proto);
obj.foo // "GET foo"

第三个参数receiver表示原始操作所在的对象

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true  原始读操作就在proxy对象

const d = Object.create(proxy);
d.a === d // true    原始读操作在d对象

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


(2) set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。

结合getset方法,就可以做到防止某些以下划线(_)开头,表示内部的属性被外部读写。

const handler = {
  get (target, key) {
    invariant(key, 'get');
    return target[key];
  },
  set (target, key, value) {
    invariant(key, 'set');
    target[key] = value;
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

上述代码,只要读写属性的第一个字符是下划线,则一律报错,达到禁止读写内部属性的目的

同样与get操作类似,set操作的receiver参数代表原始操作行为所在的那个对象

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);

proxy.foo = 'bar';
proxy.foo === proxy // true

const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = 'bar';
myObj.foo === myObj // true

注意,如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用。


(3)apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.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"

上面代码中,变量p是 Proxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。

另外,直接调用Reflect.apply方法,也会被拦截。

Reflect.apply(p, null, []) // 'I am the proxy'

(4)has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。

var handler = {
  has (target, key) {
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false

上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has就会返回false,从而不会被in运算符发现。

如果原对象不可配置或者禁止扩展,这时has拦截会报错。has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。虽然for…in循环也用到了in运算符,但是has拦截对for…in循环不生效。


(5)construct(target,args):拦截Proxy实例作为构造函数调用的操作,比如new proxy(...args)。返回的必须是一个对象,否则会报错

construct方法可以接受两个参数:

  • target:目标对象
  • args:构造函数的参数对象
var p = new Proxy(function () {}, {
  construct: function(target, args) {
    console.log('called: ' + args.join(', '));
    return { value: args[0] * 10 };
  }
});

(new p(1)).value
// "called: 1"
// 10

(6)deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property

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


(7)defineProperty(target,propKey,propDesc):拦截Object.defineProperty()Object.defineProperties(),返回一个布尔值。如果返回false,则导致新添加的属性总是无效

var handler = {
  defineProperty (target, key, descriptor) {
    return false;
  }
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效

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


(8)getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象或undefined


(9)getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。返回值必须是对象或null,另外如果对象不可扩展,则必须返回目标对象的原型对象


(10)isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。该方法有个限制,其返回值必须跟对象的isExtensible属性保持一致,也就是如果对象可以扩展,你必须返回true


(11)preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。该方法有个限制,只有当目标对象不可扩展时,才能返回true,否则会报错。

var p = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(p) // 报错

为了防止出现这个问题,通常要在proxy.preventExtensions方法里面,调用一次Object.preventExtensions


(12)ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

下面的例子是拦截第一个字符为下划线的属性名不被遍历。

let target = {
  _bar: 'foo',
  _prop: 'bar',
  prop: 'baz'
};

let handler = {
  ownKeys (target) {
    return Reflect.ownKeys(target).filter(key => key[0] !== '_');
  }
};

let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
  console.log(target[key]);
}
// "baz"

ownKeys方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。

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


(13)setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。

如果目标对象不可扩展(extensible),setPrototypeOf方法不得改变目标对象的原型。


Proxy.revocable()

该方法返回一个可取消的Proxy实例和一个取消函数,调用该函数,可以取消Proxy实例,不再允许访问

let target = {};
let handler = {};

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

proxy.foo = 123;
proxy.foo // 123

revoke();
proxy.foo // TypeError: Revoked

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


this问题

在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。将this绑定原始对象可以解决这个问题

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

proxy.getDate();   //有些原生对象的内部属性,只有通过正确的this才能拿到
// TypeError: this is not a Date object.

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);   //绑定this至目标对象
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);

proxy.getDate() // 1

猜你喜欢

转载自blog.csdn.net/zjw_python/article/details/80951116