ES6功能扩展-代理(Proxy)和反射(Reflect)

代理和反射

代理

代理(Proxy)是一种可以拦截并改变底层JS引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象

通过new Proxy()可以创建目标对象的代理,它虚拟化了目标,所以两者看起来功能一致

代理可以拦截JS引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数

反射

反射API以Reflect对象的形式出现,Reflect对象中方法的默认特性与JS引擎的底层操作一致,而代理可以覆盖这些操作,所以每个代理陷阱都可以覆写JS对象的一些内建特性。如果仍需要使用这些内建特性,可以使用相应的反射(Reflect) API 方法。

下表总结了代理陷阱的特性:

代理陷阱 覆写的特性 默认特性
get 读取一个属性值 Reflect.get()
set 设置一个属性值 Reflect.set()
has in操作符 Reflect.has()
deleteProperty delete操作符 Reflect.deleteProperty
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols() Reflect.ownKeys()
apply 调用一个函数 Reflect.apply()
construct 用new调用一个函数 Reflect.construct()

创建简单代理

用Proxy构造函数创建代理需要传入两个参数:目标对象和处理程序。处理程序用于定义陷阱对象。

let target = {}
let proxy = new Proxy(target, {})
proxy.name = 'hello'
console.log(proxy.name, target.name) // hello hello

target.name = 'world'
console.log(proxy.name, target.name) // world world

示例中的proxy是一个代理,把为其添加的name属性转发给了target,它自身不会存储这个属性。由于proxy.nametarget.name引用的都是target.name,因此两者的值相同

set陷阱

任务:创建一个对象,当为对象新增属性时,验证属性值是不是一个数字,如果不是就抛出错误。

现在用set陷阱覆写设置值的默认特性,set陷阱接收4个参数

trapTarget: 用于接收代理目标的对象
key: 要写入的属性键
value: 被写入的属性值
receiver: 操作发生的对象(通常是代理)
let target = {name: 'hello'}
let proxy = new Proxy(target, {
  set(trapTarget, key, value, receiver) {
    console.log(trapTarget, receiver)
    // 如果新添加了一个属性,并且属性值不是数字
    if(!trapTarget.hasOwnProperty(key) && isNaN(value)) {
      throw new TypeError('Property must be a number.')
    }
    // 添加属性
    return Reflect.set(trapTarget, key, value, receiver)
  }
})

proxy.name = 'wmui' // 已有属性可以赋值任意值
console.log(proxy.name, target.name) // wmui wmui 

proxy.count = 1  // 新增属性可以赋值数字
console.log(proxy.count, target.count) // 1 1 

proxy.age = '18岁'  // 新增属性赋值非数字报错

示例中trapTarget就是target, receiver就是代理

Reflect.set()是set陷阱对应的默认反射方法,它和set陷阱接收相同的参数

set代理陷阱可以拦截写入属性的操作,get代理陷阱可以拦截读取属性的操作

get陷阱

使用get陷阱验证对象结构

任务:当读取对象上不存在的属性时抛出错误,而不是返回默认值undefined

get陷阱接收3个参数

trapTarget 被读取属性的源对象
key 要读取的属性键(字符串或Symbol)
receiver 操作发生的对象(通常是代理)
let obj = new Proxy({}, {
  get(trapTarget, key, receiver) {
    // 如果属性不在代理对象上
    if(!(key in receiver)) {
      throw new TypeError("Property " + key + " doesn't exist.")
    }
    return Reflect.get(trapTarget, key, receiver) 
  }
})

obj.name = 'hello'
console.log(obj.name) // hello
console.log(obj.age) // Uncaught TypeError: Property age doesn't exist.

has陷阱

使用has陷阱隐藏已有属性

任务:in操作符来检测给定对象是否含有某个属性,如果拥有这个属性则返回true。现在使用has陷阱改变一部分属性被in检测时的行为,即隐藏已有属性。

has陷阱接收2个参数

trapTarget读取属性的对象(代理的目标)
key要检查的属性键(字符串或Symbol)
let target = {
  name: 'wmui',
  age: 18
}

console.log('name' in target, 'toString' in target) // true true

默认情况下,对象自有的属性和继承自Object的属性都可以被in操作符检测到

现在使用Reflect.has()方法隐藏name属性

let target = {
  name: 'wmui',
  age: 18
}

let proxy = new Proxy(target, {
  has(trapTarget, key) {
    if(key === 'name') {
      return false
    }
    return Reflect.has(trapTarget, key)
  }
})

console.log('name' in proxy, 'toString' in proxy) // false true

deleteProperty陷阱

