ES6新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。

具体就是,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。

在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。

兼容性:代理的行为实际上是无可替代的(无法polyfill模拟)。

元编程及抽象的新天地

代理

可以用作目标对象的替身,但又完全独立于目标对象。

空代理:除了作为一个抽象的目标对象,其他什么也不做。在代理对象上执行的所有操作都会无障碍地传播到目标对象。

代理的构造函数接收两个(必选)参数:目标对象和处理程序对象。缺少任一一个都会抛出TypeError。

空代理:可以传一个简单的对象字面量作为处理程序对象,从而让所有操作畅通无阻地抵达目标对象。唯一可感知的不同就是代码中操作的是代理对象。

const target = {
    id: 'target'
};

const handler = {}; // 处理程序对象

// 创建空代理
// 除了作为一个抽象的目标对象,什么也不做
const proxy = new Proxy(target, handler);

// 在目标对象和代理对象上获取属性值
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

// console.log( target instanceof Proxy ); // TypeError: Function has non-object prototype 'undefined' in instanceof check
// console.log( proxy instanceof Proxy ); // TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log( Proxy.prototype ); // undefined

console.log( target === proxy ); // false

const p1 = Object.create( proxy );
console.log( p1.__proto__ === proxy ); // true

捕获器

使用代理的主要目的是可以定义捕获器(trap)。

捕获器就是在处理程序对象中定义的“基本操作的拦截器”。

每个处理程序对象可以包含0个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或间接在代理对象上调用。代理可以在基本操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。

只有在代理对象上执行这些操作才会触发捕获器。

捕获器参数和反射API

所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。

开发者并不需要手动重建原始行为,而是可以通过调用全局Reflect对象上(封装了原始行为)的同名方法来轻松重建。

处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API方法。这些方法与捕获器拦截的方法具有相同的名称和参数列表,而且也具有与被拦截方法相同的行为。

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

以上两种方式都是创建了空代理/透传代理。可以捕获所有方法,然后将每个方法转发给对应反射API的空代理。

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

捕获器不变式

捕获器几乎可以改变所有基本方法的行为,但也不是没有限制。

通常都会防止捕获器定义出现过于反常的行为

每个捕获的方法都知道目标对象上下文、捕获函数的参数列表,而捕获处理程序的行为必须遵循“捕获器不变式”(trap invariant)。

/*
 trap 捕获器
 基本操作的拦截器
*/
const target1 = {
    foo: 'bar'
};

const handler1 = {
    // 捕获器在处理程序对象中以方法名为键
    get() { // 对应属性的[[Get]]操作
        return 'handler override';
    }
};

const proxy1 = new Proxy(target1, handler1);
console.log( proxy1.foo );
console.log( proxy1['foo'] );
console.log( Object.create(proxy1)['foo'] );
// handler override
// handler override
// handler override
console.log( target1.foo );
console.log( target1['foo'] );
console.log( Object.create(target1)['foo'] );
// bar
// bar
// bar

/*
 捕获器参数和反射API
*/
const handler2 = {
    // 所有捕获器都可以基于自己的参数“重建”原始操作
    // 可通过调用全局Reflect对象上的同名方法(封装了原始行为)来重建
    get(trapTarget, property, receiver) {
        console.log( trapTarget === target1 );
        console.log( property );
        console.log( receiver === proxy2 );
        // return trapTarget[property];
    }
};
const proxy2 = new Proxy(target1, handler2);
proxy2.foo;
// true
// foo
// true

const handler3 = {
    // get() {
    //     return Reflect.get( ...arguments );
    // }
    get: Reflect.get
};
const proxy3 = new Proxy(target1, handler3);
console.log( proxy3.foo ); // bar

const proxy4 = new Proxy(target1, Reflect);
console.log( proxy4.foo ); // bar

/*
 反射API准备好了样板代码,开发者可在此基础上修改捕获的方法
*/
const target2 = {
    foo: 'bar',
    baz: 'qux'
};
const handler5 = {
    get(trapTarget, property, receiver) {
        let decoration = '';
        if(property === 'foo') {
            decoration = '!!!';
        }
        return Reflect.get(...arguments) + decoration;
    }
};
const proxy5 = new Proxy(target2, handler5);
console.log( proxy5.foo );
console.log( proxy5.baz );
// bar!!!
// qux
console.log( target2.foo );
console.log( target2.baz );
// bar
// qux

