JavaScript代理proxy

1,556 阅读6分钟

代理的概念

宏观:设计模式

代理模式:当用户不方便直接访问一个对象或者不满足需要时,提供一个替身对象来控制这个对象的访问,用户实际访问的是替身对象

关键:代理对象和本体都对外提供了 某个行为【方法】,通过代理分层,将不同的行为和主体分离的思想

简单实现一个原生代理:

const mult = function () {
  console.log("开始计算乘积");
  let a = 1;
  for (let i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
};

const  proxyMult = (function () {
   console.log("创建代理空间,赋值给proxyMult");
  const cache = {};
  return function () {
   console.log("访问代理");
   const args = Array.prototype.join.call(arguments, ",");
    if (args in cache) {
      return cache[args];
    }
    return (cache[args] = mult.apply(this, arguments));
  };
})();

proxyMult(11); 
proxyMult(1, 1); 

微观:ES6新语法-proxy

创建代理

创建一个空代理,可以让所有操作都抵达目标对象

const target={ //代表需要添加代理的对象
    id:'target',
    o:{},
};

const handler={};//捕获器
// 或者采用反射API const proxy=new Proxy(target,Reflect); 
const proxy=new Proxy(target,handler); //Proxy构造函数,创建代理(目标对象和处理程序对象)
console.log(target.o === proxy.o);//true 代理可以拿到目标对象 注意 {} !== {}
console.log(target === proxy);//false严格模式可以区分代理和目标

定义捕获器

一种基本操作对应一个拦截器,每个处理程序对象可以包含一个或多个。e.g: get

const target={
    foo:'bar'
};

const handler={
    //捕获器以方法名为键,基本操作get触发get捕获器
    get(){//代理的所有属性的get操作
        return 'handle override';
    }
};

const proxy=new Proxy(target,handler);//Proxy构造函数,创建代理(目标对象和处理程序对象)
console.log(target.foo);//bar
console.log(proxy.foo);//handle override
console.log(proxy.f);//handle override

反射API

开发者并不需要手动重建原始行为,而是可以通过调用全局Reflect反射对象上的同名方法来重建。反射API为开发者准备好了样板代码,可以用最少的代码修改捕获方法。

// Object
var obj = { x: 1, y: 2 };
Reflect.get(obj, "x"); // 1

// Array
Reflect.get(["zero", "one"], 1); // "one"

// Proxy with a get handler
var x = {p: 1};
var obj = new Proxy(x, {
// get: Reflect.get
  get(target, key, proxy) { 
      return target[key] + "bar";
      // return Reflect.get(...arguments);
  }
});
Reflect.get(obj, "foo"); // "foobar"

反射API不止用于捕获处理程序,适用于替换Function.prototype、Object.prototype
大多数反射API方法在Object类型上有对应的方法。object上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。

状态标记:返回布尔值-操作是否成功
部分反射方法提供状态标记:

Reflect.defineProperty();
Reflect.deleteProperty();//delete操作符
Reflect.preventExtensions();
Reflect.setPrototypeOf();
Reflect.set();
Reflect.getPrototypeOf();
Reflect.get();// 代替对象属性访问//更直接,不用经过get捕获器
Reflect.has();//with、in
Reflect.construct(); //new
Reflect.getOwnPropertyDescriptor();
Reflect.ownKeys();
Reflect.isExtensible();
Reflect.apply();

撤销代理Proxy.revocable

new Proxy()创建的普通代理,代理对象与目标对象之间的联系会在代理对象的生命周期内一直存在。

Proxy.revocable() 方法可以用来创建一个可撤销的代理对象。

var revocable = Proxy.revocable({}, {
  get(target, name) {
    return "[[" + name + "]]";
  }
});
var proxy = revocable.proxy;
proxy.foo;              // "[[foo]]"

revocable.revoke();

console.log(proxy.foo); // 抛出 TypeError
proxy.foo = 1           // 还是 TypeError
delete proxy.foo;       // 又是 TypeError
typeof proxy            // "object",因为 typeof 不属于可代理操作

revoke方法会使 proxy 被自动回收,之后的任何调用均以 TypeError 结尾。

嵌套代理

代理另一个代理:在一个目标对象之上创建多层拦截网

const target={
    foo:'bar'
};

const proxy1=new Proxy(target,{
    get(){
        console.log("first proxy");
        return Reflect.get(...arguments);//访问target
    }
});//Proxy构造函数,创建代理(目标对象和处理程序对象)

const proxy2=new Proxy(proxy1,{
    get(){
        console.log("second proxy");
        return Reflect.get(...arguments);//访问proxy1
    }
});//Proxy构造函数,创建代理(目标对象和处理程序对象)

console.log(proxy2.foo);
//second proxy
//first proxy
//bar

代理中的this问题

方法中的this指向调用方法的对象,所以代理执行target的代码时this指向proxy

联系之前弱引用、同时区别代理一个实例的代理实例和代理一个类的代理类

const wm = new WeakMap();

class User {
  constructor(userId) {
    wm.set(this, userId);
  }

  set id(userId) {
    wm.set(this, userId);
  }

  get id() {
    return wm.get(this);
  }
}  
const myuser = new User(123);
console.log(myuser.id);  // 123
const userInstanceProxy = new Proxy(myuser, {});// 代理一个实例
console.log(userInstanceProxy.id);  // undefined
const UserProxy = new Proxy(User, {});//代理类
console.log(UserProxy.id);  // undefined
const xx=new UserProxy(456);//实例化代理类
console.log(xx.id);  // 456

代理与内部卡槽 代理与部分内置类型可能会依赖代理无法控制的机制。 e.g:Date类方法的执行依赖内部槽位[NumberDate],代理对象上不存在这个槽位所以不能成功。

const target = new Date();
const proxy = new Proxy( target, {});
console.log(proxy instanceof Date);
proxy.getDate(); // Uncaught TypeError: this is not a Date object

应用

跟踪属性访问

捕获get、set、has等操作

const user={
    name:'jake'
};

const proxy=new Proxy(user,{
    get(target,property,receiver){
        console.log('getting ${property}');
        return Reflect.get(...arguments);
    },
    set(target,property,value,receiver){
        console.log('setting ${property}=${value}');
        return Reflect.set(...arguments);
    }
    //has
});


proxy.name;//getting name

隐藏属性

代理地内部实现对外部代码是不可见地,因此要隐藏目标对象上的属性也很轻易。涉及has、get函数

const hidden=['foo','bar'];//隐藏属性列表
const target={
    foo:1,
    bar:2,
    baz:3
};
const proxy=new proxy(target,{
    get(target,property){
        if(hidden.includes(property)){
            return undefined;
        }else{
            return Reflect.get(...arguments);
        }
    },
    has(target,property){
        if(hidden.includes(property)){
            return false;
        }else{
            return Reflect.has(...arguments);
        }
    },
});

属性验证

所有赋值操作都触发set捕获器,可以在其中根据所赋值决定是拒绝还是允许

构造函数参数验证

也对函数和构造函数的参数进行审查。

数据绑定、可观察对象。

通过代理可以把运行中原本不相关的部分联系到一起,这样就可以实现各种模式,从而让代码互操作。

将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中

const userList=[];
class User{
    constructor(name){
        this.name_=name;
    }
}

//注意是对类创建代理
const proxy=new Proxy(User,{
    constructor(){
        const newUser=Reflect.construct(...arguments);
        userList.push(newUser);
        return newUser;
    }
});

图片预加载

虚拟代理 考虑网络不佳或是图片太大,图片一时加载不出来: 先使用一张loading图片占位,然后用异步的方式加载图片,等图片加载好了再填充到img节点里

        const myImage = (function(){
    		const imgNode = document.createElement( 'img' );
    		document.body.appendChild( imgNode );
 
    		return {
    			setSrc: function( src ){
    				imgNode.src = src;
    			}
    		};
    	})();
 
    	const proxyImage = (function(){
    		const img = new Image;
    		img.onload = function(){ // 加载http://finlly.jpg后立即触发
    			myImage.setSrc( this.src );
    		};
    		return {
    			setSrc: function( src ){
    				myImage.setSrc( '代理对象设置的占位符.jpg' );   
    				img.src = src;
    			}
    		};
    	})();
    	proxyImage.setSrc( 'http://finlly.jpg' );            

控制函数的调用权

代理目标可是是函数,函数的本质也是对象

使用不信任的第三方库的时候,需要向不受自己控制的库要求传递一个函数,可以给他传递一个可撤销代理函数,使用完库之后撤销代理,防止第三方库持有对我方函数的引用,在我们不知道时候调用。

function accessTheDatabase(){
    // 我方重要函数
}

const {proxy, revoke} = Proxy.revocable(accessTheDatabase, {}); // 代理重要函数

代理捕获器与反射方法

get()

捕获器不定式:如果target.property不可写且不可配置,则处理程序返回的值必须与target.property匹配

拦截的操作:
proxy.property
proxy[property]
Object.create(proxy)[property]
Reflect.get(proxy,property,receiver)

捕获器处理程序参数

  • 目标对象
  • 字符串键属性
  • 代理对象receiver

set()

捕获器不定式 如果target.property不可写且不可配置,则不能修改

返回布尔值,操作成功与否

拦截的操作:
proxy.property=value
proxy[property]=value
Object.create(proxy)[property]=value
Reflect.set(proxy,property,value,receiver)
const target = {};
Object.defineProperty(target,"foo",{
	writable:true,
	value:"bar"
	});
const handler = {
  get(trapTarget, property, receiver) {
    return trapTarget[property];
  },
  set(trapTarget,property,value,receiver) {
    console.log("set");
    return Reflect.set(...arguments);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.foo);   // bar
console.log(target.foo);  // bar 
proxy.foo="ywy";
console.log(proxy.foo);   // ywy
console.log(target.foo);  // ywy 

has()

返回布尔值,表示属性是否存在

拦截的操作:
property in proxy
property in Object.create(proxy)
with(proxy){(property);}
Reflect.has(proxy,property)

defineProperty()

捕获器不定式 如果目标对象不可扩展,则无法定义属性

返回布尔值,操作成功与否

拦截的操作:
Object.defineProperty(proxy,property,descriptor)
Reflect.defineProperty(proxy,property,descriptor)//descriptor包含可选的enumerable、configurable、writable、value、get、set定义

getOwnPropertyDescriptor()

返回对象,或者undefined

拦截的操作:
Object.getOwnPropertyDescriptor(proxy,property)
Reflect.getOwnPropertyDescriptor(proxy,property)

deleteProperty()

返回布尔值,是否删除

拦截的操作:
delete proxy.property
delete proxy[property]
Reflect.deleteProperty(proxy,property)

ownKeys()

捕获不定式: 返回的可枚举对象必须准确地包含target所有不可配置的自有属性 如果target不可扩展,则返回可枚举对象必须准确地包含自由属性键

返回包含字符串或符号的可枚举对象

拦截的操作:
Object.getOwnPropertyNames(proxy)
Object.getOwnPropertySymbols(proxy)
Object.keys(proxy)
Reflect.ownKeys(proxy)

getPrototypeOf()

返回对象或者null

拦截的操作:
proxy.__ proto __

setPrototypeOf()

返回布尔,原型赋值是否成功

isExtensions()

返回布尔,是否可以扩展

preventExtensions()

返回布尔,是否已经不可以扩展

apply()

捕获不定式 target必须是一个函数对象

调用代理时候被捕获

function median(...nums) {
  return nums.sort()[Math.floor(nums.length / 2)];
}
console.log("proxy前median:"+median(4, 7, 1)); // 4
let count=0;
const proxy = new Proxy(median, {
  apply(target, thisArg, ...argumentsList) {
    console.log("this proxy apply");
    return Reflect.apply(...arguments);
  }
});
console.log("proxy后median:"+median(4, 7, 1)); // 4
console.log(proxy(4, 7, 1)); //this proxy apply 4

construct()

返回一个对象

拦截的操作:
new proxy(...argumentsList)
Reflec.construct(target,argumentsList,newTarget)