1

什么是Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

与大多数全局对象不同Reflect并非一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect对象作为一个函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)。

Reflect 对象提供了以下静态方法,这些方法与proxy handler methods的命名相同.

其中的一些方法与 Object相同, 尽管二者之间存在 某些细微上的差别

静态方法

对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。

对构造函数进行 new 操作,相当于执行 new target(...args)

Object.defineProperty() 类似。如果设置成功就会返回 true

作为函数的delete操作符,相当于执行 delete target[name]

获取对象身上某个属性的值,类似于 target[name]。

类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,  否则返回 undefined.

类似于 Object.getPrototypeOf()

判断一个对象是否存在某个属性,和 in 运算符的功能完全相同。

类似于 Object.isExtensible().

返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).

类似于 Object.preventExtensions()。返回一个Boolean

将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true

设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。

实例

检测一个对象是否存在特定属性

const duck = {
  name: 'Maurice',
  color: 'white',
  greeting: function() {
    console.log(`Quaaaack! My name is ${this.name}`);
  }
}

Reflect.has(duck, 'color');
// true
Reflect.has(duck, 'haircut');
// false

返回这个对象自身的属性

Reflect.ownKeys(duck);
// [ "name", "color", "greeting" ]

为这个对象添加一个新的属性

Reflect.set(duck, 'eyes', 'black');
// returns "true" if successful
// "duck" now contains the property "eyes: 'black'"

什么是Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

// `target`要使用 `Proxy` 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// `handler`一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 `p` 的行为。
const p = new Proxy(target, handler)

// traps 提供属性访问的方法。这类似于操作系统中捕获器的概念。
const p = new Proxy(target, { traps })

// 创建一个可撤销的`Proxy`对象。
const p = new Proxy.revocable()(target, handler)

创建一个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属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
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); // 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
// 严格相等可以用来区分Proxy和target
console.log(target === proxy); // false

traps捕获器使用

定义一个 get() 捕获器,在 ECMAScript 操作以某种形式调用 get() 时触发。

const target = {
    foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
    get() {
        return 'handler override';
    }
};
const proxy = new Proxy(target, handler);

当通过target执行 get() 操作时,就会触发定义的 get() 捕获器。当然, get() 不是ECMAScript 对象可以调用的方法。这个操作在 JavaScript 代码中可以通过多种形式触发并被 get() 捕获器拦截到。 proxy[property] 、 proxy.property 或 Object.create(proxy)[property] 等操作都会触发基本的 get() 操作以获取属性。因此所有这些操作只要发生在代理对象上,就会触发 get() 捕获器。

注意,只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。

const target = {
    foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
    get() {
        return 'handler override';
    }
};
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override
console.log(target['foo']); // bar
console.log(proxy['foo']); // handler override
console.log(Object.create(target)['foo']); // bar
console.log(Object.create(proxy)['foo']); // handler override

traps捕获器参数

所有traps都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。比如, get()捕获器会接收到目标对象、要查询的属性和代理对象三个参数。

const target = {
    foo: 'bar'
};
const handler = {
    get(trapTarget, property, receiver) {
        console.log(trapTarget === target);
        console.log(property);
        console.log(receiver === proxy);
    }
};
const proxy = new Proxy(target, handler);
proxy.foo;
// true
// foo
// true

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