/*
 捕获器不变式: 防止捕获器定义出现过于反常的行为
*/
const target3 = {};
Object.defineProperty(target3, 'foo', {
    value: 'bar',
    writable: false,
    configurable: false
});
const handler6 = {
    get() {
        return 'qux';
    }
};
const proxy6 = new Proxy(target3, handler6);
// console.log( proxy6.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')

可撤销代理

有时可能需要中断代理对象与目标对象之间的联系。不然这种联系会在代理对象的生命周期内一直持续存在。

Proxy的静态方法revocable(),支持创建可撤销的代理对象(工厂方法)。撤销代理的操作是不可逆的。撤销代理之后再调用代理会抛出TypeError。

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

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

// ...
revoke();
// ...

可能的应用场景: 给第三方库传对象的代理,想要做的事做完就revoke(),避免后续影响。

/*
 创建可撤销的代理
*/
target3.baz = 'qux';
const handler7 = {
    get() {
        return 'intercepted';
    }
};
const { proxy: proxy7, revoke } = Proxy.revocable(target3, handler7);
console.log( 'proxy7', proxy7.baz ); // proxy7 intercepted
console.log( target3.baz ); // qux
revoke();
// console.log( proxy7.baz ); // TypeError: Cannot perform 'get' on a proxy that has been revoked

实用的反射API(底层操作)

某些情况下应该优先使用反射API。

  1. Reflect API与Object API

    反射API并不限于捕获处理程序;大多数反射API方法在Object类型上有对应的方法。

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

  2. 状态标记

    很多反射方法返回称作”状态标记“的布尔值,表示意图执行的操作是否成功。

    以下反射方法都会提供状态标记:

    • Reflect.defineProperty()
    • Reflect.preventExtensions()
    • Reflect.setPrototypeOf()
    • Reflect.set()
    • Reflect.deleteProperty()
  3. 用一等函数替代操作符

    以下反射方法提供只有通过操作符才能完成的操作:

    • Reflect.get() 对象属性访问
    • Reflect.set() 对象属性赋值
    • Reflect.has() in操作符或者with()
    • Reflect.deleteProperty() delete操作符
    • Reflect.construct() new操作符
  4. 安全地应用函数

    Reflect.apply(myFunc, thisVal, argumentsList)

    替代Function.prototype.apply.call(myFunc, thisVal, argumentsList)。

    避免被调用的函数可能定义了自己的apply属性(虽然可能性极小)。

/*
 Reflect API
*/
// 1.状态标记    意图执行的操作是否成功
const o = {};
try {
    Object.defineProperty( o, 'foo', 'bar' );
    console.log( 'success' );
} catch(e) {
    console.log( 'failure' );
}
// failure

/*if(Reflect.defineProperty( o, 'foo', 'bar')) {
    console.log( 'success' );
} else {
    console.log( 'failure' );
}*/
// TypeError: Property description must be an object: bar

Object.defineProperty( o, 'foo', {
    value: 'bar',
    writable: false,
    configurable: false
} );
if(Reflect.defineProperty( o, 'foo', { value: 'bar' })) {
    console.log( 'success' );
} else {
    console.log( 'failure' );
} // success

if(Reflect.defineProperty( o, 'foo', { value: 'bar11' })) {
    console.log( 'success' );
} else {
    console.log( 'failure' );
} // failure

console.log( Reflect.get( o, 'foo' ) ); // bar    相当于o.foo


function TestReflect() {}
const tr = Reflect.construct(TestReflect, []);
console.log( tr instanceof TestReflect ); // true

代理另一个代理

可以在一个目标对象之上构建多层拦截网

/*
 在一个目标对象之上构建多层拦截网
*/
const firstProxy = new Proxy(target, {
    get() {
        console.log( 'first proxy' );
        return Reflect.get( ...arguments );
    }
});
const secondProxy = new Proxy(firstProxy, {
    get() {
        console.log( 'second proxy' );
        return Reflect.get( ...arguments );
    }
});
console.log( secondProxy.id );
// second proxy
// first proxy
// bar

