第9章 代理与反射


ES 6 新增的代理和反射未开发者提供了拦截向基本操作嵌入额外行为的能力。具体地说,可以给目标对象定义一个关联的代理对象,而这个对象可以作为抽象的目标对象来使用在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

9.1 代理基础

​ 代理可视作目标对象的抽象,可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以被操作,也可以通过代理来操作。

9.1.1 创建空代理

​ 代理是使用Proxy构造函数创建的。接收两个参数:目标对象和处理程序对象。两个都是必需的,少一个都会抛出错误。在代理对象上执行的任何操作实际上都会应用到目标对象

const target = {
    id:'target'
};

const handler = {};

const proxy = new Proxy(target,handler);

//id属性会访问同一个值
console.log(target.id);//target
console.log(proxy.id);//target

//给目标属性赋值会反映在两个对象上,因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id);//foo
console.log(proxy.id);//foo

//给代理属性赋值会反映在两个对象上,因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id);//bar
console.log(proxy.id);//bar

//hasOwnProperty()在两个地方都会应用到目标对象
console.log(target.hasOwnProperty('id'));//true
console.log(proxy.hasOwnProperty('id'));//true

//Proxy.prototype 是 undefined,因此不能使用instanceof操作符
console.log(target instanceof Proxy);//Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy);//Function has non-object prototype 'undefined' in instanceof check

//严格相等可以用来区分代理和目标
console.log(target === proxy);//false
console.log(typeof proxy);//object

9.1.2 定义捕获器

​ 使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作。可以直接或间接在代理对象上调用。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

const target = {
    id:'target'
};

const handler = {
    //捕获器在处理程序对象中以方法名为键,当通过代理对象执行get()操作时,就会触发定义的get()捕获器
    get(){
        return 'handler override';
    }
};

const proxy = new Proxy(target,handler);

//在有在代理对象上执行这些操作才会触发捕获器,在目标对象上执行这些操作仍会产生正常的行为
console.log(target.id);//target
console.log(proxy.id);//handler override

console.log(target['id']);//target
console.log(proxy['id']);//handler override

console.log(Object.create(target)['id']);//target
console.log(Object.create(proxy)['id']);//handler override

9.1.3 捕获器参数和反射API

​ 所有捕获器都可以访问相应的参数。如get()捕获器会接收目标对象、要查询的属性、代理对象3个参数。

const target = {
    id:'target'
};

const handler = {
    get(trapTarget,property,receiver){
        console.log(trapTarget === target);
        console.log(property);
        console.log(receiver === proxy);
    }
};

const proxy = new Proxy(target,handler);

proxy.id;
// true
// id
// true

​ 有了这些参数,可以重建捕获方法的原始行为:

const target = {
    id:'target'
};

const handler = {
    get(trapTarget,property,receiver){
        return trapTarget[property];
    }
};

const proxy = new Proxy(target,handler);

console.log(proxy.id);//target

​ 所有捕获器都可以基于自己的参数重建原始操作,但有的捕获器的行为比较复杂。要想重建原始行为,可以通过调用全局Reflect对象(封装了原始行为)上的同名方法来轻松重建。

​ 处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方法,这些方法与捕获器拦截的方法具有相同的名称和函数签名、行为。

const target = {
    id:'target',
    name:'foo'
};
//定义了一个空代理对象
const handler = {
    get(){
        return Reflect.get(...arguments);
    }
};

const proxy = new Proxy(target,handler);

console.log(proxy.id,proxy.name);//target foo

​ 可以简写:

const handler = {
    get:Reflect.get
};

​ 事实上,如果打算创建一个可以捕获所有方法,然后将每个方法转发给对应代理API的空代理,那么甚至不需要定义处理程序对象:

const target = {
    id:'target',
    name:'foo'
};

const proxy = new Proxy(target,Reflect);

console.log(proxy.id,proxy.name);//target foo