使用deleteProperty陷阱防止删除对象属性

任务:不允许使用delete操作符强制删除某个属性

delete操作符可以从对象中移除属性,如果成功则返回true,不成功则返回false。如果删除一个不可配置属性,在非严格模式下返回false,严格模式下直接报错

let target = {
  name: 'wmui',
  age: 18
}
target.defineProperty(target, 'name', {
  configurable: false
})

let ret = delete target.name
console.log(ret, 'name' in target) 
// false true
// 严格模式下报错 Uncaught TypeError: Cannot delete property 'name'

Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,通过delete操作符删除对象属性时,deleteProperty陷阱会被调用,它接收两个参数

trapTarget 要删除属性的对象(代理的目标)
key 要删除的属性键(字符串或Symbol)
let target = {
  name: 'wmui',
  age: 18
}

let proxy = new Proxy(target, {
  deleteProperty(trapTarget, key) {
    if(key === 'name') {
      return false
    }
    return Reflect.deleteProperty(trapTarget, key)
  }
})

let ret = delete proxy.name
console.log(ret, 'name' in proxy) // false true

如果希望保护属性不被删除,而且在严格模式下不抛出错误,那么这个方法非常有用

原型代理陷阱

原型代理陷阱指的是setPrototypeOf()陷阱和getPrototype()陷阱

setPrototypeOf()陷阱接收两个参数

trapTarget 原型设置的对象(代理的目标)
proto 作为原型使用的对象

getPrototype()陷阱接收一个参数

trapTarget 原型获取的对象(代理的目标)

通过代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以拦截Object.setPrototypeOf()和Object.getPrototypeOf()这两个方法的默认执行过程

注意: getPrototypeOf()陷阱必须返回一个对象或null,否则会报错。在setPrototypeOf()陷阱中,如果返回false则表示操作失败,如果返回了任何不是false的值则表示操作成功

let target = {}
let proxy = new Proxy(target, {
  getPrototypeOf(trapTarget) {
    return null
  },
  setPrototypeOf(trapTarget, proto) {
    return false
  }
})

let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)
console.log(targetProto === Object.prototype, proxyProto === Object.prototype) // true false
console.log(proxyProto) // null

Object.setPrototypeOf(target, {}) // 成功
Object.setPrototypeOf(proxy, {}) // 报错

如果需要使用这两个陷阱的默认行为,则可以使用Reflect上的相应方法

let target = {}
let proxy = new Proxy(target, {
  getPrototypeOf(trapTarget) {
    return Reflect.getPrototypeOf(trapTarget)
  },
  setPrototypeOf(trapTarget, proto) {
    return Reflect.setPrototypeOf(trapTarget, proto)
  }
})

let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)
console.log(targetProto === Object.prototype, proxyProto === Object.prototype) // true true

Object.setPrototypeOf(target, {}) // 成功
Object.setPrototypeOf(proxy, {}) // 成功

Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法疑似Object.getPrototypeOf()方法和Object.setPrototypeOf()方法,尽管两组方法执行相似的操作,但两者间仍有一些不同之处

两组方法的差异

ES6中提供了Object.setPrototypeOf()方法,它接收和Reflect.setPrototypeOf()相同的参数。ES5中提供了Object.getPrototypeOf()方法,它接收和Reflect.getPrototype()相同的参数。尽管接收的参数一样,行为看起来也相似,但它们还是有一些不同之处

Object.getPrototypeOf()和Object.setPrototypeOf()是给开发者使用的高级操作;而Reflect.getPrototypeOf()方法和Reflect.setprototypeOf()方法则是底层操作,其赋予开发者可以访问之前只在内部操作的[[GetPrototypeOf]]和[[SetPrototypeOf]]的权限

Reflect.getPrototypeOf()方法是内部[[GetprototypeOf]]操作的包裹器,Reflect.setPrototypeOf()方法是内部[[SetprototypeOf]]操作的包裹器。Object上相应的方法虽然也调用了[[GetPrototypeOf]]和[[Setprototypeof]],但在此之前会执行一些额外步骤,并通过检查返回值来决定下一步的操作

比如说如果传入的参数不是对象,Reflect.getPrototypeOf()方法会抛出错误,而Object.getPrototypeOf()方法则会在操作执行前先将参数强制转换为一个对象

let ret = Object.getPrototypeOf(1)
console.log(ret === Number.prototype) // true
Reflect.getPrototypeOf(1) // 报错

Object.getPrototypeOf()方法会强制让数字1变为Number对象,所以可以检索它的原型并得到返回值Number.prototype;而由于Reflect.getPrototypeOf()方法不强制转化值的类型,而且1又不是一个对象,故会抛出一个错误