代理的问题与不足

  1. 代理中的this

    如果目标对象存在方法依赖于对象标识,就可能碰到意料之外的问题。

    可以把代理User实例改为代理User类

  2. 代理与内部槽位

    ECMAScript内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错。

    典型例子:Date类型方法的执行依赖this值上的内部槽位[[NumberDate]];而代理对象上不存在这个内部槽位。这个内部槽位的值也无法通过普通的get()和set()操作访问到。会抛出TypeError

/*
 代理中存在的问题
 this的指向
*/
const target4 = {
    thisValEqualsProxy() {
        return this === proxy8;
    }
};
const proxy8 = new Proxy(target4, {});
console.log( target4.thisValEqualsProxy() ); // false
console.log( proxy8.thisValEqualsProxy() ); // true

// 目标对象依赖于对象标识的情况
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
const userInstanceProxy = new Proxy(user, Reflect);
console.log( userInstanceProxy.id ); // undefined

// 把代理User实例改为代理User类本身
const UserClassProxy = new Proxy(User, Reflect);
const proxyUser = new UserClassProxy(456);
console.log( proxyUser.id ); // 456


const target5 = {
    obj: {
        a: 'lily'
    }
};
const handler8 = {
    get(target, property, receiver) {
        console.log( 'get()', property );
        return Reflect.get(...arguments);
    },
    set(target, property, value, receiver) {
        console.log( 'set()' );
        return Reflect.set(...arguments);
    }
};
const proxy9 = new Proxy(target5, handler8);
proxy9.obj.a;
proxy9.obj.a = 'lucy';

const target6 = new Date();
const proxy10 = new Proxy( target6, Reflect );
console.log( proxy10 instanceof Date ); // true
target6.getDate();
// 无内部槽位[[NumberDate]],也无法通过普通的get()和set()操作访问到
proxy10.getDate(); // TypeError: this is not a Date object. 

代理捕获器与反射方法(反射API/拦截器方法)

代理可以捕获13种不同的基本操作。

在代理对象上执行的任一操作,只会有一个捕获处理程序被调用。不会重复捕获。

只要在代理上调用,所有捕获器都会拦截它们对应的反射API操作。

  1. get(target, property, receiver):返回值无限制。

    不变式:

    • target.property为数据属性时,如果不可写且不可配置,则处理程序返回的值必须与target.property匹配;
    • target.property为访问器属性时,如果[[Get]]为undefined且不可配置,则处理程序的返回值也必须是undefined。
  2. set(target, property, value, receiver):返回布尔值。

    不变式:

    • target.property为数据属性,如果不可写且不可配置,则不能修改目标属性的值;
    • target.property为访问器属性,如果[[Set]]为undefined且不可配置,则不能修改目标属性的值,严格模式下,会抛出TypeError。
  3. has(target, property):必须返回布尔值。非布尔值会转型。

    不变式:

    • target.property存在且不可配置,必须返回true;
    • target.property存在且目标对象不可扩展,必须返回true。
  4. defineProperty(target, property, descriptor):必须返回布尔值。非布尔值会转型。

    不变式:

    • 目标对象不可扩展,则无法定义属性;
    • 有一个可配置的属性,添加同名的不可配置属性会覆盖;
    • 有一个不可配置的属性,添加同名的属性会失败。
  5. getOwnPropertyDescriptor(target, property):必须返回对象,或者undefined(属性不存在)

    不变式:

    • 如果target.property存在且不可配置,必须返回一个表示该属性存在的对象;
    • 如果target.property存在且可配置,必须返回表示该属性可配置的对象;
    • 如果target.property存在且target不可扩展,必须返回一个表示该属性存在的对象;
    • 如果target.property不存在且target不可扩展,必须返回undefined表示该属性不存在;
    • 如果target.property不存在,则不能返回表示该属性可配置的对象。
  6. deleteProperty(target, property):必须返回布尔值。非布尔值会转型。

    不变式:

    • 如果target.property存在且不可配置,则不能删除
  7. ownKeys(target):必须返回包含字符串或符号的可枚举对象。

    不变式:

    • 返回的可枚举对象必须包含target的所有不可配置的自有属性;
    • 如果target不可扩展,则返回的可枚举对象必须准确地包含自有属性键。
  8. getPrototypeOf(target):必须返回对象或null。

    不变式:

    • 如果target不可扩展,则Object.getPrototypeOf(proxy)唯一有效的返回值就是Object.getPrototypeOf(target)的返回值。
  9. setPrototypeOf(target, prototype):必须返回布尔值。非布尔值会转型。

    不变式:

    • 如果target不可扩展,则唯一有效的prototype参数就是Object.getPrototypeOf(target)的返回值。
  10. isExtensible(target):必须返回布尔值。非布尔值会转型。

    不变式:

    • 如果target可扩展,必须返回true
    • 如果target不可扩展,必须返回false
  11. preventExtensions(target):必须返回布尔值。非布尔值会转型。

    不变式:

    • 如果Object.isExtensible(proxy)是false,必须返回false。
  12. apply(target, thisArg, argumentsList)

    不变式:

    • target必须是一个函数对象。
  13. construct(target, argumentsList, newTarget)

    不变式:

    • target必须可以用作构造函数。
