JavaScript ES(6-11)全版本语法 (十三):Proxy

98 阅读5分钟

前提概要

上一篇编写的是ES6中的数值的扩展,链接:juejin.cn/post/702877… ,这次写的是ES6中Proxy,介绍Proxy的一些基础知识和应用场景。如有不对的或者不准确的地方,欢迎大家提出😄,我也积极修改。下边开始正文:

src=http___b-ssl.duitang.com_uploads_item_201808_14_20180814182612_XMzuK.thumb.700_0.jpeg&refer=http___b-ssl.duitang.jpg

基本语法

ES5中对对象的属性进行拦截方法如下:

let obj = {}
let newObj = '';
Object.defineProperty(obj,'name',{
  get(){
    console.log('get')
    return newVal
  },
  set(val){
    console.log('set')
    // 这么赋值会报错,原因是我们调用set方法是给name赋值,但既然是赋值就会调用set方法,因此会形成一个死循环,
    // this.name = val
    // 所以我们要声明一个变量newVal,用于接收这个变量
    newVal = val
  }
})
console.log(obj)
obj.name = 'xs'
console.log(obj.name)
console.log(obj)
运行结果:{} set get xs {}

ES6 中引出 Proxy 拦截

语法

let p = new Proxy(target, handler)

解释

参数含义必选
target用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler一个对象,其属性是当执行一个操作时定义代理的行为的函数

第一个参数 target 就是用来代理的“对象”,被代理之后它是不能直接被访问的,第二个参数 handler 就是实现代理的过程。

常用拦截操作

get()

get()拦截对象属性的读取,比如proxy.foo和proxy['foo']。

get(target, propKey, receiver)接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

对于数组类型的拦截

let arr = [2, 3, 4]
arr = new Proxy(arr, {
  get (target, prop) {
    console.log(target, prop)
    return prop in target ? target[prop] : 'error'
  }
})
console.log(arr[1]) // (3) [2, 3, 4] '1'     3
console.log(arr[5]) // (3) [2, 3, 4] '5'     error

对于对象类型的拦截

let dist = {
  'hello': '你好',
  'world': '世界'
}
dist = new Proxy(dist, {
  get (target, prop) {
    return prop in target ? target[prop] : prop
  }
})
console.log(dist['hello']) // 你好
console.log(dist['xs']) // xs

set()

set()拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值;

set(target, propKey, value, receiver)可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

let arr = []
arr = new Proxy(arr, {
  set (target, prop, value) {
    if (typeof value === 'number') {
      target[prop] = value
      // return true
    } else {
      throw new Error('请传入Number类型的元素')
      // return false
    }
    return true
  }
})
arr.push(5)
console.log(arr[0]) // 5
arr.push(6)
console.log(arr[1]) // 6
console.log(arr.length) // 2
arr.push('1') // Uncaught Error: 请传入Number类型的元素

has()

has()判断对象是否具有某个属性,如:拦截propKey in proxy的操作,返回一个布尔值。

has(target, propKey)方法可以接受两个参数,分别是目标对象、需查询的属性名。

// 例如:有一个范围,从1开始到5结束,我想判断传进来的值是否在这个范围当中
let range = {
  start: 1,
  end: 5
}
range = new Proxy(range, {
  has (target, prop) {
    return prop >= target.start && prop <= target.end
  }
})
console.log(2 in range) // true
console.log(8 in range) // false

ownKeys()

ownKeys(target)拦截Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。

ownKeys(target)返回目标对象 所有 自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的 可遍历属性

let obj = {
  name: 'xs',
  [Symbol('like')]: 'es6'
}
console.log(Object.getOwnPropertyNames(obj)) // ['name']
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(like)]
console.log(Object.keys(obj)) // ['name']
for (let key in obj) {
  console.log(key) // name
}

案例:假如有一个包含用户的一些信息的对象,对开头带有下划线_的私有属性不被遍历出来

let useInfo = {
  name: 'xs',
  age: 18,
  _password: '******'
}
useInfo = new Proxy(useInfo, {
  ownKeys (target) {
    return Object.keys(target).filter(key => !key.startsWith('_'))
  }
})
for (let key in useInfo) {
  console.log(key) // name   age
}
console.log(Object.keys(useInfo)) // ['name', 'age']

注意:

ownKeys()方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。

deleteProperty()

deleteProperty(target, propKey)拦截delete proxy[propKey]的操作,返回一个布尔值。

