前端你不知道的反射——Reflect和Proxy

1,768 阅读11分钟

我正在参加「掘金·启航计划」

背景

这是我第一次写基本知识普及的帖子,主题是前端的【反射】。个人认为写这种知识普及帖子难度比写实践的帖子更难一些,实践的帖子是先有了实践,然后认真总结按步骤列出解决思路和方案就可以了。而知识的普及帖子就空洞和难一些了,当然也少不了一些代码实践,但是要说明白或者说的有特点就很难了。这次还是充当搬砖的角色,自己总结,参考权威的,官方的,其他个人的观点,吸收过来,然后试图根据自己的理解讲明白。

当然任何的对语言特性的研究都应该有其应用场景的,不然无异于无病呻吟,其实对于vue实现了解的人大概都会知道vue2和vue3对于对象操作的监听方式是不一样的。

const obj = {
  name: 'siger',
  age: 35,
};

// 1. vue2中利用Object.defineProperty来做对象操作的依赖收集
for (const key of Object.keys(obj)) {
  let value = obj[key];
  Object.defineProperty(obj, key, {
    set(newValue) {
      // also can do something such as update view etc.
      value = newValue;
    },
    get() {
      return value;
    },
  });
}

// 2. vue3中利用Proxy来直接代理obj即可达到监控对象操作的作用
const proxyObj = new Proxy(obj, {
  set(traget, key, newValue, receiver) {
    // 这里可以做其他的一些事儿
    return Reflect.set(target, key, newValue, receiver);
  },
  get(traget, key, receiver) {
    // 这里可以做其他的一些事儿
    return Reflect.get(traget, key, receiver);
  },
})

// 通过比较两种用法是不是也感觉到后一种用法比较简单而且强大
// 第一种方式做不到对新增加的属性也进行监控
// 第二种方式则可以无感对obj对象所有属性(已有的和新增的)进行监控和感知

出于上面的一些写法和做法的疑问,在探究其原因的过程后,于是就有了这次的主题【反射】。

反射

上一节简单解释了我要写【反射】的原因,可能直接看示例场景中的代码很突兀,后边会逐步的介绍其中的使用方法。其实【反射】的概念对于前端还挺陌生的,但是我们日常都会用到。这里我们也给一个通俗的定义:

反射是在程序运行中获取和动态获取和操作自身内容的一项技术和能力。对于前端通俗点儿说(某个对象Object)就是前端程序在运行时可以有获取其属性和修改它自己的一种能力。

反射并不是只是前端有提及,几乎所有的后端语言(Java,Go)都有提及,一些静态语言需要有动态修改自己的能力,就是通过反射来做的。当然对于动态语言的javascript来说这或许更容易。

更通俗一些的认知就是,对于对象的属性获取,设置,删除等所有的操作的能力都可以归类到反射。

那么随着javascript语言的发展,反射能力也逐渐完善,由最初的Object的一系列方法(keys,defineProperty),发展到Reflect,Proxy的一系列的拦截方法。

Object

上文中引出了反射的一些基本信息,在介绍Reflect之前我想应该需要介绍一下Object构造函数上的一些方法,这些方法也是比较原始的反射用法。

Object构造函数

Object构造函可以用来创造一个包装类对象:

new Object(null) // {} 空对象
new Object()     // {} 空对象
new Object(1)    // Number {1} 数值包装类型
new Object('1')  // String {'1'} 字符串包装类型
new Object({foo: 'bar'}) // {foo: 'bar'} 传入引入类型则返回传入的类型

Object静态方法

由于介绍这些方法是为了下文做一些铺垫,重点不是详实的介绍所有的方法以及用法,所以只会列一些日常开发用的较多的方法:

  • Object.assign()
// 将源对象可枚举属性的复制到目标对象上(可以用作浅拷贝)
const targetObj = Object.assign({}, {a: 1, b: 2});
// {a: 1, b: 2}
  • Object.create()
// 该方法用于创建一个新对象基于传入的对象为原型
const source = {a: 1, b: 2};
const target = Object.create(source);
// target就是基于source为原型创建的空对象,可以访问source的属性和方法
// {} [[Prototype]]: Object a: 1 b: 2
  • Object.keys()
