Skip to content

【技术干货第12期】介绍ES6 Proxy #19

@fanchangyong

Description

@fanchangyong

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). - MDN

前言

前面两篇文章我们介绍了在JS中对引用类型做非破坏性操作的一些方式,对前端程序员来说,一个非常重要的好处就是可以让我们写出正确的redux reducer。在调研reducer更新方式的过程中,我注意到了一个库,叫做ImmerJS,它允许我们用破坏性的语法来得到非破坏性的结果。由于它是基于Proxy的机制来实现的,这让我对Proxy这个ES6的新特性产生了很大的兴趣。另外Vue3的reactivity system也是基于Proxy的,如果你使用Vue那么了解Proxy的机制也是很有必要的。
所以这两天我就抽时间去学习了一下,这篇文章就给大家简单介绍一下Proxy

什么是Proxy?

Proxy,也就是”代理“,在程序中的意思通常是在用户和真正访问的对象之间加了一个中间层,用户不再直接访问原始对象,而是通过Proxy来做一个中转。比如我们最常见的关于代理的概念就是网络代理,分为"正向代理"和"反向代理"。"反向代理"拿nginx来讲,它就是在用户(浏览器)和目标(web server)之间的一个中间层,浏览器通过nginx(代理)才能访问到web server。通过代理可以实现访问控制、负载均衡等目标。

我们这里讲的JavaScript的Proxy也是类似的概念,只不过它是在对象对象的访问之间的一层代理,应用了Proxy之后,我们将通过Proxy来访问目标对象。
Proxy会改变JavaScript中一些基础操作的执行路径,如读取属性、写入属性、对象遍历、函数调用等。这是一种强大的元编程(meta programming)机制,可以实现很多通过之前无法实现的功能特性。虽然Proxy并没有引入新的语法,但由于它会修改JavaScript的一些底层代码的执行方式,所以它是无法被完全polyfill的。

Proxy的几个概念

在你读Proxy的相关文档时,有几个概念会频繁出现,我们先对这些概念做一个简单介绍。

  • target - target指的就是我们的目标对象,也就是被代理的对象
  • trap - trap可以理解为是一些预定义的触发点以及定义在这些触发点上的函数,比如上面提到的读取属性、写入属性、函数调用等,如果我们在这些触发点上定义了函数,那么在对Proxy执行对应操作的时候,我们定义的函数将会被调用
  • handler - handler是一个对象,它包装了所有Proxy提供的trap函数,可以理解为是trap的集合

Proxy的创建

Proxy的创建是比较简单的,参考以下代码:

// 我们的原始对象
var target = {
  x: 1,
}
// handler对象,里面定义了各种trap函数
var handler = {
  get: function(obj, prop) {
    return obj[prop]
  }
}
// 创建proxy,参数分别为target和handler,含义见上方解释
var proxy = new Proxy(target, handler)
// 通过proxy访问target对象
console.log(proxy.x) // 输出:1

在上面代码中,我们通过new Proxy来创建proxy对象,新创建的proxy对象将代理我们对target对象的访问。当我们访问proxy.x的时候,我们定义的handler中的get函数(trap)将被调用。get接受到的参数分别为target对象和我们要访问的属性名称。而因为我们直接返回指定属性的值,所以返回proxy.x的返回值就是target中对应属性的值。
当然这里我们可以写任意的代码,也可以返回任意的值。

有哪些traps?

上面我们介绍了Proxy的创建方式,实现了一个简单的trap:get。所有的trap都是可选的,如果我们定义了trap,当我们执行特定操作的时候就会执行我们的trap,如果没有定义trap,默认操作就是执行将操作直接传递到目标对象。
接下来我们就来看一下JavaScript提供的traps都有哪些:

  • get - 当我们获取目标对象属性值的时候被调用,返回属性的值
  • set - 和get对应,当我们写入目标对象的属性值时被调用,改写属性的值
  • deleteProperty - 当我们删除目标对象的属性值时被调用
  • has - 当我们对目标对象使用in操作符时被调用,返回指定的属性是否存在
  • apply - 当我们对目标对象(这里是一个函数)执行函数调用时这个trap会被调用
  • construct - 当我们对目标对象(要求是一个函数)执行new操作符的时候被调用
  • ownKeys - 当我们调用Object.keys的时候这个trap将会被执行
  • getPrototypeOf - 当我们对目标对象调用Object.getPrototypeOf时这个trap将被执行
  • setPrototypeOf - 当我们对目标对象调用Object.setPrototypeOf时这个trap将被执行
  • isExtensible - 当我们对目标对象执行Object.isExtensible时这个trap将被执行
  • preventExtensions - 当我们对目标对象执行Object.preventExtensions时这个trap将被执行
  • getOwnPropertyDescriptor - 当我们对目标对象执行Object.getOwnPropertyDescriptor时这个trap将被调用
  • defineProperty - 当我们对目标对象执行Object.defineProperty时这个trap将被调用

Reflect

Reflect是一个对象,针对上面提到的每一个trap,它都定义了一个static方法。它主要用于方便我们写trap函数的时候将操作传递到目标对象。比如:

var target = {
  x: 1,
}
var handler = {
  get: function(...args) {
  	// 我们使用Reflect将操作直接传递到目标对象
    return Reflect.get(...args)
  },
  set: function(...args) {
    return Reflect.set(...args)
  }
}
var proxy = new Proxy(target, handler)
console.log(proxy.x) // 输出:1

由于每个trap都有一个对应的Reflect方法,而且他们的参数都是一致的,所以当我们需要在trap中将操作直接传递到目标对象的时候使用Reflect中的方法是非常方便的。Reflect中的很多方法和Object中的一些方法都比较相似,但还是有一些小的差别,比如返回值的不同等。可以参考这个网址

和getter、setter的区别

通过以上的描述,我们可以看到Proxy和getter、setter有点像。它们都可以做到当我们访问或改写一个对象的属性值时调用一个特定的函数,但有以下区别:

  1. Proxy除了对属性值的获取和改写还可以改变其他JavaScript基础操作的执行方式,也就是它的功能比getter、setter多很多
  2. getter、setter是定义在对象本身上的,而Proxy是定义在另一个对象(proxy)上的。这表示Proxy允许我们比较方便的修改一些我们不方便直接改变其定义方式的对象的执行方式,比如一些第三方库中定义的对象等。

总结

ES6的Proxy对很多人可能比较陌生,但其实它真的非常简单,以上介绍的基本就是它的全部内容了。虽然实现起来比较简单,但是它可以实现很多非常强大的功能,比如ImmerJS这个库,它提供了用破坏性的语法来实现非破坏性操作的方式,我们打算下篇文章来介绍它,如果你感兴趣请关注公众号第一时间获得推送。

欢迎扫码关注公众号<前端时光机>:

qrcode

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions