JS笔记《Proxy》

72 阅读6分钟

概述

  • Proxy可以理解成在目标对象之前架设一层拦截,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。可以由它来代理某些操作。
  • ES6提供原生Proxy构造函数,用来生成Proxy实例。接受两个参数,参数一是要代理的目标对象;参数二是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
let obj = {a: 30};
let proxy = new Proxy(obj, {
  get(target, propKey){
    return 35;
  }
})

console.log(proxy.time); // 35  必须针对Proxy实例操作才会起作用
console.log(obj.a);   // 30

实例方法

get()

  • 用于拦截某个属性的读取操作,接受三个参数,依次为目标对象、属性名和proxy实例本身(可选)。
let obj = {a: 30};
let proxy = new Proxy(obj, {
  get(target, propKey){
    return target[propKey];
  }
})

console.log(proxy.a); // 35
  • 如果一个属性设置了不可配置和不可写,则Proxy不能修改该属性,否则通过Proxy对象访问该属性会报错。
const obj = Object.defineProperty({}, 'foo', {
  value: 10,
  writable: false,
  configurable: false
})

let proxy = new Proxy(obj, {
  get(target, propKey) {
    return 'abc';
  }
})

console.log(proxy.foo);
// 'get' on proxy: property 'foo' is a read-only and 
// non-configurable data property on the
// proxy target but the proxy did not 
// return its actual value (expected '10' but got 'abc')

set()

  • 用来拦截某个属性的赋值操作,接受四个参数,依次为目标对象、属性名、属性值和proxy实例本身(可选)。
  • 如果目标对象的某个属性不可写,那么set方法将不起作用。
// 指定age属性是一个不大于200的整数

let person = new Proxy({}, {
  set(target, prop, value){
    if('age' === prop){
      if(!Number.isInteger(value)){
        throw new TypeError('The age is not an Integer');
      }
      if(value > 200){
        throw new RangeError('The age eems invalid');
      }
      target[prop] = value;
      return true;
    }
  }
})
// person.age = ''    // TypeError: The age is not an Integer
// person.age = 300   // The age eems invalid

person.age = 100   // true

apply()

  • 拦截函数的调用、callapply操作。接受三个对象,依次是目标对象,目标对象的上下文对象(this)和目标对象的参数数组。
let fun = function (str) {
  return str;
}

// 代理 fun函数
let proxy = new Proxy(fun, {
  apply(target, context, args) {
    console.log(target)  // function (str) { return str; }
    console.log(context) // {} --> obj
    console.log(args)    // ['123']
  }
})

let obj = {}
proxy.call(obj, '123');

has()

  • 用来拦截HasProperty操作,即判断对象是否具有某个属性(自身的和继承的都算)时,这个方法会生效,典型的就是in操作符。接收两个参数,依次是目标对象、需要查询的属性名。
  • 只对in运算符有效,对for in循环不生效。
let obj = {
  '_a': 1, // 表示私有属性
  b: 2
}
Object.prototype.c = 3;

let proxy = new Proxy(obj, {
  has(target, prop){
    // 以下划线开头的表示私有属性,不能被访问
    if(prop.startsWith('_')){
      return false;
    }
    return prop in target;
  }
})

'_a' in proxy  // false 私有属性
'b' in proxy   // true
'c' in proxy   // true 原型对象上的属性也算在内

construct()

  • 用于拦截new命令。接收三个参数,依次是目标对象、构造函数的参数数组、proxy实例。返回的必须是一个对象,否则会报错。
function Person(name, age){
  this.name = name;
  this.age = age;
}

let personFactory = new Proxy(Person, {
  construct(target, args, fn){
    console.log(target)   // function Person(name, age){ this.name = name; this.age = age; }
    console.log(args)     // ['张三', 20]
    console.log(fn)       // Proxy(Function) {length: 2, name: 'Person' ,...}
    return new target(...args);
  }
})    
let person = new personFactory('张三', 20);  // Person {name: '张三', age: 20}

deleteProperty()

  • 用于拦截delete操作,如果此方法抛出错误或返回false,当前属性就无法被delete删除。两个参数,依次是目标对象,属性名。如果对象的属性的是不可配置的,则不能被删除。
let obj = Object.defineProperties({}, {
  '_a': {  // 私有属性
    value: 1,
    configurable: true
  },
  'b': {
    value: 2,
    configurable: false
  },
  'c': {
    value: 3,
    configurable: true
  }
})

let p = new Proxy(obj, {
  deleteProperty(target, prop){
    if(prop.startsWith('_')){
      return false;
    }
    return delete target[prop];
  }
})