// 该方法返回一个给定的对象的自身可枚举的属性组成的数组
const obj = {
  a: 'somstring',
  b: 42,
  c: false,
}
console.log(Object.keys(obj));
// ['a', 'b', 'c']
  • Object.values()
// 该方法返回一个给定的对象的可枚举属性的值的集合
const obj = {
  a: 'somstring',
  b: 42,
  c: false,
}
console.log(Object.values(obj));
// ['somstring', 42, false]
  • Object.entries()
// 该方法返回一个给定的对象的可枚举属性的key和vaule的二维集合
const obj = {
  a: 'somstring',
  b: 42,
  c: false,
}
console.log(Object.entries(obj));
// [['a', 'somstring'], ['b', 42], ['c', false]]
  • Object.defineProperty()
// 该方法可以直接在一个对象上定义一个新属性或者修改一个现有属性,并返回此对象
const obj = {
  a: 'somstring',
  b: 42,
  c: false,
}
// 1. 简单使用,定义一个新的值,与obj.c = true 表现一致
Object.defineProperty(obj, 'c', {
  value: true
})
console.log(obj.c);
// true

// 2. 可以定义的属性有 configurable(false,是否该属性可修改), enumerable(false,是否可枚举),value(设置值),writable(可写),get(getter),set(setter)

let cValue = true
Object.defineProperty(obj, 'c', {
  enumerable: false,
  configerable: false,
  writable: false,
  value: true,
  get() { return cValue }, // 这样每次都在获取属性【c】的时候触发get函数返回cValue
  set(newValue){ cValue = newValue } // 这样每次设置属性【c】的值就可以触发set函数,更新cValue
})

通过简单介绍Object构造函数的几个静态方法就可以发现其实在Reflect之前前端就可以有反射的能力。比如可以通过Object.defineProperty()来获取对象的运行的状态也可以根据一定的逻辑设置已有对象的属性值或者新增属性,这本身就符合反射的定义和行为。

Reflect

做了这么多铺垫应该请出今天的主角来了-Reflect,说了这么多,前端的反射方案就是通过Reflect对象来实现。

先看一下MDN上对Reflect的描述:

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。Reflect 不是一个函数对象,因此它是不可构造的。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。

其实大概的意思是Reflect就是一个对象,提供了拦截JavsScript操作的一系列方法,当然它只有静态方法和属性,不可使用new,从名称和解释上就明确了我JavaScript从此支持了反射。

兼容性

拿Reflect.ownKeys()的兼容性来看: ownKeys

为什么要有Reflect

上文有提到Object构造函数上本身就有一些方法可以归类为反射的能力,但是为什么还要有一个Reflect呢?

我这里的理解就是这样一来明确了JavaScript官宣支持反射,同时使JS更强大,讲JS的反射机制完善后然后统一到Reflect对象上并做了一些优化和简化:

  • 简化了写法
  • 优化了实现和程序表现
const s = Symbol('foo');
const obj = {
  a: 1,
  b: 'string',
  [s]: 1,
};
// 1. 获取对象的key来说
// 原先Object构造函数可以使用 Object.keys() Object.getOwnPropertyNames() Object.getOwnPropertySymbols()
Object.keys(obj);
// ['a', 'b']

// Reflect则可以用Reflect.ownKeys()来获取
Reflect.ownKeys(obj);
// ['a', 'b', Symbol(foo)]

// 上面的两个用法可以看到Object.keys()不能获取Symbol的key,这时候就只能通过组合Object.getOwnPropertyNames()和Object.getOwnPropertySymbols()的方式拿到所有的key
const keys = Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
// ['a', 'b', Symbol(foo)]

// 这样看来Reflect上的api更强大兼容性更强,写法当然比后一种兼容写法更简单

// 2. 在行为表现上不同,比如在定义一个新的属性或者修改属性失败时的表现
// Object.defineProperty()执行失败后会抛出一个错误这是进程会被打断,我们需要使用try...catch来保证代码执行
try {
  Object.defineProperty(obj, prop, attr);
  // success do some other logic
} catch(e) {
  // fail
}

// 那么使用Reflect.defineProperty()的方式则是这样

if (Reflect.defineProperty(obj, prop, attr)) {
  // success do some other logic
} else {
  // fail
}

