关于前端:JavaScript高级程序设计笔记09-代理与反射

代理与反射

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 } ]

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理