对于Reflect.setPrototypeOf()方法与Object.setPrototypeOf()方法,Reflect.setPrototypeOf()方法返回一个布尔值来表示操作是否成功,成功时返回true,失败则返回false;而Object.setPrototypeOf()方法成功则返回第一个参数作为它的值,一旦失败则会抛出一个错误

let target1 = {}
let result1 = Object.setPrototypeOf(target1, {})
console.log(result1, result1 === target1) // {} true

let target2 = {}
let result2 = Reflect.setPrototypeOf(target2, {})
console.log(result2, result2 === target2) // true false

除了上面提到的这些差异外,在Object和Reflect上还有更多看似重复的方法,但是在所有代理陷阱中一定要使用Reflect上的方法

isExtensible和preventExtensions陷阱

ES5通过Object.preventExtensions()方法和Object.isExtensible()方法修正了对象的可扩展性,ES6可以通过代理中的preventExtensions和isExtensible陷阱拦截这两个方法并调用底层对象操作。

两个陷阱都接收唯一参数trapTarget对象,isExtensible陷阱返回一个布尔值表示对象是否可扩展,preventExtensions也返回一个布尔值,表示操作是否成功

Reflect.preventExtensions()方法和 Reflect.isExtensible()方法实现相应陷阱中默认行为,二者都返回布尔值

let target = {}

let proxy = new Proxy(target, {
  isExtensible(trapTarget) {
    return Reflect.isExtensible(trapTarget)
  },
  preventExtensions(trapTarget) {
    return Reflect.preventExtensions(trapTarget)
  }
})

console.log(Object.isExtensible(target), Object.isExtensible(proxy)) // true true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(target), Object.isExtensible(proxy)) // false false

这个示例实现了isExtensible和preventExtensions陷阱的默认行为,如果想让Object.preventExtensions()对于proxy失效,那么可以在preventExtensions陷阱中返回false

let target = {}

let proxy = new Proxy(target, {
  isExtensible(trapTarget) {
    return Reflect.isExtensible(trapTarget)
  },
  preventExtensions(trapTarget) {
    return false
  }
})

console.log(Object.isExtensible(proxy)) // true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(proxy)) // true

Object.isExtensible()和Object.preventExtensions()方法

Object.isExtensible()方法和Reflect.isExtensible()方法相似,当传入非对象值时,Object.isExtensible()返回false,而Reflect.isExtensible()则抛出一个错误

let result1 = Object.isExtensible(3)
console.log(result1) // false

let result2 = Reflect.isExtensible(3)// 抛出错误

对于Object.preventExtensions()方法和Reflect.preventExtensions()方法,无论传入Object.preventExtensions()方法的参数是否为一个对象,它总是返回该参数;而如果Reflect.preventExtensions()方法的参数不是对象就会抛出错误,如果参数是一个对象,操作成功时会返回true,否则返回false

let result1 = Object.preventExtensions(3)
console.log(result1) // 3

let result2 = Reflect.preventExtensions(3) // 抛出错误

属性描述符陷阱

ES5中可以通过Object.defineProperty()方法定义属性特性,并且可以通过Object.getOwnPropertyDescriptor()方法来获取这些属性

在代理中可以分别用defineProperty陷阱和getOwnPropertyDescriptor陷阱拦截 Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法的调用。definePropepty陷阱接受以下参数

trapTarget 要定义属性的对象(代理的目标)
key 属性的键(字符串或Symbol)
descriptor 属性的描述符对象

defineProperty陷阱需要在操作成功后返回true,否则返回false。getOwnPropertyDescriptor陷阱只接受trapTarget和key两个参数,最终返回描述符。

Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法与对应的陷阱接受相同参数。

let target = {}
let proxy = new Proxy(target, {
  defineProperty(trapTarget, key, descriptor) {
    return Reflect.defineProperty(trapTarget, key, descriptor)
  },
  getOwnPropertyDescriptor(trapTarget, key) {
    return Reflect.getOwnPropertyDescriptor(trapTarget, key)
  }
})

Object.defineProperty(proxy, 'name', { value: 'proxy' })
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name')
console.log(proxy.name, descriptor.value) // proxy proxy

这个示例展示了陷阱的默认行为

defineProperty陷阱返回布尔值来表示操作是否成功。返回true时,Object.defineProperty()方法成功执行;返回false时,Object.defineProperty()方法抛出错误。这个功能可以用来限制Object.defineProperty()方法可定义的属性类型,例如,如果希望阻止Symbol类型的属性,则可以当属性键为symbol时返回false

