> ## Content Index
> Fetch the complete content index at: https://iiong.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# ES6 中的 Proxy 的一些奇淫技巧
- URL: https://iiong.com/es6-proxy-some-skills/
- Published: 2020-04-15T15:34:33.000Z
- Updated: 2026-06-22T15:30:46.000Z
- Description: 老早之前就使用它处理一些业务，不过后来很少接触复杂的业务，几乎忘了它的存在，正好进来因为 Vue3 特性让我想起来这个对象，花点时间深入了解这个对象使用和一些方法的技巧。
- Author: 淮城一只猫
- Tags: 编程技术, Web 前端

### 前言

老早之前就使用它处理一些业务，不过后来很少接触复杂的业务，几乎忘了它的存在，正好进来因为 `Vue3` 特性让我想起来这个对象，花点时间深入了解这个对象使用和一些方法的技巧。

### 语法

`Proxy` 翻译中文叫 `代理` ，联想可以认为它可以在 `JavaScript` 给代理人进行一些操作。

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

在语法上，也没有很多复杂的东西：

```javascript
const proxy = new Proxy(target, handler)

```

**Target:** 要使用 `Proxy` 包装的目标对象（可以是任何类型的对象，包括原生数组，函数，甚至另一个代理）。

**handler:** 处理器对象，作为代理配置，包涵 `traps`（陷阱对象） 对象，也就是拦截操作的方法。常见的陷阱对象有：`get` 和 `set` 。

没有配置任何陷阱对象代理函数如下：

```javascript
const target = {}
const proxy = new Proxy(target, {})

proxy.test = 100
console.log(target.test) // => 100
console.log(proxy.test) // => 100
for(const key in proxy) {
  console.log(key) // => test
}

```

因为上面没有设置代理配置，也就是陷阱对象，所有的操作都转发给 `target`：

- 写入操作 `proxy.test` 值设置给 `target`
- 读取操作 `proxy.test` 从 `target` 获取返回值
- 从 `proxy` 迭代获取的 `key` 属性从 `target` 获取返回值

所以从上面得知，没有任何陷阱对象，`proxy` 是一个透明包装的 `target` 对象。如果需要更多的功能需要添陷阱对象。在 `JavaScript` 对象中，对于对象的大部分操作，都有一个所谓的『内部方法』，例如：用于读取属性的 `[[Get]]` 的内部方法，用于设置属性的 `[[Set]]` 的内部方法。但这些方法仅在规范中使用，无法直接去调用它们。代理中的陷阱方法在规范调用方法如下：

