blog

首页 / knowledge / proxy

Proxy

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

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

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);
  }
});

上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

api

const proxy = new Proxy(target,handler)

Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。 如果handler没有设置任何拦截,那就等同于直接通向原对象。

var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"

Proxy 的实例方法

get()

get api

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

demo1

get方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。

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 // 抛出一个错误

demo2

下面的例子使用get拦截,实现数组读取负数的索引。

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

demo3

利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。

var pipe = (function () {
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (pipeObject, fnName) {
        if (fnName === 'get') {
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        funcStack.push(window[fnName]);
        return oproxy;
      }
    });

    return oproxy;
  }
}());

var double = n => n * 2;
var pow    = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;

pipe(3).double.pow.reverseInt.get; // 63

上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。

demo4

如果一个属性不可配置(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

demo5

下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});
proxy.getReceiver === proxy // true
上面代码中proxy对象的getReceiver属性是由proxy对象提供的所以receiver指向proxy对象

const proxy = new Proxy({}, {
  get: function(target, property, receiver) {
    return receiver;
  }
});

const d = Object.create(proxy);
d.a === d // true

上面代码中,d对象本身没有a属性,所以读取d.a的时候,会去d的原型proxy对象找。这时,receiver就指向d,代表原始的读操作所在的那个对象。

set()

api

function(target, property, value, receiver)可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

demo1

假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。

let 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) {
        throw new RangeError('The age seems invalid');
      }
    }
    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
  }
};

let person = new Proxy({}, validator);

person.age = 100;

person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错

apply()

api

apply(target, ctx, args)apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组

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

has()

api

has(target, key)has方法可以接受两个参数,分别是目标对象、需查询的属性名

demo1

下面的例子使用has方法隐藏某些属性,不被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拦截会报错。

var obj = { a: 10 };
Object.preventExtensions(obj);

var p = new Proxy(obj, {
  has: function(target, prop) {
    return false;
  }
});

'a' in p // TypeError is thrown

上面代码中,obj对象禁止扩展,结果使用has拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has方法就不得“隐藏”(即返回false)目标对象的该属性。

值得注意的是,has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。

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

demo2

let stu1 = {name: '张三', score: 59};
let stu2 = {name: '李四', score: 99};

let handler = {
  has(target, prop) {
    if (prop === 'score' && target[prop] < 60) {
      console.log(`${target.name} 不及格`);
      return false;
    }
    return prop in target;
  }
}

let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);

'score' in oproxy1
// 张三 不及格
// false

'score' in oproxy2
// true

for (let a in oproxy1) {
  console.log(oproxy1[a]);
}
// 张三
// 59

for (let b in oproxy2) {
  console.log(oproxy2[b]);
}
// 李四
// 99

上面代码中,has拦截只对in运算符生效,对for…in循环不生效,导致不符合要求的属性没有被for…in循环所排除。

construct()

api

用于拦截new命令

var handler = {
  construct (target, args, newTarget) {
    return new 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

construct方法返回的必须是一个对象,否则会报错。 ```js var p = new Proxy(function() {}, { construct: function(target, argumentsList) { return 1; } });

new p() // 报错 // Uncaught TypeError: ‘construct’ on proxy: trap returned non-object (‘1’)


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

demo1

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    delete target[key];
    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

上面代码中,deleteProperty方法拦截了delete操作符,删除第一个字符为下划线的属性会报错。

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

defineProperty()

api

defineProperty方法拦截了Object.defineProperty操作。 defineProperty(target, property, descriptor)

demo1

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方法不得改变这两个设置。

getOwnPropertyDescriptor()

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

api

getOwnPropertyDescriptor(target, prop)

demo1

var handler = {
  getOwnPropertyDescriptor (target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }

上面代码中,handler.getOwnPropertyDescriptor方法对于第一个字符为下划线的属性名会返回undefined。

getPrototypeOf()

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

api

getPrototypeOf(target)

注意,getPrototypeOf方法的返回值必须是对象或者null,否则报错。另外,如果目标对象不可扩展(non-extensible), getPrototypeOf方法必须返回目标对象的原型对象。

isExtensible()

isExtensible方法拦截Object.isExtensible操作。

api

isExtensible(target)

demo1

var p = new Proxy({}, {
  isExtensible: function(target) {
    console.log("called");
    return true;
  }
});

Object.isExtensible(p)
// "called"
// true

上面代码设置了isExtensible方法,在调用Object.isExtensible时会输出called.

注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。

这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。

Object.isExtensible(proxy) === Object.isExtensible(target)

demo2

var p = new Proxy({}, {
  isExtensible: function(target) {
    return false;
  }
});

Object.isExtensible(p)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')

ownKeys()

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

demo1

let target = {
  a: 1,
  b: 2,
  c: 3
};

let handler = {
  ownKeys(target) {
    return ['a'];
  }
};

let proxy = new Proxy(target, handler);

Object.keys(proxy)
// [ 'a' ]

上面代码拦截了对于target对象的Object.keys()操作,只返回a、b、c三个属性之中的a属性。

demo2

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

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"

注意,使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回。

Object.defineProperty(target, ‘key’, { enumerable: false, configurable: true, writable: true, value: ‘static’ });

let handler = { ownKeys(target) { return [‘a’, ‘d’, Symbol.for(‘secret’), ‘key’]; } };

let proxy = new Proxy(target, handler);

Object.keys(proxy) // [‘a’] ``` 上面代码中,ownKeys方法之中,显式返回不存在的属性(d)、Symbol 值(Symbol.for(‘secret’))、不可遍历的属性(key),结果都被自动过滤掉。