let target = {}
let proxy = new Proxy(target, {
  defineProperty(trapTarget, key, descriptor) {
    if(typeof key === 'symbol') {
      return false
    }else{
      return Reflect.defineProperty(trapTarget, key, descriptor)
    }
  },
  getOwnPropertyDescriptor(trapTarget, key) {
    return Reflect.getOwnPropertyDescriptor(trapTarget, key)
  }
})
Object.defineProperty(proxy, 'name', { value: 'proxy' }) // 正常
let nameSymbol = Symbol()
Object.defineProperty(proxy, nameSymbol, { value: 'nameSymbol' }) // 报错

这个示例中,调用Object.defineProperty()并传入'name',因此键的类型是字符串所以方法成功执行;调用Object.defineProperty()方法并传入nameSymbol,defineProperty陷阱返回false所以抛出错误

描述符对象限制

无论将什么对象作为第三个参数传递给Object.defineProperty()方法,都只有属性enumerable、configurable、value、writable、get和set将出现在传递给defineProperty陷阱的描述符对象中

let target = {}
let proxy = new Proxy(target, {
  defineProperty(trapTarget, key, descriptor) {
    console.log(descriptor.value, descriptor.name) // proxy undefined
    return Reflect.defineProperty(trapTarget, key, descriptor)
  }
})

Object.defineProperty(proxy, 'name', { 
  value: 'proxy', // 标准
  name: 'custom' // 非标准
})

示例中,调用Object.defineProperty()时传入包含非标准name属性的对象作为第三个参数,当defineProperty陷阱被调用时,descriptor对象有value属性却没有name属性,这是因为descriptor不是实际传入Object.defineProperty()方法的第三个参数的引用,而是一个只包含那些被允许使用的属性的新对象。Reflect.defineProperty()方法同样也忽略了描述符上的所有非标准属性

对于getOwnPropertyDescriptor陷阱,它的返回值必须是null、undefined或一个对象。如果返回对象,则对象自己的属性只能是enumepable、configurable、value、writable、get和set,在返回的对象中使用不被允许的属性会抛出一个错误

let target = {}
let proxy = new Proxy(target, {
  getOwnPropertyDescriptor(trapTarget, key) {
    return {
      name: 'custom'
    }
  }
})
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name') // 报错

属性描述符中不允许有name属性,所以当调用Object.getOwnPropertyDescriptor()时抛出了错误

两组方法的差异

Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法与Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法做了同样的事情,看起来很相似,但它们也有一些不同

Object.defineProperty()方法和Reflect.defineProperty()方法只有返回值不同:Object.defineProperty()方法返回第一个参数,而Reflect.defineProperty()的返回值与操作有关,成功则返回true,失败则返回false

// 示例1
let target = {}
let result = Object.defineProperty(target, 'name', { value: 'target'})
console.log(result, target === result) // {name: "target"} true

// 示例2
let target = {}
let result = Reflect.defineProperty(target, 'name', { value: 'reflect'})
console.log(result, target === result) // true fasle

Object.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,内部将这个值强制转换为一个对象;而调用Reflect.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,则抛出一个错误

// 示例1
let descriptor1 = Object.getOwnPropertyDescriptor(3, 'name');
console.log(descriptor1) // undefined

// 示例2
let descriptor2 = Reflect.getOwnPropertyDescriptor(3, 'name') // 抛出错误

ownKeys陷阱

ownKeys代理陷阱可以拦截内部方法[[OwnPropertyKeys]],通过返回数组的值可以覆盖其行为。这个数组被用于Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()4个方法

ownKeys陷阱通过Reflect.ownKeys()方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和Symbol类型的都包含在内。Object.getOwnPropertyNames()方法和Object.keys()方法返回的结果将Symbol类型的属性名排除在外,Object.getOwnPropertySymbols()方法返回的结果将字符串类型的属性名排除在外。Object.assign()方法支持字符串和Symbol两种类型

ownKeys陷阱唯一接受的参数是操作的目标,返回值必须是一个数组或类数组对象,否则就抛出错误。当调用Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()或Object.assign()方法时,可以用ownKeys陷阱来过滤掉不想使用的属性键。

假设不想引入任何以下划线字符(在JS中下划线符号表示字段是私有的)开头的属性名称,则可以用ownKeys陷阱过滤掉那些键

let proxy = new Proxy({}, {
  ownKeys(trapTarget) {
    return Reflect.ownKeys(trapTarget).filter(key => {
      return typeof key === 'symbol' ||  key[0] !== '_'
    })
  }
})

let nameSymbol = Symbol()
proxy.name = 'proxy'
proxy._name = 'private'
proxy[nameSymbol] = 'symbolName'

