前言

很多同学在学习es6的时候都会产生这么一个疑问, 就是个 Reflect 到底是个啥, 它到底有什么作用,

其实要说明它的作用只需要一句话, 就是调用对象的基本方法, 或者叫基本操作或者内部方法都是一个意思

一句话结束了, 那剩下的事情就是要解释一下什么叫基本方法, 以及它的作用.

对象基本方法

这是ES官方文档里面的描述:

internal-methods

这是什么意思呢, 就是说作为一个js开发者, 无论你怎么操作一个对象, 最终都要对应到这里面的内部方法, 比如我们使用这么一个语法:

const obj = {};

obj.a = 1; // 最终访问了一个内部方法 [[Set]]

console.log(obj.a); // [[Get]]

也就是说, 我们书写的语法层面的代码, 它最终运行的时候, 实际上运行的是这个对象的内部方法, 对一个对象的所有操作, 都不能避免这一点.

Reflect 是什么

它是一个包含了与所有内部方法对应的静态方法的类, 它不能创建对象, 就和Math一样, 所有方法都是静态的, 它可以直接访问对象的内部方法, 上面的语法使用Reflect就是这样:

const obj = {};

Reflect.set(obj, 'a', 1);

console.log(Reflect.get(obj, 'a'));

这就是直接调用对象的内部[[Get]] / [[Set]] 方法来对对象添加键值和访问键, 在es6之前我们没有任何办法直接调用内部方法, 现在可以了. 官方还给我们提供了一个表格, 说明了对象的内部方法和反射方法的对应关系:

IM-Ref.png

肯定有人疑惑了, 这么整的意义是什么, 我直接通过点属性名去get/set不就行了, 为什么需要这玩意?

这是因为, 如果你通过点语法就意味着你是间接去调用的内部方法, 这和直接调用内部方法是有区别的, 区别在哪呢?

我举例说明, 当大家使用某种语法操作对象时, 它会经过一系列规则和步骤, 这些规则和步骤中的某一步就是在调用内部方法, 好理解吧, 就相当于语法层面的操作是一个高级封装的功能, 而内部方法只是其中的核心实现, 每种语法对应了不同的规则和步骤, 如果你不希望有任何额外的规则和步骤存在, 你就需要直接调用内部方法.

比如下面的对象, 有abc三个属性, 其中c是一个getter属性

const obj = {
a: 1,
b: 2,
get c(){
return this.a + this.b;
}
};

console.log(obj.c); // [[Get]] // 3

我们看一下[[Get]] 内部方法的定义:

get-intl

其中第二个参数Receiver是一个指定的this对象, 因为有时候我们访问一个对象属性时, 这个属性可能是一个getter, 它会执行一个函数, 这个函数内部的this就是这个Receiver.

上面的代码, 我们通过点属性的语法访问c属性, getter函数中默认的this是obj, 因此输出3.

此时看出问题没, 当我想改变getter中的this指向时, 我们完全没有任何办法, 但是通过Reflect就很容易做到:

const obj = {
a: 1,
b: 2,
get c(){
return this.a + this.b;
}
};

console.log(Reflect.get(obj, 'c', { a: 3, b: 4})); // [[Get]] // 7

我们将{ a: 3, b: 4}作为this来访问c属性, 此时得到的结果是7. 我想现在你应该体验到区别了吧, 语法层面. 即点属性, 这种方式已经固定了this指向, 它实际上大致做了以下 ‘规则和步骤’:

  1. 定义thisobj
  2. 调用Reflect.get(obj, 'c', obj)

当你不想要语法带来的额外规则和步骤时, 你就可以直接使用Reflect.

但到这可能又有人疑惑了, 这能干嘛呢, 谁没事会这样写代码呢, 别急, 接下来再通过一个例子加深理解

Reflect 应用

ES6中的Proxy大家不陌生吧, Reflect最常用的使用场景就是在Proxy中, 比如:

const proxy = new Proxy(obj, {
get(target, key) {
console.log('read', key);
return target[key];
}
});

proxy.a; // read a
proxy.c // read c

大家对上面访问c的结果意外吗, 按照期望, 访问c时, 由于c是一个getter, 其中还访问了a和b, 应该打印三次 read c/a/b,

为啥呢?

因为你在proxy的get中返回的时候, 直接返回了target[key] target是原始obj对象, 它的c属性getter中的this指向的是obj自身, 因此你访问proxy.c当然只打印read c了, 因为a和b的访问是发生在obj上, 并不是proxy上, 因此我们可以这么写来解决这个问题:

const proxy = new Proxy(obj, {
get(target, key) {
console.log('read', key);
return Reflect.get(target, key, proxy);
}
});

proxy.a; // read a
proxy.c // read c / a / b

我们修改了返回值, 强制让所有的属性访问都发生在proxy对象上, 这样就能完整的拦截所有proxy属性的访问了, 如果你对vue3的源码有所了解, 你可以看看它这块的处理.

我在举一个应用例子

const obj = {
a: 1,
b: 2,
}

const keys = Object.keys(obj)
console.log(keys); // a b

实际上Object.keys也会调用内部方法[[GetOwnProperty]]

keys-intl

但是它不是直接调用的, 它有额外的规则和步骤, 比如下面的代码运行后依然没有c属性, 也没有symbol属性.

Object.defineProperty(obj. 'c', {
value: 3,
enumerable: false
})

console.log(keys); // ['a', 'b']

obj[Symbol()] = 4;

这就说明了通过语法api去访问对象方法, 它不是直接调用基本方法, 它会有语法api特定的逻辑, 官方文档中看一下对Object.keys的方法说明:

keys-step

keys-step-2

可以很明显看到, 第二个大步骤中的第一步就是调用基本方法获取对象所有的key, 后续再有一系列的处理最终得到Object.keys的结果.

所以我如果不希望有这些处理, 我就想直接拿到对象所有的key, 那我们就可以直接调用反射方法:

const keys = Reflect.ownKeys(obj);
console.log(keys); // ['a', 'b', 'c', Symbol()]

可以看到结果包含了对象中所有的定义的key.

总结

再看开始的一句话, Reflect就是直接调用对象的基本方法(内部方法), 在es6之前, 我们没有这个能力, 现在通过它我们可以触碰到更底层的基本操作, 有了它之后, 其实你还可以有更多的想象, 比如针对对象提供的官方的方法, 你都可以模拟了, 修改扩展api语法层面的”规则和步骤”.