// 删除开头带有下划线`_`的属性
let obj = {
  name: 'xs',
  age: 18,
  _password: '******'
}
console.log(Object.keys(obj)) // ['name', 'age', '_password']
obj = new Proxy(obj, {
  deleteProperty (target, prop) { // 拦截删除
    if (prop.startsWith('_')) {
      delete target[prop]
      return true
    } else {
      throw new Error('对象中没有以下划线开头的属性')
    }
  },
})
delete obj._password
for (let key in obj) {
  console.log(key) // name   age
}
console.log(Object.keys(obj)) // ['name', 'age']
try {
  delete obj.age
} catch (e) {
  console.log(e.message) // 对象中没有以下划线开头的属性
}

注意

目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

apply()

apply(target, object, args)拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。

apply(target, object, args)方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

// 对一个组数字的进行累加,再将结果扩大2倍
let sum = (...args) => {
  let num = 0
  args.forEach(item => {
    num += item
  })
  return num
}
sum = new Proxy(sum, {
  apply (target, ctx, args) {
    return target(...args) * 2
  }
})
console.log(sum(1, 2)) // 6
console.log(sum.call(null, 1, 2, 3)) // 12
console.log(sum.apply(window, [1, 2, 3, 4])) // 20

construct()

construct()方法用于拦截new命令(实例化对象的时候)。 construct(target, args, newTarget)方法可以接受三个参数,依次是目标对象(必须是函数)、构造函数的参数数组、创造实例对象时new命令作用的构造函数(下面的NewUser)。而且结果必须返回一个对象

let User = class {
  constructor(name) {
    this.name = name
  }
}
// 注意,construct()方法中的this指向的是handler,而不是实例对象。
let NewUser = new Proxy(User, {
  construct (target, args, newTarget) {
    console.log(this, 'this') // {construct: ƒ} 'this'
    console.log(target, 'target') // 指的是class类
    console.log(args, 'args') // ['xs'] 'args'
    console.log(newTarget, 'newTarget') // Proxy {length: 1, name: 'User', prototype: {…}} 'newTarget'
    return new target(...args)
  }
})
console.log(new NewUser('xs')) // User {name: 'xs'}

拦截操作的场景

场景一

我们在读取一个对象的key-value时,

let obj = {
  name:'xs',
  age:18,
}
console.log(obj.name) // xs
console.log(obj.age) // 18
console.log(obj.like) // undefined

当这个key不存在的时候,会返回undefined,我们常规解决办法是console.log(obj.like || 'es')

在 ES6 的 Proxy 中可以这么写来解决这个问题

let obj = {
  name:'xs',
  age:18,
}
let p = new Proxy(obj,{
  get(target,prop){
    return Reflect.has(target,prop) ? target[prop] : 'es'
  }
})
console.log(p.like) // es

这个代码的意思是如果obj对象有这个key-value,则直接返回,如果没有一律返回 es ,当然这里是自定义,我们可以根据自己的实际情况来写适合自己业务的规则。

上面讲述的是对数据的“读操作”进行了拦截,接下来我们描述下“写操作”进行拦截。

场景二

从服务端获取的数据希望是只读,不允许在任何一个环节被修改。

// 在ES5中只能通过遍历把所有属性设置为只读
let response = {
  data:{
    name:'xs',
    age:18
  }
}
for(let key of Object.keys(response.data)){
  Object.defineProperty(response.data,key,{
    writable: false,
  })
}
response.data.name = 'xiaoming'
console.log(response.data.name) // xs

// 在ES6中就可以使用Proxy
let data = new Proxy(response.data,{
  set(target,prop,value){
    return false
  }
})
data.age = 20
console.log(data.age) // 18

场景三

对数据进行校验

// Validator.js
export default (obj,key,value) => {
  if(Reflect.has(obj,key) && value > 18){
    obj[key] = value
  }
}

import Validator from './Validator.js'
let data = new Proxy(response.data,{
  set: Validator
})

场景四

对读写进行监控

let validator = {
  set(target,prop,value){
    if(prop === 'age'){
      if(typeof value !== 'number' || Number.isNaN(value)){
        throw new Error('Age must be a number')
      }
      if(value <= 0){
        throw new Error('Age must be a positive number')
      }
      if(!Number.isInteger(value)){
        throw new Error('Age must be an integer')
      }
    }
  target[prop] = value
  return true
  }
}
let p = new Proxy({},validator)
p.age = 18 // 18
p.age = NaN // Uncaught Error: Age must be a number
p.age = null // Uncaught Error: Age must be a number
p.age = 16.66 // Uncaught Error: Age must be an integer
p.age = -1 // Uncaught Error: Age must be a positive number