let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy)

console.log(names, keys, symbols) // ["name"] ["name"] [Symbol()]

for(const key in proxy) {
  console.log(key) // name
}

这个示例只返回Symbol类型以及非_开头的属性。ownKeys代理陷阱也会影响for/in循环。

apply和construct陷阱

所有的代理陷阱中,只有apply和construct的代理目标是一个函数。函数有两个内部方法[[Call]]和[[Construct]],apply陷阱和construct陷阱可以覆盖这些内部陷阱,若使用new操作符调用函数,则执行construct陷阱;若不用new调用,则调用apply陷阱

apply陷阱和Reflect.apply()接受以下参数

trapTaqget 被执行的函数(代理的目标)
thisArg 函数被调用时内部this的值
argumentsList 传递给函数的参数数组

construct陷阱和Reflect.construct接收以下参数

trapTarget 被执行的函数(代理的目标)
argumentsList 传递给函数的参数数组

Reflect.construct()方法除了接收这两个参数,还可以接收可选的第三个参数newTarget,该参数用于指定函数内部new.target的值

使用apply和construct陷阱,可以完全控制任何代理目标函数的行为

let target = function() { return 3 }
let proxy = new Proxy(target, {
  apply: function(trapTarget, thisArg, argumentList) {
    return Reflect.apply(trapTarget, thisArg, argumentList)
  },
  construct: function(trapTarget, argumentList) {
    return Reflect.construct(trapTarget, argumentList)
  }
})
// 使用了函数的代理,其目标对象会被视为函数
console.log(typeof proxy, proxy()); // function 3

let instance = new proxy()
console.log(instance instanceof proxy, instance instanceof target) // true true

示例中使用了apply陷阱和construct陷阱,代理函数与目标函数的结果完全相同。当用new调用时创建一个instance对象,它同时是代理和目标的实例,因为instanceof通过原型链来确定此信息,而原型链查找不受代理影响,这也就是代理和目标好像有相同原型的原因

应用:验证函数参数

现在有一个求和函数,要求所有参数都属于Number类型,如果不符合要求就抛出错误

// 求和函数
function sum(...values) {
  return values.reduce((prev, current) => prev + current, 0)
}

// 代理
let sumProxy = new Proxy(sum, {
  apply(trapTarget, thisArg, argumentList) {
    argumentList.forEach((arg) => {
      if (typeof arg !== 'number') {
        throw new TypeError('All arguments must be numbers.')
      }
    })
    return Reflect.apply(trapTarget, thisArg, argumentList)
  },
  // 不允许使用new调用
  construct: function(trapTarget, argumentList) {
    throw new TypeError("This function can't be called with new.")
  }
})

console.log(sumProxy(1, 2, 3, 4)) // 10
console.log(sumProxy(1, "2", 3, 4)) // 报错
let ret = new sumProxy() // 报错

应用:可调用的类构造函数

类调用时必须使用new操作符,因为类构造函数的内部方法[[Call]]被指定来抛出一个错误。由于类构造函数是也是函数,所以可以使用代理拦截对[[Call]]方法的调用。如果希望类构造函数不用new就可以运行,那么可以使用apply陷阱来创建一个新实例

class Person {
  constructor(name) {
      this.name = name
  }
}
let PersonProxy = new Proxy(Person, {
  apply: function(trapTarget, thisArg, argumentList) {
    return new trapTarget(...argumentList)
  }
})
let me = PersonProxy('wmui')
console.log(me.name) // wmui
console.log(me instanceof Person) // true
console.log(me instanceof PersonProxy) // true

可撤销代理

通常在创建代理后,代理不能脱离其目标,如果脱离了目标,代理便失去效力。如果希望创建的代理可以脱离目标,可以使用proxy.revocable()方法创建可撤销的代理,该方法采用与Proxy构造函数相同的参数:目标对象和代理处理程序,返回值是具有以下属性的对象

proxy 可被撤销的代理对象
revoke 撤销代理要调用的函数
let target = {
  name: 'target'
}

let {proxy, revoke} = Proxy.revocable(target, {})
console.log(proxy.name) // target

revoke()
console.log(proxy.name) // 报错

示例创建一个可撤销代理,它使用解构功能将proxy和revoke变量赋值给Proxy.revocable()方法返回的对象上的同名属性。之后proxy对象可以像不可撤销代理对象一样使用,一旦revoke()函数被调用,代理不再是函数,尝试访问proxy.name会抛出一个错误

posted @ 2021-09-29 13:02  wmui  阅读(194)  评论(0编辑  收藏  举报