学习一下 Proxy

62 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 27 天,点击查看活动详情

start

开始

解释:

MDN官网的解释:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

ECMAScript 6 入门的解释:Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

1. 基础用法:

// 1. 基础的使用
var proxy = new Proxy(target, handler);

target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。 (Proxy的用法,主要的不同是 handler 参数的写法)

2. handler 参数有哪些

  1. handler.get()

  2. handler.set()

  3. handler.deleteProperty()

  4. handler.apply()

  5. handler.construct()

  6. handler.getPrototypeOf()

  7. handler.setPrototypeOf()

  8. handler.isExtensible()

  9. handler.preventExtensions()

  10. handler.getOwnPropertyDescriptor()

  11. handler.defineProperty()

  12. handler.has()

  13. handler.ownKeys()

image.png

可以看到截止目前有 13 个可以设置的配置项。

3. 需要注意的事项

  1. 代理对象是通过 new Proxy() 来实现的;new Proxy() 两个参数都必填,可为空对象;

  2. 代理对象 proxy 和目标对象 target 不相等;

    var target = {}
    var proxy = new Proxy(target, {})
    
    console.log(target === proxy) // false
    
  3. 如果 handler 没有设置任何拦截,那就等同于直接通向原对象;但是需要注意一下 this 指向

    var target = { name: '你好' }
    var proxy = new Proxy(target, {})
    
    console.log(target, proxy) // { name: '你好' } { name: '你好' }
    
    proxy.name = '修改代理对象'
    console.log(target, proxy) // { name: '你好' } { name: '你好' }
    

handler中的配置项

1. get() 拦截属性的读取

》 可以接受三个参数,依次为目标对象、属性名和 操作行为所针对的对象

操作行为所针对的对象: handler 的 get 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。

var obj = {
  name: 'tomato',
}

var proxy = new Proxy(obj, {
  get: function () {
    console.log('读取属性', ...arguments)

    /*  可以接受三个参数,依次为目标对象、属性名和 操作行为所针对的对象  */
    console.log('obj === arguments[0]', obj === arguments[0]) // true
    console.log('proxy === arguments[2]', proxy === arguments[2]) // true
  },
})

console.log(proxy.name)
// 读取属性 { name: 'tomato' }  name  { name: 'tomato' }
// undefined

》get方法的返回值就是访问到的数据

var obj = {
  name: 'tomato',
}

var proxy = new Proxy(obj, {
  get: function (target, prop) {
    if (prop === 'tomato') {
      return 'cool'
    }
    return target[prop]
  },
})

console.log(proxy.name) // tomato
console.log(proxy.tomato) // cool

get方法可以继承

var obj = {
  name: 'tomato',
}

var proxy = new Proxy(obj, {
  get: function (target, prop) {
    if (prop === 'tomato') {
      return 'cool'
    }
    return target[prop]
  },
})

var son = Object.create(proxy)

console.log(son) // {}
console.log(son.name) // tomato
console.log(son.tomato) // cool
console.log(son.lazy) // undefined

总结:

  1. 可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象);

  2. get方法的返回值就是访问到的数据;

  3. get方法可以继承;

2. set() 拦截属性的设置

》可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身

var obj = {
  name: 'tomato',
}

var proxy = new Proxy(obj, {
  set: function () {
    console.log('设置属性', ...arguments)

    /*  可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身 */
    console.log('obj === arguments[0]', obj === arguments[0]) // true
    console.log('proxy === arguments[3]', proxy === arguments[3]) // true
  },
})

proxy.name = 'lazy'
// { name: 'tomato' } name lazy { name: 'tomato' }

》新增属性(能拦截到新增的属性,实际设置属性还是要通过 target[propKey] = prop 实现)

var obj = {
  name: 'tomato',
}

var proxy = new Proxy(obj, {
  set: function (target, propKey, prop, receiver) {
    console.log('设置属性', arguments[1])
    target[propKey] = prop
  },
})

proxy.age = 18
// 设置属性 age
console.log(obj, proxy) // { name: 'tomato', age: 18 } { name: 'tomato', age: 18 }

》可以直接通过数组索引修改

var arr = [1, 2, 3, 4, 5]
var proxy = new Proxy(arr, {
  set: function (target, propKey, prop, receiver) {
    console.log('设置属性', arguments[1])
    target[propKey] = prop
  },
})

proxy[2] = '通过索引设置值'
// 设置属性 2

console.log(arr, proxy) // [ 1, 2, '通过索引设置值', 4, 5 ] [ 1, 2, '通过索引设置值', 4, 5 ]

set代理应当返回一个布尔值。严格模式下,set代理如果没有返回true,就会报错。

'use strict'
var obj = {
  name: 'tomato',
}

var proxy = new Proxy(obj, {
  set: function () {
    console.log('设置属性', ...arguments)
  },
})

proxy.name = 'lazy'
/* 

proxy.name = 'lazy'
           ^
TypeError: 'set' on proxy: trap returned falsish for property 'name'

*/

总结:

  1. 可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身;
  2. 新增属性(能拦截到新增的属性,实际设置属性还是要通过 target[propKey] = prop 实现);
  3. 可以直接通过数组索引修改;
  4. set代理应当返回一个布尔值。严格模式下,set代理如果没有返回true,就会报错。

3. apply() 拦截函数的调用

》可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

function say() {
  console.log(this, '你好呀番茄', ...arguments)
}

var p = new Proxy(say, {
  apply: function () {
    /* 可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。 */
    console.log('拦截函数的执行', ...arguments)
  },
})

p(1, 2, 3)
// 拦截函数的执行 [Function: say] undefined [1,2,3]