​ 反射API 准备了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法:

const target = {
    id:'target',
    foo:'foo'
};

const handler = {
    get(trapTarget,property,receiver){
        let decoration = '';
        if (property === 'foo'){
            decoration = '!!!';
        }
        return Reflect.get(...arguments) + decoration;
    }
};

const proxy = new Proxy(target,handler);

console.log(proxy.foo);//foo!!!
console.log(target.foo);//foo

console.log(proxy.id);//target
console.log(target.id);//target

9.1.4 捕获器不变式

​ 使用捕获器几乎可以改变所有基本方法的行为,但是也有限制。根据ECMAScript规范,每个捕获的方法都知道目标对象上下文、捕获函数签名,而捕获处理程序的行为必须遵循“捕获器不变式”捕获器不变式(trap invariant)因方法不同而不同,但通常都会防止捕获器定义出现过于反常的行为。

const target = {};
Object.defineProperty(target,'foo',{
    configurable:false,
    writable:false,
    value:'bar'
});

const handler = {
    get(){
        return 'qux';
    }
};

const proxy = new Proxy(target,handler);

console.log(proxy.foo);
//TypeError: '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 'bar' but got 'qux')

9.1.5 可撤销代理

​ 有时可能需要中断代理对象和目标对象之间的联系。对于new Proxy()创建的普通代理来说,这种联系会在代理对象的生命周期内一直延续存在。

​ Proxy暴露了 revocable()(表示代理可撤销)方法,这个方法支持撤销代理对象与目标对象之间的关联。撤销代理的操作是不可逆的,而且撤销函数(revoke())调用多少次结果都一样,撤销代理之后再调用代理时会抛出 TypeError。

​ 撤销函数和代理对象是在实例化时同时生成的:

const target = {
    id:'target'
};

const handler = {
    get(){
        return 'handler override';
    }
};

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

console.log(proxy.id);//handler override
console.log(target.id);//target

revoke();

console.log(proxy.id);//TypeError: Cannot perform 'get' on a proxy that has been revoked

9.1.6 实用 Reflect API

​ 某些情况下应该优先使用反射 API,这是有一些理由的。

  1. 反射 API 与对象 API 在使用反射 API 时,要记住:

​ (1) 反射 API 并不限于捕获处理程序;

​ (2) 大多数反射 API 方法在 Object 类型上有对应的方法。

通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作

  1. 状态标记 很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。
const o = {};
if(Reflect.defineProperty(o, 'foo', {
    value:'bar',
    writable:true,
    Configurable:true,
    enumerable:true//想要属性被cosole.log打印出,这个属性必须是true
})) {
    console.log('success');
} else {
    console.log('failure');
}

console.log(o);
// success
// { foo: 'bar' }
  1. 用一等函数替代操作符 以下反射方法提供只有通过操作符才能完成的操作。

     Reflect.get():可以替代对象属性访问操作符。

     Reflect.set():可以替代=赋值操作符。

     Reflect.has():可以替代 in 操作符或 with()。

     Reflect.deleteProperty():可以替代 delete 操作符。

     Reflect.construct():可以替代 new 操作符。

9.1.7 代理另一个代理

const target = { 
 foo: 'bar' 
}; 
const firstProxy = new Proxy(target, { 
 get() { 
 console.log('first proxy'); //firstProxy调用get方法时调用
 return Reflect.get(...arguments); //相当于Reflect.get(taget,foo)
 } 
}); 
const secondProxy = new Proxy(firstProxy, { 
 get() { 
 console.log('second proxy'); 
 return Reflect.get(...arguments); 
 } 
}); 
console.log(secondProxy.foo); 
// second proxy 
// first proxy 
// bar 

9.1.8 代理的问题与不足

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 user = new User(123);
console.log(user.id); // 123,此处调用了User类方法
console.log(wm.get(user));//123

const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined,代理对象尝试从自身取得实例,相当于wm.get(this)中的this指向userInstanceProxy,因此返回undefined