delete p['_a'] // false  私有属性不可删除
delete p.b     // false  属性描述为不可配置
delete p.c     // true 可以删除

p  // Proxy(Object) {_a: 1, b: 2}

defineProperty()

  • 拦截Object.defineProperty操作。三个参数,依次为目标对象、属性名、属性描述对象。如果目标对象不可扩展,则不能在目标对象上增加新属性。如果目标对象的某个属性为不可写或不可配置,则修改依然不生效。
let obj = {};

let p = new Proxy(obj, {
  defineProperty(target, key, descriptor) {
    if (key[0] === '_') { // 不能添加私有属性
      return target;
    } else {
      return Object.defineProperty(target, key, descriptor)
    }
  }
})
p.b = 2;   
p // Proxy(Object) {b: 2} .赋值也会触发

Object.defineProperty(p, 'a', { value: 1 }) // Proxy(Object) {b: 2, a: 1}
Object.defineProperty(p, '_a', { value: 1}) // Proxy(Object) {b: 2, a: 1}

getOwnPropertyDescriptor()

  • 拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。两个参数,依次为目标对象,属性名。
let target = {
  _foo: 'bar',
  baz: 'tar'
};

let proxy = new Proxy(target, {
  getOwnPropertyDescriptor(target, key) {
    if (key[0] === '_') {
      return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
  }
});

Object.getOwnPropertyDescriptor(proxy, 'wat')  // undefined  不存在对应属性
Object.getOwnPropertyDescriptor(proxy, '_foo') // undefined  私有属性
Object.getOwnPropertyDescriptor(proxy, 'baz')  // { value: 'tar', writable: true, enumerable: true, configurable: true }

getPropertyOf()

  • 拦截获取对象原型,包括以下方法:__proto__、isPrototypeOf()、Object.getPrototypeOf()、Reflect.getPrototypeOf()、instanceof
let proto = {};

let p = new Proxy(proto, {
  getPrototypeOf(target){
    return [];
  }
});

console.log(Object.getPrototypeOf(p))  // [] 

isExtensible()

  • 拦截Object.isExtensible()操作。只能返回boolean值
  • 这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。
let p = new Proxy({}, {
  isExtensible: function(target) {
    console.log("called");
    return true;
  }
});

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

ownKeys()

  • 拦截对象自身属性的读取操作,拦截以下方法:getOwnPropertyNames()、getOwnPropertySymbols()、Object.keys()、for in 循环
  • ownKeys()方法只能返回数组,且返回的数组成员只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
let target = {
  a: 1,
  b: 2,
  c: 3
};

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

let proxy = new Proxy(target, handler);

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

preventExtensions()

  • 拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
  • 这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true,否则会报错。
let proxy = new Proxy({}, {
  preventExtensions: function(target) {
    return true;
  }
});

Object.preventExtensions(proxy);
// Uncaught TypeError: 'preventExtensions' on proxy: 
// trap returned truish but the proxy target is extensible

setPrototypeOf()

  • 用来拦截Object.setPrototypeOf()方法。只能返回布尔值。如果目标不可扩展,则此方法不得改变目标对象的原型。
let obj = {}
let obj2 = {a: 1};

let p = new Proxy(obj, {
  setPrototypeOf(target, proto) {
    console.log('change prototype')
    Object.setPrototypeOf(target, proto);
    return true;
  }
})

Object.setPrototypeOf(p, obj2); // change prototype true
console.log(Object.getPrototypeOf(p) === obj2) // true

Proxy.revocable()

  • 创建一个可撤销的proxy实例,返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。
  • 使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
let { proxy, revoke } = Proxy.revocable({a: 1 }, {
  get() {
    return '哈哈';
  }
})
console.log(proxy.a);  // 哈哈
revoke();   // 关闭访问
console.log(proxy.a);  // TypeError: Cannot perform 'get' on a proxy that has been revoked

this指向

  • 在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 实例。
const target = {
  m: function () {
    console.log(this === target);
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m()  // true(target调用的m函数,函数中的this指向依然是target)  false 
proxy.m()   // false  true(通过proxy代理访问时,对象中的this就指向proxy实例)
  • Proxy 拦截函数内部的this,指向的是配置对象。
const handler = {
  get: function (target, key, receiver) {
    console.log(this === handler);
    return 'Hello, ' + key;
  },
  set: function (target, key, value) {
    console.log(this === handler);
    target[key] = value;
    return true;
  }
};

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

proxy.foo
// true
// Hello, foo

proxy.foo = 1
// true