揭秘JavaScript元编程能力

1,532 阅读5分钟

前言

js中一直以来都有元编程的概念,比如ES5中强大的Object.defineProperty,ES6中Proxy + Reflect更是将这一能力发扬光大。

首先我们首先需要了解什么是元编程?在编程中,按层次可划分为以下级别:

  • 应用程序级别,代码用来处理用户输入
  • 在元级别,代码用来处理基础的语言级别的功能,甚至可以改变一些语法的默认行为。 这也是元编程在日常开发中用的很少的原因,因为它的应用场景更多的是在一些通用的库与框架中,比如vue中,它的核心功能是:
    将对象的属性修改与视图更新映射起来,那么就需要监听到针对对象属性的所有操作,并进行相应的拦截,中间增加一层vue视图更新的逻辑。
    到这里,假如你是vue的设计者,不妨思考下,应该暴露什么样级别的api给开发者呢?
  • 第一种,提供一套专门的api调用,比如想触发get依赖收集需要开发者手动调vue.$get方法,发布依赖更新主动调vue.$set方法),那么template里取值的地方都需要调专用的$get,极其繁琐。
  • 第二种,语言级别的实现。也就是之前你咋写js还是咋写,依然是obj.xxx obj.xxx = val这种最基础的对象属性取值、赋值语法,剩下的一切都交给框架,不破坏开发者原先的心智模型。 毫无疑问,一定是第二种语言级别的实现是最为简洁的,框架在使用层面推广起来也最为友好,所以元编程思想我们并不陌生,熟悉的vue等依赖mutable对象的mvvm框架就是其中一种很强大的应用。
    在js中,属于元编程范畴的有
  • Object上的静态方法都具有元编程的功能,比如definedProperty可以重新定义对象属性的默认操作行为。
  • 通过SymbolSymbol.toStringTag Symbol.toPrimitive等静态属性可以读取并改写这些 语言内部 的方法。
  • eval(code)new Function(...args,code)这些可以动态编译执行js语句的方法
  • 原生提供的代理模式实现 Proxy 本文重点会介绍Proxy和与之相对的Reflect

正文

Proxy

代理,是一种非常经典的设计模式,ES6中提供的Proxy api,就是对这一模式在代理对象操作方面的实现。它的工作方式如下:

     const xiaoming = {
        age: 30,
      };
      const handler = {
        get() {
          return 18;
        },
      };
      const proxyedTarget = new Proxy(xiaoming, handler);
      console.log(proxyedTarget.age); // 18

在这个例子中,小明依靠Proxy实现了自己永远18岁的美好愿望。这是因为obj.xxx执行触发了读取属性值的【Get】操作,会默认触发代理对象的get捕获,在方法中可以将返回值进行修改。

捕获操作与对应的js触发语法

  • getobj.xxx obj[xxx] Reflect.get对象取值语法
  • setobj.xxx = val Reflect.set对象属性的设置语法
  • haskey in obj 判断属性是否存在的操作符
  • apply】函数类型的对象调用 fn()
  • construct】函数对象的 new fn()调用
  • defineProperty】对应 Object.defineProperty调用
  • deletePropertydelete obj[foo] delete obj.foo或者 Reflect.deleteProperty
  • getOwnPropertyDescriptor】对应 Object.defineProperty调用
  • getPrototypeOf】对应 Object.getPrototypeOf调用
  • setPrototypeOf】是Object.setPrototypeOf 方法的捕捉器
  • ownKeys】是Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
  • preventExtensions】是Object.preventExtensions 方法的捕捉器
  • isExtensible】 是 Object.isExtensible 方法的捕捉器。

Reflect

为Proxy而生

js中Reflect与其他语言(如java、python)中的Reflect的意义有所不同,因为它在js中,是为了更方便的使用Proxy才出现的,因为它的方法与Proxy完全一一对应,下边这个例子就是最好的佐证。
比如让你设计一个功能,对一个对象的所有操作都log下来。

 function log(target){
        const handler = {};
        Reflect.ownKeys(Reflect).forEach((op)=>{
            handler[op] = (...args)=>{
                console.log(op,args)
                return Reflect[op](...args)
            }
        })
        return new Proxy(obj,handler)
    }
    const obj = {
        age:30
    }
    const proxyedObj = log(obj)
    proxyedObj.age = 18
    proxyedObj.age

执行结果

image.png

可以想象,如果你手动对上边提到的Proxy能代理到的10种的操作都进行分别处理,那代码得写多长。。。Reflect让这一切都变得非常简单,针对Proxy的每一个能被代理的方法,都有对应的操作进行处理,而且参数与方法名完全一致,就像一面镜子那样会反射对一个代理对象的所有操作,这也是它的最大价值所在。
除此之外,Reflect还带来了其他的一些好处:

替代Object

这其实也是ES官方一个无心插柳柳成荫的创意,上边讲到,Proxy能代理的操作类型,除了一些来源于obj.xxx in的语法操作外,其他都基本来自Object这个方法的静态方法调用,Reflect为了实现与Proxy的操作反射,必然也要在自身上实现一套Object的静态方法,而且在这些方法迁移过程中,修正了一些之前设计不好的地方,比如object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false,并且,增加了receiver参数,这就导致Reflect相比Object设计上更安全,功能上更强大,直接可以取代了Object上的静态方法,ES官方表示,以后新增的对象操作方法,都会直接在Reflect身上实现,Object身上的就不会更新了,就让它最大程度的保持一个对象的构造函数的功能就好了。

运行时特性

可以想象,Proxy代理的操作,都是在js运行时触发的,那么Reflect的反射操作,也当然都是在运行时调用,利用这个特性,可以绕过一些静态强类型的类型限制,比如在TypeScript中。 遍历对象场景时,常常会遇到这个错误

image.png

原因其实很明显:遍历过程中的 item 属性变量只约束为string类型,太宽泛了,应该只限定为对象存在的属性字符串,访问未知的属性会报错 这个固然可以用TS的类型断言方式来解决
image.png
在这里,还可以利用Reflect的运行时特性,去躲避TS的静态类型校验。

image.png

结束

以上就是关于Proxy Reflect的全部介绍及自己一些使用经验。