const target = {
    foo: 'bar'
};
const handler = {
    get(trapTarget, property, receiver) {
        return trapTarget[property];
    }
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

所有捕获器都可以基于自己的参数重建原始操作,但并非所有捕获器行为都像 get() 那么简单。因此,通过手动写码如法炮制的想法是不现实的。实际上,开发者并不需要手动重建原始行为,而是可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来轻松重建。
处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。因此,使用反射 API 也可以像下面这样定义出空代理对象:

const target = {
    foo: 'bar'
};
const handler = {
    get() {
        return Reflect.get(...arguments);
    }
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

甚至还可以写得更简洁一些:

const target = {
    foo: 'bar'
};
const handler = {
    get: Reflect.get
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

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

const target = {
    foo: 'bar'
};
const proxy = new Proxy(target, Reflect);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

反射 API 为开发者准备好了样板代码,在此基础上开发者可以用最少的代码修改捕获的方法。比如,下面的代码在某个属性被访问时,会对返回的值进行一番修饰:

const target = {
    foo: 'bar',
    baz: 'qux'
};
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); // bar!!!
console.log(target.foo); // bar
console.log(proxy.baz); // qux
console.log(target.baz); // qux

traps捕获器保持行为

使用traps几乎可以改变所有基本方法的行为,但也不是没有限制。根据 ECMAScript 规范,每个捕获的方法都知道target上下文、捕获函数签名,而捕获处理程序的行为必须遵循 "trap invariant"。traps保持行为因方法不同而异,但通常都会防止traps定义出现过于反常的行为。
比如,如果target有一个不可配置且不可写的数据属性,那么在traps返回一个与该属性不同的值时,会抛出 TypeError :

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

可撤销Proxy

有时候可能需要中断Proxy与target之间的联系。对于使用 new Proxy() 创建的普通Proxy来说,这种联系会在Proxy的生命周期内一直持续存在。
Proxy 也暴露了 revocable() 方法,这个方法支持撤销Proxy与target的关联。撤销Proxy的操作是不可逆的。而且,撤销函数( revoke() )是幂等的,调用多少次的结果都一样。撤销Proxy之后再调用Proxy会抛出 TypeError 。
撤销函数和Proxy对象是在实例化时同时生成的:

const target = {
    foo: 'bar'
};
const handler = {
    get() {
        return 'intercepted';
    }
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar

revoke();

console.log(proxy.foo); // TypeError

Reflect API

某些情况下应该优先使用Reflect API

1.Reflect API 与Object API

在使用Reflect API 时:
(1) Reflect API并不限于捕获处理程序;
(2) 大多数Reflect API 方法在 Object 类型上有对应的方法。
通常, Object 上的方法适用于通用程序,而Reflect方法适用于细粒度的对象控制与操作。

2.状态标记

很多Reflect方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的Reflect API 方法更有用。例如,可以使用Reflect API 对下面的代码进行重构:

// 初始代码
const o = {};
try {
    Object.defineProperty(o, 'foo', 'bar');
    console.log('success');
} catch(e) {
    console.log('failure');
}

在定义新属性时如果发生问题, Reflect.defineProperty() 会返回 false ,而不是抛出错误。因此使用这个反射方法可以这样重构上面的代码:

// 重构后的代码
const o = {};
if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
    console.log('success');
} else {
    console.log('failure');
}

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

  • Reflect.defineProperty()
  • Reflect.preventExtensions()
  • Reflect.setPrototypeOf()
  • Reflect.set()
  • Reflect.deleteProperty()

3. 用函数替代操作符

以下Reflect方法提供只有通过操作符才能完成的操作。

  • Reflect.get() :可以替代对象属性访问操作符。
  • Reflect.set() :可以替代 = 赋值操作符。
  • Reflect.has() :可以替代 in 操作符或 with() 。
  • Reflect.deleteProperty() :可以替代 delete 操作符。
  • Reflect.construct() :可以替代 new 操作符。

4. 安全地应用函数

在通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 属性(虽然可能性极小)。为绕过这个问题,可以使用定义在 Function 原型上的 apply 方法,比如:

Function.prototype.apply.call(myFunc, thisVal, argumentList)

这种可怕的代码完全可以使用 Reflect.apply 来避免:

Reflect.apply(myFunc, thisVal, argumentsList);

Proxy嵌套使用

Proxy可以拦截Reflect API 的操作,而这意味着完全可以创建一个新的Proxy,实现嵌套操作数据。这样就可以在一个target上构建多层拦截网:

const target = {
    foo: 'bar'
};
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.foo);
// second proxy
// first proxy
// bar

Proxy的问题与不足

Proxy是在 ECMAScript 现有基础之上构建起来的一套新 API,因此其实现已经尽力做到最好了。很大程度上,Proxy作为对象的虚拟层可以正常使用。但在某些情况下,Proxy也不能与现在的 ECMAScript机制很好地协同。

  1. Proxy中的 this

Proxy潜在的一个问题来源是 this 值。我们知道,方法中的 this 通常指向调用这个方法的对象:

const target = {
    thisValEqualsProxy() {
        return this === proxy;
    }
}
const proxy = new Proxy(target, {});
console.log(target.thisValEqualsProxy()); // false
console.log(proxy.thisValEqualsProxy()); // true

从直觉上讲,这样完全没有问题:调用代理上的任何方法,比如 proxy.outerMethod() ,而这个方法进而又会调用另一个方法,如 this.innerMethod() ,实际上都会调用 proxy.innerMethod() 。多数情况下,这是符合预期的行为。可是,如果target依赖于对象标识,那就可能碰到意料之外的问题。

一个通过 WeakMap 保存私有变量的例子:

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

由于这个实现依赖 User 实例的对象标识,在这个实例被代理的情况下就会出问题:

const user = new User(123);
console.log(user.id); // 123
const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined

这是因为 User 实例一开始使用目标对象作为 WeakMap 的键,代理对象却尝试从自身取得这个实例。要解决这个问题,就需要重新配置代理,把代理 User 实例改为代理 User 类本身。之后再创建代理的实例就会以代理实例作为 WeakMap 的键了:

const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);
2.代理与内部槽位
代理与内置引用类型(比如 Array )的实例通常可以很好地协同,但有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错。
一个典型的例子就是 Date 类型。根据 ECMAScript 规范, Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]] 。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError :
const target = new Date();
const proxy = new Proxy(target, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: 'this' is not a Date object

// 这样写就没有问题
const proxyDate = new Proxy(Date, {});
const date = new proxyDate();
date.getDate();
console.log(date.getDate());

Proxy实际应用

跟踪属性访问

通过捕获 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);
    }
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27

隐藏属性

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

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

属性验证

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

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

函数参数验证

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

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

数据绑定与可观察对象

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

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 {}, User {}, User{}]

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

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
proxy.push('Jacob');
// Jacob

万年打野易大师
1.5k 声望1.1k 粉丝