const myTarget = {
    *[Symbol.iterator]() {
        yield *[1,2,3];
    },
    t1: 't1'
};
const proxy = new Proxy(myTarget, {
    get(target, property, receiver) {
        console.log( 'get()' );
        return Reflect.get( ...arguments );
    }
});
proxy.foo; // get()
proxy[Symbol.iterator]; // get()
for (const item of myTarget) {
    console.log( item );
}
// 1
// 2
// 3
proxy['foo']; // get()
Object.create(proxy)['foo']; // get()
Reflect.get(proxy, 'foo'); // get()

/*function Test() {}
Test.prototype[Symbol.iterator] = function* () {
    yield *[1,2,3];
};
for (const item of new Test()) {
    console.log( item );
}*/
// 1
// 2
// 3

Object.defineProperty(myTarget, 'foo', {
    value: 'foo',
    writable: false,
    configurable: false
});
Object.defineProperty(myTarget, 'bar', {
    set: value => {
        console.log( 'set bar' );
        return value;
    },
    configurable: false
});
Object.defineProperty(myTarget, 'baz', {
    set: value => {
        this.baz_ = value;
    },
    get: () => {
        return this.baz_;
    }
});
const proxy1 = new Proxy(myTarget, {
    get(target, property, receiver) {
        // console.log(property, receiver === proxy1);
        return 'foo1';
    },
    set(target, property, value, receiver) {
        console.log( 'set()' );
        return Reflect.set(...arguments);
    },
    has(target, property) {
        console.log( 'has()' );
        return Reflect.has(...arguments);
    },
    defineProperty(target, property, descriptor) {
        console.log( 'defineProperty()' );
        return Reflect.defineProperty(...arguments);
    },
    ownKeys(target) {
        console.log( 'ownKeys()' );
        return Reflect.ownKeys(...arguments);
    }
});
// proxy1.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 'foo' but got 'foo1')
// proxy1.bar; // TypeError: 'get' on proxy: property 'bar' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got 'foo1')

proxy1.bar = 'bar';
// set()
// set bar
Reflect.set(proxy1, 'bar', 'bar1');
// set()
// set bar
proxy1.baz = 'bazzz'; // set()
console.log( proxy1.baz ); // foo1

'foo' in proxy1; // has()
// for(const key in proxy1) { console.log( key ); }
with(proxy1) {
    (baz);
}
// has()
Reflect.has(proxy1, 'baz'); // has()
'foo' in Object.create(proxy1); // has()

let obj = Object.create(proxy1);
console.log( obj ); // Object [foo1] {}
// console.log( obj.bar ); // TypeError: 'get' on proxy: property 'bar' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got 'foo1')

//console.log( obj.baz );
//// defineProperty
// Object.defineProperty( proxy1, 'foo', { value: 'bar1' } ); // TypeError: 'defineProperty' on proxy: trap returned falsish for property 'foo'

Object.defineProperty( proxy1, 'foo1', { value: 'bar1' } );
// defineProperty()

/*Object.defineProperty( proxy1, 'bar', { value: 'bar1' } );*/
// defineProperty()
// TypeError: 'defineProperty' on proxy: trap returned falsish for property 'bar'

Object.defineProperty( proxy1, 't1', { value: 't2', configurable: false } );
// defineProperty()

/*Object.defineProperty( proxy1, 'baz', {
    value: 1,
    configurable: true
});*/
// defineProperty()
// TypeError: 'defineProperty' on proxy: trap returned falsish for property 'baz'