》函数直接调用,call,apply,bind生成的函数都会被 handler.apply 拦截,new 不会;

function say() {
  console.log(this, '你好呀番茄', ...arguments)
}

var p = new Proxy(say, {
  apply: function () {
    /* 可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。 */
    console.log('拦截函数的执行', ...arguments)
  },
})

p(1, 2, 3)
// 拦截函数的执行 [Function: say] undefined [1,2,3]

p.call({ name: 'call会被拦截' })
// 拦截函数的执行 [Function: say] { name: 'call会被拦截' } []
p.apply({ name: 'apply会被拦截' })
// 拦截函数的执行 [Function: say] { name: 'apply会被拦截' } []

p.bind({ name: 'bind生成的函数会被拦截嘛' })()
// 拦截函数的执行 [Function: say] { name: 'bind生成的函数会被拦截嘛' } []

new p()
// say {} 你好呀番茄

总结:

  1. 可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
  2. 函数直接调用,call,apply,bind生成的函数都会被 handler.apply 拦截,new 不会;

4. deleteProperty() 用于拦截 delete 操作

》如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

》可以接受两个参数,分别是目标对象,删除的属性名

var obj = {
  name: 'tomato',
  age: 18,
}

var proxy = new Proxy(obj, {
  deleteProperty: function () {
    console.log('删除属性', ...arguments)
  },
})

delete proxy.age

console.log(obj, proxy)

/* 必须返回 true 才会删除对应的属性 */
// 删除属性 { name: 'tomato', age: 18 } age
// { name: 'tomato', age: 18 } { name: 'tomato', age: 18 }

总结:

  1. 如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

  2. 可以接受两个参数,分别是目标对象,删除的属性名

Proxy 这里就比 Object.defineProperty 多了对 delete 的拦截

5. construct() 用于拦截new命令

》 construct 返回值必须是对象,返回的对象,就是 new 代理对象 的返回值;

function fn() {
  console.log('fn', ...arguments)
}

var proxyFn = new Proxy(fn, {
  construct: function() {
    console.log('construct的参数', ...arguments)
  }
})

new proxyFn(1, 2, 3)
// TypeError: 'construct' on proxy: trap returned non-object ('undefined')

construct()方法可以接受三个参数。target:目标对象。args:构造函数的参数数组。newTargetnew命令作用的构造函数

function fn() {
  console.log('fn', ...arguments)
}

var proxyFn = new Proxy(fn, {
  construct: function() {
    console.log('construct的参数', ...arguments)

    console.log(fn === arguments[0]) // true
    console.log(proxyFn === arguments[2]) // true

    return { name: '随意的参数' }
  }
})

var son = new proxyFn(1, 2, 3)
// construct的参数 [Function: fn] [ 1, 2, 3 ] [Function: fn]

console.log(son)
// { name: '随意的参数' }

》目标对象必须是函数

var proxyFn = new Proxy(
  { name: '对象' },
  {
    construct: function() {
      console.log('construct的参数', ...arguments)
      return { name: '随意的参数' }
    }
  }
)

/* TypeError: proxyFn is not a constructor */

》construct中的 this 指向 handler

function fn() {
  console.log('fn')
}

var handler = {
  construct: function() {
    console.log('construct的参数', this, this === handler)
    return { name: '随意的参数' }
  }
}
var proxyFn = new Proxy(fn, handler)

new proxyFn()
/* construct的参数 { construct: [Function: construct] } true */

其他

其他的我个人暂时用的不多,这里就整体列一下

  1. handler.getPrototypeOf()

    • getPrototypeOf()方法主要用来拦截获取对象原型
  2. handler.setPrototypeOf()

    • 用来拦截Object.setPrototypeOf()方法。
  3. handler.isExtensible()

    • 拦截Object.isExtensible()操作。
  4. handler.preventExtensions()

    • 拦截Object.preventExtensions()
    • **Object.preventExtensions()**方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
  5. handler.getOwnPropertyDescriptor()

    • 拦截Object.getOwnPropertyDescriptor()
    • Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
  6. handler.defineProperty()

    • 拦截Object.defineProperty
  7. handler.has()

    • has()方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
  8. handler.ownKeys()

    • ownKeys()方法用来拦截对象自身属性的读取操作

Proxy.revocable()

Proxy.revocable()方法返回一个可取消的 Proxy 实例;

revocable(英文释义:可撤销的)

let target = {}
let handler = {}

let { proxy, revoke } = Proxy.revocable(target, handler)

proxy.foo = 123
console.log(target, proxy)
/* { foo: 123 } { foo: 123 } */

revoke()
console.log(target, proxy)
/* {foo: 123} {} */

this 问题

虽然说:如果 handler 没有设置任何拦截,那就等同于直接通向原对象;但是两者是两个不同的对象,这就导致会有一些差异需要注意。

代理对象 p 的this指向 Proxy

let target = new Date()

var p = new Proxy(target, {})

console.log(target.getTime())
/* 1661323388976 */

console.log(p.getTime())
/* TypeError: this is not a Date object. */

拦截配置函数的 this 指向 handle

var target = {}

var handle = {
  get: function() {
    console.log(this === handle)
  }
}

var p = new Proxy(target, handle)

console.log(p.name)
// true
// undefined

end

总结一下收获:

  • 熟悉了 Proxy的基本用法;

  • 知道了可以使用 new Proxy的方法代理对象,通过常见的 get set deleteProperty拦截对象的操作;

  • 可以通过 Proxy.revocable() 创建可取消的代理对象;

  • 使用 proxy 的实例的时候,需要注意一下 this指向的问题。

加油