| 内部方法                      | 处理方法                     | 触发条件                                                                                                                                                                                                                                                                                                                     |
| ------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| \[\[Get\]\]               | get                      | 读取属性：[handler.get()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy/handler/get?ref=iiong.com)                                                                                                                                                                                 |
| \[\[Set\]\]               | set                      | 修改属性：[handler.set()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy/handler/set?ref=iiong.com)                                                                                                                                                                                 |
| \[\[HasProperty\]\]       | has                      | in 运算符操作：[handler.has()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy/handler/has?ref=iiong.com)                                                                                                                                                                             |
| \[\[Delete\]\]            | deleteProperty           | 删除操作：[handler.deleteProperty()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy/handler/deleteProperty?ref=iiong.com)                                                                                                                                                           |
| \[\[Call\]\]              | apply                    | 函数调用：[handler.apply()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy/handler/apply?ref=iiong.com)                                                                                                                                                                             |
| \[\[Construct\]\]         | construct                | 实例化对象：[handler.construct()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy/handler/construct?ref=iiong.com)                                                                                                                                                                    |
| \[\[GetPrototypeOf\]\]    | getPrototypeOf           | [Object.getPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/getPrototypeOf?ref=iiong.com)                                                                                                                                                                          |
| \[\[SetPrototypeOf\]\]    | setPrototypeOf           | [Object.setPrototypeOf](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/setPrototypeOf?ref=iiong.com)                                                                                                                                                                          |
| \[\[IsExtensible\]\]      | isExtensible             | [Object.isExtensible](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/isExtensible?ref=iiong.com)                                                                                                                                                                              |
| \[\[PreventExtensions\]   | preventExtensions        | [Object.preventExtensions](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/preventExtensions?ref=iiong.com)                                                                                                                                                                    |
| \[\[DefineOwnProperty\]\] | defineProperty           | [Object.defineProperty](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/defineProperty?ref=iiong.com), [Object.defineProperties](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/defineProperties?ref=iiong.com)                     |
| \[\[GetOwnProperty\]\]    | getOwnPropertyDescriptor | [Object.getOwnPropertyDescriptor](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/getOwnPropertyDescriptor?ref=iiong.com)                                                                                                                                                      |
| \[\[OwnPropertyKeys\]\]   | ownKeys                  | [Object.getOwnPropertyNames](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/getOwnPropertyNames?ref=iiong.com), [Object.getOwnPropertySymbols](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Object/getOwnPropertySymbols?ref=iiong.com) |

### 陷阱对象使用

这边讲述上面列出来各个使用方法，为了提高理解这边对部分陷阱对象含有 `receiver`参数不进行详细讲解，就放在后面再讲解吧。

#### get

改属性和 `handler.set()` 一样是常见的陷阱对象，从上述文档得知语法如下：

```javascript
get(target, property, receiver)

```

- `target`：目标对象，作为第一个 参数传递给新的 `Proxy`
- `property`：属性名称
- `receiver`：`Proxy` 或者继承 `Proxy` 的对象

返回值：返回访问任何值

在平时开发项目中，要获取一个对象的属性，如果不存在的属性系统会返回一个 `undefined` 还需要额外判断等操作给予默认值：

```javascript
const num1 = [1, 2, 3]
console.log(num1[2]) // => 3
console.log(num1[4]) // => undefined

```

但如果使用 `Proxy` 方法将上面进一步提高操作可行性：

```javascript
let num2 = [1, 2, 3]
num2 = new Proxy(num2, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 1 // 如果没有找到集合存在的值，返回一个默认值
    }
  }
})

console.log(num2[2]) // => 3
console.log(num2[4]) // => 1

```

#### set

```javascript
set(target, property, value, receiver)

```

- `target`：目标对象，作为第一个 参数传递给新的 `Proxy`
- `property`：将被设置的属性名或 `Symbol`
- `value`：新属性值
- `receiver`：最初被调用的对象。通常是 `Proxy` 本身，但 `handler` 的 `set` 方法也有可能在原型链上，或以其他方式被间接地调用（因此不一定是 `Proxy` 本身）

返回值：返回 `Boolean` 值

这边使用场景的情景做个案例，比如表单验证，填入年龄是 `number` 类型，需要根据用户输入判断输入类型来决定是否进行下一步操作：

```javascript
let num3 = []
num3 = new Proxy(num3, {
  set(target, prop, value) {
    if (typeof value === 'number') {
      target[prop] = value
      return true
    } else {
      return false
    }
  }
})

num3.push(20)
num3.push(100)

console.log(num3.length) // => 2

num3.push('100') // => 报错：TypeError: 'set' on proxy: trap returned falsish for property '2'

```

> 对于 `set` 它必须用有个 `true` 的返回值，否则告知写入失败。

#### has

```javascript
has(target, property)

```

- `target`：目标对象
- `property`：需要检查是否存在的属性

返回值：`Boolean` 属性值

一般来说是在 `in` 运算符触发陷阱对象：

```javascript
let range = {
  start: 1,
  end: 10
}

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end
  }
})

console.log(7 in range) // => true
console.log(20 in range) // => false

```

#### apply

```javascript
apply(target, thisArg, argumentsList)

```

- `target`：目标对象
- `thisArg`：被调用时的上下文对象
- `argumentsList`：被调用时的参数数组

返回值：返回任何值

使用 `Proxy` 实现简单的函数转发功能函数：

```javascript
function delay(func, ms) {
  return new Proxy(func, {
    apply(target, thisArg, argumentsList) {
      setTimeout(() => target.apply(thisArg, argumentsList), ms)
    }
  })
}

function sayHello(user) {
  console.log(`Hello, ${user}!`)
}

sayHello = delay(sayHello, 5000)

console.log(sayHello.length) // => 1 // Proxy 转发这个操作

sayHello('Jaxson') // => 五秒后出现：Hello, Jaxson!

```

#### ownKeys && getOwnPropertyDescriptor

在 `Object.keys` , `for in` 和大部分遍历对象属性的内部方法 `[[OwnPropertyKeys]]` 来获取属性列表，这些方法都有不同的细节：

- `Object.getOwnPropertyName(obj)` 返回非 `Symbol` 键值
- `Object.getOwnPropertySymbols(obj)` 返回 `Symbol` 键值
- `Object.keys/values()` 返回可枚举标识的非 `Symbol` 键值
- `for in` 遍历具有可枚举标识的非 `Symbol` 键值以及原型链

在下面使用上述进行简单的例子，使用 `ownKeys` 陷阱对象让 `for in` 遍历一组用户对象，这个对象包括用户的信息，然后忽略特定的对象属性：

```javascript
let user = {
  name: 'Jaxson',
  age: 25,
  sex: '男',
  _password: '****' // 密码选项不得公开展示
}

user = new Proxy(user, {
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'))
  }
})

for (const key in user) console.log(key) // => name, age, sex

console.log(Object.keys(user)) // => ['name', 'age', 'sex']
console.log(Object.values(user)) // => ['Jaxson', 25, '男']

```

如果是对象不包含任何键值， `Object.keys` 不会列出该键值：

```javascript
let user = {}

user = new Proxy(user, {
  ownKeys(target) {
    return ['user1', 'user2', 'user3']
  }
})

console.log(Object.keys(user)) // => 返回空

```

因为 `Object.keys` 仅返回带有可枚举标识的属性，在遍历对象它为每个属性调用内部属性 `[[GetOwnProperty]]` 来获取它描述符，如果没有属性，其描述符为空则跳过。

> *描述符* 是描述对象属性的属性 , 对象里目前存在的属性描述符有两种主要形式：**数据描述符** 和 **存取描述符**. 可以通过 `Object.getOwnPropertyDescriptor()` 函数来获取某个对象下指定属性的对应的 *描述符* .

如果想要在上面例子返回一个属性，那么需要将它存在带有可枚举标识的对象里或者拦截对 `[[GetOwnProperty]]` 的调用，而这个陷阱对象就是 `getOwnPropertyDescriptor` 来处理，并返回一个描述符 `enumerable: true` ：

```javascript
let user = {}

user = new Proxy(user, {
  ownKeys(target) {
    return ['user1', 'user2', 'user3']
  },
  getOwnPropertyDescriptor(target, prop) {
    return {
      enumerable: true,
      configurable: true
    }
  }
})

console.log(Object.keys(user)) // => ['user1', 'user2', 'user3']

```

#### deleteProperty

继续拿上面的用户对象来作为例子，希望所有带 `_` 开头的属性对此进行保护：

- `get` 访问属性时报错告知无法访问
- `set` 设置属性时报错告知无法设置
- `deleteProperty` 删除该属性时报错告知无法删除
- `ownKeys` 排除所有的 `_` 开头属性操作

```javascript
let user = {
  name: 'Jaxson',
  age: 25,
  sex: '男',
  _password: '****' // 密码选项不得公开展示并且不可修改
}

user = new Proxy(user, {
  get(target, prop) {
    if (prop.startsWith('_')) throw new Error('访问失败')
    const value = target[prop]
    return (typeof value === 'function') ? value.bind(target) : value
  },
  set(target, prop, value) {
    if (prop.startsWith('_')) {
      throw new Error('访问失败')
    } else {
      target[prop] = value
      return true
    }
  },
  deleteProperty(target, prop) {
    if (prop.startsWith('_')) {
      throw new Error('访问失败')
    } else {
      delete target[prop]
      return true
    }
  },
  ownKeys(target) {
    return Object.keys(target).filter(key => !key.startsWith('_'))
  }
})

user._password // => 报错：Uncaught Error: 访问失败
user._password = 'abc123456' // => 报错：Uncaught Error: 访问失败
delete user._password // => 报错：Uncaught Error: 访问失败
for (const key in user) console.log(key) // => name, age, sex

```

在上述有一段代码：

```javascript
return (typeof value === 'function') ? value.bind(target) : value

```

如果对象包含一个检查这个属性的方法：

```javascript
let user = {
  name: 'Jaxson',
  age: 25,
  sex: '男',
  _password: '****', // 密码选项不得公开展示并且不可修改
  checkPassword(value) {
    return value === this._password
  }
}

```

所以在访问的时候会触发陷阱 `get` 对象返回无法访问的错误，所以这边只需要绑定原始对象就不会出现访问错误的错误。

### Reflect

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

根据上述可以知道使用 `Reflect` 可以简化 `Proxy` 创建，当然也可以认为它是 `Proxy` 伴侣，之前说的 `[[Get]]`、`[[Set]]` 内部方法仅用于规范，不能直接调用，但使用 `Reflect` 对象使之可能，因为它的方法是内部方法的最小包装：

| 操作                  | Reflect 调用                        | 内部方法              |
| ------------------- | --------------------------------- | ----------------- |
| obj\[prop\]         | Reflect.get(obj, prop)            | \[\[Get\]\]       |
| obj\[prop\] = value | Reflect.set(obj, prop, value)     | \[\[Set\]\]       |
| delete obj\[prop\]  | Reflect.deleteProperty(obj, prop) | \[\[Delete\]\]    |
| new Obj(value)      | Reflect.construct(Obj, value)     | \[\[Construct\]\] |

还有诸多操作方法，就不多举例了，使用它们的方法也很简单：

```javascript
let user = {}
Reflect.set(user, 'name', 'Jaxson') // => true
console.log(user.name) // => Jaxson

```

在普通场景下，一般 `Proxy` 操作足够了：

```javascript
const user = {
  _name: 'Test1',
  get name() {
    return this._name
  }
}

const userProxy = new Proxy(user, {
  get(target, porp, receiver) {
    return target[porp]
  }
})

console.log(userProxy.name) // => Test1

```

在这边操作的时候建议配合 `Reflect` 对象更佳，在上面例子稍微复杂化就明白了：

```javascript
const user = {
  _name: 'Test1',
  get name() {
    return this._name
  }
}

const userProxy = new Proxy(user, {
  get(target, porp, receiver) {
    return target[porp]
  }
})

const userTest = {
  __proto__: userProxy,
  _name: 'Test2'
}

console.log(userTest.name) // => Test1

```

发现这边输出的内容和预想值不一样，为什么不是 `Test2` ，然后输出 `get` 的操作对象的返回值对象却是 `user` 而不是 `userTest` ：

- 在寻找 `admin.name` 的时候， `admin` 没有自己的属性，就转向原型链它的原型链
- 发现原型链是 `userProxy`
- 然后读取属性 `name` ，从 `get` 陷阱对象获取返回对象是原始对象，从而读取到属性
- 对于 `target[prop]` 调用在 `this = target` 上下文运行代码，所以结果是原始对象 `this._name` 就是在 `user` 对象里

所以解决这个方法就需要陷阱对象的第三个参数：`receiver` ，它可以正确把 `this` 传递给返回的对象。在普通对象里可以直接 `call/apply` 绑定上下文，但这边是不能被调用，而是被使用，所以就需要上面的 `Reflect` 的静态方法：

```javascript
const user = {
  _name: 'Test1',
  get name() {
    return this._name
  }
}

const userProxy = new Proxy(user, {
  get(target, porp, receiver) {
    return Reflect.get(target, prop, receiver)
  }
})

const userTest = {
  __proto__: userProxy,
  _name: 'Test2'
}

console.log(userTest.name) // => Test2

```

### 实战

根据上面的方法进行实战，就选择最常见的表单验证的场景，例如：

```html
<body>
    <form action="http://xxx.com/register" id="registerForm" method="post">
        <div class="form-group">
            <label for="user">请输入用户名:</label>
            <input type="text" class="form-control" id="user" name="userName">
        </div>
        <div class="form-group">
            <label for="pwd">请输入密码:</label>
            <input type="password" class="form-control" id="pwd" name="passWord">
        </div>
        <div class="form-group">
            <label for="phone">请输入手机号码:</label>
            <input type="tel" class="form-control" id="phone" name="phoneNumber">
        </div>
        <div class="form-group">
            <label for="email">请输入邮箱:</label>
            <input type="text" class="form-control" id="email" name="emailAddress">
        </div>
        <button type="button" class="btn btn-default">Submit</button>
    </form>
</body>

```

针对上面进行不同的表单需要不同的验证，例如用户名长度，密码强度，手机号和邮箱的判断验证等等，如果采用最简单可以像这样：

```javascript
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function() {
  if (registerForm.userName.value === '') {
    alert('用户名不能为空！')
    return false
  }
  if (registerForm.userName.length < 6) {
    alert('用户名长度不能少于6位！')
    return false
  }
  if (registerForm.passWord.value === '') {
    alert('密码不能为空！')
    return false
  }
  if (registerForm.passWord.value.length < 6) {
    alert('密码长度不能少于6位！')
    return false
  }
  if (registerForm.phoneNumber.value === '') {
    alert('手机号码不能为空！')
    return false
  }
  if (!/^1(3|5|7|8|9)[0-9]{9}$/.test(registerForm.phoneNumber.value)) {
    alert('手机号码格式不正确！')
    return false
  }
  if (registerForm.emailAddress.value === '') {
    alert('邮箱地址不能为空！')
    return false
  }
  if (!/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(registerForm.emailAddress.value)) {
    alert('邮箱地址格式不正确！')
    return false
  }
}, false)

```

如果随着表单随着维护增多以及复杂化，上面的各种 `if-else` 判断相信后期可读性非常差，所以这边需要提高代码可读性以及复用性。所以利用 `Proxy` 重构表单验证。

首先利用 `Proxy` 拦截一些不符合要求的数据：

```javascript
function validator(target, validator, errorMsg) {
  return new Proxy(target, {
    _validator: validator,
    set(target, key, value, proxy) {
      let errMsg = errorMsg
      if (value === '') {
        alert(`${errMsg[key]}不能为空！`)
        return target[key] = false
      }
      let va = this._validator[key]
      if (!!va(value)) {
        return Reflect.set(target, key, value, proxy)
      } else {
        alert(`${errMsg[key]}格式不正确`)
        return target[key] = false
      }
    }
  })
}

```

负责校验的逻辑代码：

```javascript
const validators = {
  name(value) {
    return value.length > 6
  },
  passwd(value) {
    return value.length > 6
  },
  moblie(value) {
    return /^1(3|5|7|8|9)[0-9]{9}$/.test(value)
  },
  email(value) {
    return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)
  }
}

```

整合俩者：

```javascript
const errorMsg = {
    name: '用户名',
    passwd: '密码',
    moblie: '手机号码',
    email: '邮箱地址'
}
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function() {
  let validatorNext = function*() {
    yield vali.name = registerForm.userName.value
    yield vali.passwd = registerForm.passWord.value
    yield vali.moblie = registerForm.phoneNumber.value
    yield vali.email = registerForm.emailAddress.value
  }
  let validator = validatorNext()
  validator.next();
  !vali.name || validator.next(); //上一步的校验通过才执行下一步
  !vali.passwd || validator.next();
  !vali.moblie || validator.next();
}, false)

```

所以对比上面的 `if-else` 来比，不仅仅编码量少，并且表单对象和表单条件完全隔离，代码健壮性和复用性非常的强。

这边看起来比较简单，有时间再看稍微复杂的例子吧。

### 参考文档

[Specification Proxy](https://tc39.es/ecma262/?ref=iiong.com#sec-proxy-object-internal-methods-and-internal-slots)

[MDN Proxy 文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Proxy?ref=iiong.com)

[MDN JSPropertyDesciptor 参考文档](https://developer.mozilla.org/zh-CN/docs/Mozilla/Projects/SpiderMonkey/JSAPI%5Freference/JSPropertyDescriptor?ref=iiong.com)

[MDN in 运算符文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/in?ref=iiong.com)

[MDN Reflect 文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global%5FObjects/Reflect?ref=iiong.com)

[MDN 元编程文档](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Meta%5Fprogramming?ref=iiong.com)