//解决方法:把代理 User 实例改为代理 User 类本身
const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);//456,需加深理解

9.2 代理捕获器与反射方法

9.2.1 get()

9.2.2 set()

9.2.3 has()

9.2.4 defineProperty()

9.2.5 getOwnPropertyDescriptor()

9.2.6 deleteProperty()

9.2.7 ownKeys()

9.2.8 getPrototypeOf()

9.2.9 setPrototypeOf()

9.2.10 isExtensible()

9.2.11 preventExtensions()

9.2.12 apply()

9.2.13 construct()

9.3 代理模式

​ 使用代理可以在代码中实现一些有用的编程模式。

9.3.1 跟踪属性访问

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

console.log(proxy.name);
//Getting name
//Jake

proxy.name = 27;
//Setting name=27

9.3.2 隐藏属性

const hiddenProperties = ['foo', 'bar'];
const targetObject = {
    foo: 1,
    bar: 2,
    baz: 3
};
const proxy = new Proxy(targetObject, {
    get(target, property) {
        if (hiddenProperties.includes(property)) {
            return undefined;
        } else {
            return Reflect.get(...arguments);
        }
    },
    has(target, property) {
        if (hiddenProperties.includes(property)) {
            return false;
        } else {
            return Reflect.has(...arguments);
        }
    }
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false 
console.log('baz' in proxy); // true

9.3.3 属性验证

const target = {
    onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
    set(target, property, value) {
        if (typeof value !== 'number') {
            return false;
        } else {
            return Reflect.set(...arguments);
        }
    }
});
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1 
proxy.onlyNumbersGoHere = '2';
console.log(proxy.onlyNumbersGoHere); // 1 

9.3.4 函数与构造函数参数验证

​ 跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种 类型的值。

function median(...nums) {
    return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
    apply(target, thisArg, argumentsList) {
        for (const arg of argumentsList) {
            if (typeof arg !== 'number') {
                throw 'Non-number argument provided';
            }
        }
        return Reflect.apply(...arguments);
    }
});
console.log(proxy(4, 7, 1)); // 4
console.log(proxy(4, '7', 1));
// Error: Non-number argument provided 

​ 类似地,可以要求实例化时必须给构造函数传参:

class User {
    constructor(id) {
        this.id_ = id;
    }
}
const proxy = new Proxy(User, {
    construct(target, argumentsList, newTarget) {
        if (argumentsList[0] === undefined) {
            throw 'User cannot be instantiated without id';
        } else {
            return Reflect.construct(...arguments);
        }
    }
});
new proxy(1);
new proxy();
// Error: User cannot be instantiated without id

9.3.5 数据绑定与可观察对象

​ 通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的代码互操作。 比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:

const userList = [];
class User {
    constructor(name) {
        this.name_ = name;
    }
}
const proxy = new Proxy(User, {
    construct() {
        const newUser = Reflect.construct(...arguments);
        userList.push(newUser);
        return newUser;
    }
});
new proxy('John');
new proxy('Jacob');
new proxy('Jingleheimerschmidt');
console.log(userList);
// [
//     User { name_: 'John' },
//     User { name_: 'Jacob' },
//     User { name_: 'Jingleheimerschmidt' }
// ]

​ 另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息

const userList = [];
function emit(newValue) {
    console.log(newValue);
}
const proxy = new Proxy(userList, {
    set(target, property, value, receiver) {
        const result = Reflect.set(...arguments);
        if (result) {
            emit(Reflect.get(target, property, receiver));
        }
        return result;
    }
});
proxy.push('John');
// John
//1
console.log(proxy);//[ 'John' ]
proxy.push('Jacob');
// Jacob
//2
console.log(proxy);//[ 'John', 'Jacob' ]
posted @ 2021-09-01 16:48  unuliha  阅读(104)  评论(0)    收藏  举报