// 通过上面的对比就发现Refect的出现完善了整个JS的反射能力,有相同的使用习惯,利于延续正常的编程逻辑

Reflect的静态方法

这里先只列出Reflect的拦截的方法,下面做一个简单的功能解释,具体的使用则不再一一介绍,可以点击链接跳转到MDN中查看,大部分方法都可以跟Object构造函数的静态方法对应上。

由上面的介绍可以看出Reflect是对已有的api进行了一个收敛和简化,使之更强大,更易用,更符合编程思维。

Proxy

上文简单介绍了有了Object构造函数的一系列静态方法,为什么还需要Reflect。那么本节内容的Proxy又是什么呢?在Reflect描述中提到,所有Reflect拦截的操作方法都与Proxy handler的方法相同,也就是说所有反射的劫持方法都可以通过Proxy的handler来进行处理。

Proxy见名之意是代理的意思,它是一个构造器,用来创建Proxy对象,而构造器创建出来的Proxy对象则是一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

同时Proxy中的所有操作都可以使用Relfect的静态方法处理。

基本语法:

// 其中target是被操作对象
// handler是指的捕获器/劫持器,其上面有Reflect对应的13个静态方法
// 例如: const handler = {get(){},set(){},has(){}}
new Proxy(targe, handler)

举个例子:

const target = {
  notProxied: "original value",
  proxied: "original value"
};

const handler = {
  get: function(target, prop, receiver) {
    if (prop === "proxied") {
      return "replaced value";
    }
    return Reflect.get(target, prop, receiver); // 正如这里可以在不需要处理的时候直接使用Reflect.get来处理
  },
  // set, has, ownKeys etc.
};

const proxy = new Proxy(target, handler);

console.log(proxy.notProxied); // "original value"
console.log(proxy.proxied);    // "replaced value"

上面的例子应该就可以和背景一节中的用法对应上了,在所有被Proxy代理下的方法下均可以用Reflect处理。同时每个proxy方法(例如set,get),同时在getter和setter函数的最后一个参数都是receiver,用来表征该对象的this(一般是指的原proxy对象)。

Proxy上面的的捕获器

前面提到了Proxy可代理(捕获)的handler方法和Reflect的上的静态方法完全一致,也是有13个,并且是一一对应的,这就保证了现代javascript对运行时的对象数据进行全面的劫持和代理,一些基本的用法上面的例子也有提到。

ownKeys

延伸

通过上面的一系列举例和解释应该知道了反射的由来,作用,和演化过程Object->Reflect,那么作为流行框架的vue也离不开该语言特性的应用,首先在开篇已经介绍了vue2和vue3对于数据依赖处理根本不同,一个是原始的Object.defineProperty(),一个是全面拥抱了Reflect和Proxy,使依赖收集没有短板。

  • 先来看vue2中的实现:
/**
 * Define a reactive property on an Object.
 * 这是一个专门定义的reactive(可监控的)数据
 * 传入obj,和obj的key,val,还有其他的一些定制参数
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep() // 依赖收集存储

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 这里对传入的obj的key属性进行reactive化
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // getter时的一些操作
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // setter时的一些操作
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 设置的话 通知视图修改
      dep.notify()
    }
  })
}
  • vue3中的实现
// 创建reactive对象的方法
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  if (!isObject(target)) {
      {
          console.warn(`value cannot be made reactive: ${String(target)}`);
      }
      return target;
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (target["__v_raw" /* RAW */] &&
      !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
      return target;
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
      return existingProxy;
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target);
  if (targetType === 0 /* INVALID */) {
      return target;
  }
  // 前面一系列的判断
  // proxy代理传入的对象target
  const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
  // 存储
  proxyMap.set(target, proxy);
  return proxy;
}

通过上面的两段源码对比发现核心即是本文所说的对数据对象的监听操作。思路一致但是使用的方法不一样。

总结

本文通过一个对数据监听的场景引出了编程语言的一个核心概念反射,以及对于javascript来说反射的使用历程,直至Reflect的出现一统了javascript的反射的概念,把反射能力收敛到Reflect对象上,更利于我们编程实现,当然可能该方式更多的应用到类库的编写上面,但是确是一种编程思维的体现。

搬砖