Object.defineProperty( myTarget, 't2', { 
    set: value => {
        this.baz_ = value;
    },
    get: () => {
        return this.baz_;
    },
    configurable: true
} );
Object.defineProperty( proxy1, 't2', {
    value: 1,
    configurable: true
});
console.log( Object.getOwnPropertyDescriptor( myTarget, 't2' ) );
// { value: 1, writable: false, enumerable: false, configurable: true }

console.log( Reflect.ownKeys( myTarget ) );
// [ 't1', 'foo', 'bar', 'baz', 'foo1', 't2', Symbol(Symbol.iterator) ]
console.log( Reflect.ownKeys( proxy1 ) );
// ownKeys()
// [ 't1', 'foo', 'bar', 'baz', 'foo1', 't2', Symbol(Symbol.iterator) ]
console.log( Object.keys( proxy1 ) );
// ownKeys()
// [ 't1' ]
console.log( Object.getOwnPropertyNames( proxy1 ) );
// ownKeys()
// [ 't1', 'foo', 'bar', 'baz', 'foo1', 't2' ]
console.log( Object.getOwnPropertySymbols( proxy1 ) );
// ownKeys()
// [ Symbol(Symbol.iterator) ]

const targetFunc = function() {};
const proxy2 = new Proxy(targetFunc, {
    construct(target, argumentsList, newTarget) {
        console.log( 'construct()' );
        return Reflect.construct(...arguments);
    }
})
new proxy2; // construct()

代理模式(应用)

  1. 跟踪属性访问

    通过捕获get、set和has等操作,可以知道对象属性什么时候被访问、被查询。

  2. 隐藏属性

    代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。

  3. 属性验证

    所有赋值操作都会触发set()捕获器,可以根据所赋的值决定是允许还是拒绝赋值。

  4. 函数与构造函数参数验证

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

  5. 数据绑定与可观察对象

    使用场景:

    • 可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中。
    • 还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息。
/*
 * 代理应用
*/


/*跟踪属性访问*/
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);
    }
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27


/*隐藏属性*/
const hiddenProperties = ['foo', 'bar'];
const targetObj = {
    foo: 1,
    bar: 2,
    baz: 3
};
const proxy1 = new Proxy(targetObj, {
    get(target, property, receiver) {
        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);
        }
    }
});
console.log( proxy1.foo );
console.log( proxy1.bar );
console.log( proxy1.baz );
// undefined
// undefined
// 3
console.log( 'foo' in proxy1 );
console.log( 'bar' in proxy1 );
console.log( 'baz' in proxy1 );
// false
// false
// true

/*对属性赋值做判断限制*/
const target2 = {
    onlyNumbersGoHere: 0
};
const proxy2 = new Proxy(target2, {
    set(target, property, value, receiver) {
        if(typeof value !== 'number') {
            return false;
        } else {
            return Reflect.set(...arguments);
        }
    }
});
proxy2.onlyNumbersGoHere = 1;
console.log( proxy2.onlyNumbersGoHere ); // 1
proxy2.onlyNumbersGoHere = 's';
console.log( proxy2.onlyNumbersGoHere ); // 1

/*函数与构造函数参数验证*/
function median(...nums) {
    return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy3 = new Proxy(median, {
    apply(target, thisArg, argumentsList) {
        console.log( argumentsList );
        for(const arg of argumentsList) {
            if(typeof arg !== 'number') {
                throw 'Non-number argument provided';
            }
        }
        return Reflect.apply(...arguments);
    }
});
console.log( proxy3(4, 7, 1) ); // 4
// console.log( proxy3(4, '7', 1) ); // Non-number argument provided

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


/*数据绑定与可观察对象*/
const userList = [];

function emit(newValue) {
    console.log( newValue );
}

const proxy5 = new Proxy(User, {
    construct(target, argumentsList, newTarget) {
        const newUser = Reflect.construct(...arguments);
        // userList.push(newUser);
        proxy6.push(newUser);
        return newUser;
    }
});

const proxy6 = new Proxy(userList, {
    set(target, property, value, receiver) {
        const result = Reflect.set(...arguments);
        if(result) {
            emit(Reflect.get(target, property, receiver));
        }
        return result;
    }
});

new proxy5(1);
new proxy5(2);
new proxy5(3);

console.log( userList ); // [ User { id_: 1 }, User { id_: 2 }, User { id_: 3 } ]

beckyyyy
550 声望414 粉丝

工作多年的一只前端菜鸟