ES6(8)Proxy(拦截器、代理器)

2,608 阅读10分钟

概述:改变默认行为,对外界的访问进行过滤和改写

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

Proxy改变默认行为

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

var proxy = 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,去读写它的属性,就会得到下面的结果。

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

上面代码说明,Proxy 实际上重载了点运算符,即用自己的定义覆盖了语言的原始定义

语法,var proxy = new Proxy(target, handler);

var proxy = new Proxy(target, handler);

target:参数表示所要拦截的目标对象

handler:参数也是一个对象,用来定制拦截行为

要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作

如果handler没有设置任何拦截,那就等同于直接通向原对象,没有任何拦截效果,访问proxy就等同于访问target

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

Proxy 对象,设置到object.proxy属性,从而可以当做object`对象的属性调用

var object = { proxy: new Proxy(target, handler) };

Proxy 实例作为其他对象的原型对象

必须是new以后生成的实例,才会触发拦截

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

let obj = Object.create(proxy);
obj.time // 35

//obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。

同一个拦截器函数,可以设置拦截多个操作

var handler = {
  get: function(target, name) {
    if (name === 'prototype') {
      return Object.prototype;
    }
    return 'Hello, ' + name;
  },

  apply: function(target, thisBinding, args) {
    return args[0];
  },

  construct: function(target, args) {
    return {value: args[1]};
  }
};

var fproxy = new Proxy(function(x, y) {
  return x + y;
}, handler);

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true

Proxy 支持的拦截操作

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
方法 描述
handler.apply() 拦截 Proxy 实例作为函数调用的操作
handler.construct() 拦截 Proxy 实例作为构造函数调用的操作
handler.defineProperty() 拦截 Object.defineProperty() 的操作
handler.deleteProperty() 拦截 Proxy 实例删除属性操作
handler.get() 拦截 读取属性的操作
handler.set() 拦截 属性赋值的操作
handler.getOwnPropertyDescriptor() 拦截 Object.getOwnPropertyDescriptor() 的操作
handler.getPrototypeOf() 拦截 获取原型对象的操作
handler.has() 拦截 属性检索操作
handler.isExtensible() 拦截 Object.isExtensible()操作
handler.ownKeys() 拦截 Object.getOwnPropertyDescriptor() 的操作
handler.preventExtension() 拦截 Object().preventExtension() 操作
handler.setPrototypeOf() 拦截Object.setPrototypeOf()操作
Proxy.revocable() 创建一个可取消的 Proxy 实例

get(target,key,receiver)

拦截某个属性的读取操作

依次三个参数:目标对象、被读取的属性名、proxy 实例本身

target:必选、目标对象

key:必选、被读取的属性名,在get内部是字符串

receiver:可选、proxy 实例本身(严格地说,是操作行为所针对的对象)

需要return

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

需要return

应用实例

1、访问目标对象不存在的属性,抛出一个错误而不是返回undefined

如果没有这个拦截函数,访问不存在的属性,只会返回undefined

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 // 抛出一个错误
2、get方法可以继承(定义在Prototype对象上,拦截实例通过原型链获取继承的方法的操作)

当拦截操作定义在Prototype对象上面时,读取obj对象继承的属性(自身没有的属性)时,拦截会生效。

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

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

上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时(本身没有,通过原型链查找的属性),拦截会生效。

3、使用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
//简略版
var arr=new Proxy([0,1,2,3,4],{
    get(target, p, receiver) {
        if(p<0){
            var n=eval(target.length-1+p);
            return target[n];
        }
        return target[p];
    }
})

console.log(arr[-1]);   //3
4、将读取属性的操作(get),转变为执行某个函数【vue3.0】有点像发布订阅

利用Proxy get拦截以后仍然返回Proxy实例的特性,达到了将函数名链式使用的效果

var pipe = (function () {
  return function (value) {
    var funcStack = [];
    var oproxy = new Proxy({} , {
      get : function (pipeObject, fnName) {
        if (fnName === 'get') {
	        //如果获取的是get 对数组funcStack中的函数一次调用
          return funcStack.reduce(function (val, fn) {
            return fn(val);
          },value);
        }
        //如果不是get  向数组funcStack中添加函数
        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

//有点像发布订阅
5、利用get拦截,实现一个生成各种 DOM 节点的通用函数dom【vue3.0】
const dom=new Proxy({},{
    get(target, p, receiver) {
        //(attrs={},...children)   第一个参数为attrs 剩余的都是children
        return function (attrs={},...children) {
            //get属性名  就是要创建的 元素名称
            const el=document.createElement(p);
            //循环 attrs设置元素属性
            for (let prop of Object.keys(attrs)){
                el.setAttribute(prop,attrs[prop]);
            }
            for (let child of children){
                if(typeof child === 'string'){
                    child = document.createTextNode(child);
                }
                el.append(child)
            }
            return el;
        }
    }
})


const el = dom.div(
    {},
    'Hello, my name is ',
    dom.a({href: '//example.com'}, 'Mark'),
    '. I like:',
    dom.ul({},
        dom.li({}, 'The web'),
        dom.li({}, 'Food'),
        dom.li({}, '…actually that\'s it')
    )
);

//相对与div :{}为 attrs ,剩余的都是子级
console.log(el);
document.body.appendChild(el);

image.png

第三个参数的例子,一般情况下就是 Proxy 实例

总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

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

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(obj, prop, value,receiver)

拦截某个属性的赋值操作

依次四个参数:目标对象,属性名,属性值,Proxy 实例本身

target:目标对象

prop:属性名,在get内部是字符串

value:属性值

receiver:可选,Proxy 实例本身

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

在赋值操作发生时进行自己想要的操作,还可以进行数据绑定,即每当对象发生变化时,会自动更新 DOM(vue3.0)

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 // 报错

设置一些内部属性不被外部读写(假定开头是_的为内部属性)

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

第四个参数的例子

set方法的第四个参数receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身,跟get的第三个参数的运用相同

const handler = {
  set: function(obj, prop, value, receiver) {
    obj[prop] = receiver;
  }
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true

apply(target, context, args),拦截函数的调用、call和apply操作

依次三个形参:目标对象,上下文对象(this),参数数组

target:目标对象

context:目标对象的上下文对象this

args:目标对象的参数数组

设置apply拦截后 函数自身内部的代码不再执行

var handler = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments);
  }
};

示例

var fn=function () {
    //不执行
    console.log(this,'fn')
    return 'I am fn'
}

var p=new Proxy(fn,{
    apply(target, thisArg, argArray) {
        console.log(thisArg,'p')   //undefined 'p'
        return 'I am p'
    }
})

console.log(p());   //I am p
var twice = {
  apply (target, ctx, args) {
    return Reflect.apply(...arguments) * 2;
  }
};
function sum (left, right) {
  return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30

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

Reflect.apply(proxy, null, [9, 10]) // 38

construct(target, args, newTarget)

用于拦截new命令

依次三个形参:目标对象,参数对象,new 后面的函数

target:目标对象

args:构造函数的参数对象

newTarget:可选,创造实例对象时,new命令作用的构造函数(new 后面的函数)

construct方法返回的必须是一个对象

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

has(target, key),拦截HasProperty操作,即判断对象是否具有某个属性(in运算符)

拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符

依次两个形参

target:目标对象

key:需查询的属性名

下面的例子使用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

deleteProperty(target, key),用于拦截delete操作

用于拦截delete操作

通过抛出错误或者返回false,阻止delete命令删除。

依次两个形参:目标对象、需查删除的属性名

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

Proxy.revocable() ,返回一个可取消的 Proxy 实例

Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。

当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误

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

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

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

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

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

this 问题

Proxy不做任何拦截的情况下,也无法保证与目标对象的行为一致

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

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true
const _name = new WeakMap();

class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

const jane = new Person('Jane');
jane.name // 'Jane'

const proxy = new Proxy(jane, {});
proxy.name // undefined

上面代码中,目标对象janename属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy.name访问时,this指向proxy,导致无法取到值,所以返回undefined

原生对象的内部属性,只有通过正确的this才能拿到

也就是说this必须是对应类的实例才能拿到的内部属性

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

proxy.getDate();
// TypeError: this is not a Date object.

用bind将this绑定到原始处理对象,就可以解决这个问题

const target = new Date('2015-01-01');
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
  }
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1