跳到主要内容

元编程 Proxy 代理

  • Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法
const p = new Proxy(target, handler)
// target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

基础示例

  • 在以下简单的例子中,当对象中不存在属性名时,默认返回值为 37。下面的代码以此展示了 get handler 的使用场景。
const handler = {
get: function (obj, prop) {
return prop in obj ? obj[prop] : 37
},
}
const p = new Proxy({}, handler)
p.a = 1
p.b = undefined
console.log(p.a, p.b) // 1, undefined
console.log('c' in p, p.c) // false, 37

无操作转发代理

在以下例子中,我们使用了一个原生 JavaScript 对象,代理会将所有应用到它的操作转发到这个对象上。
let target = {}
let p = new Proxy(target, {})

p.a = 37 // 操作转发到目标

console.log(target.a) // 37. 操作已经被正确地转发

验证

通过代理,你可以轻松地验证向一个对象的传值。下面的代码借此展示了 set handler 的作用。
let validator = {
set: function (obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer')
}
if (value > 200) {
throw new RangeError('The age seems invalid')
}
}

// The default behavior to store the value
obj[prop] = value

// 表示成功
return true
},
}
let person = new Proxy({}, validator)
person.age = 100
console.log(person.age)
// 100
person.age = 'young'
// 抛出异常:Uncaught TypeError: The age is not an integer
person.age = 300
// 抛出异常:Uncaught RangeError: The age seems invalid

扩展构造函数

function extend(sup, base) {
var descriptor = Object.getOwnPropertyDescriptor(
base.prototype,
'constructor'
)
base.prototype = Object.create(sup.prototype)
var handler = {
construct: function (target, args) {
var obj = Object.create(base.prototype)
this.apply(target, obj, args)
return obj
},
apply: function (target, that, args) {
sup.apply(that, args)
base.apply(that, args)
},
}
var proxy = new Proxy(base, handler)
descriptor.value = proxy
Object.defineProperty(base.prototype, 'constructor', descriptor)
return proxy
}

var Person = function (name) {
this.name = name
}

var Boy = extend(Person, function (name, age) {
this.age = age
})

Boy.prototype.sex = 'M'

var Peter = new Boy('Peter', 13)
console.log(Peter.sex) // "M"
console.log(Peter.name) // "Peter"
console.log(Peter.age) // 13

操作 DOM 节点

有时,我们可能需要互换两个不同的元素的属性或类名。下面的代码以此为目标,展示了 set handler 的使用场景。
let view = new Proxy(
{
selected: null,
},
{
set: function (obj, prop, newval) {
let oldval = obj[prop]

if (prop === 'selected') {
if (oldval) {
oldval.setAttribute('aria-selected', 'false')
}
if (newval) {
newval.setAttribute('aria-selected', 'true')
}
}

// 默认行为是存储被传入 setter 函数的属性值
obj[prop] = newval

// 表示操作成功
return true
},
}
)

let i1 = (view.selected = document.getElementById('item-1'))
console.log(i1.getAttribute('aria-selected')) // 'true'

let i2 = (view.selected = document.getElementById('item-2'))
console.log(i1.getAttribute('aria-selected')) // 'false'
console.log(i2.getAttribute('aria-selected')) // 'true'

值修正及附加属性

以下products代理会计算传值并根据需要转换为数组。这个代理对象同时支持一个叫做 latestBrowser的附加属性,这个属性可以同时作为 getter 和 setter。
let products = new Proxy(
{
browsers: ['Internet Explorer', 'Netscape'],
},
{
get: function (obj, prop) {
// 附加一个属性
if (prop === 'latestBrowser') {
return obj.browsers[obj.browsers.length - 1]
}

// 默认行为是返回属性值
return obj[prop]
},
set: function (obj, prop, value) {
// 附加属性
if (prop === 'latestBrowser') {
obj.browsers.push(value)
return
}

// 如果不是数组,则进行转换
if (typeof value === 'string') {
value = [value]
}

// 默认行为是保存属性值
obj[prop] = value

// 表示成功
return true
},
}
)

console.log(products.browsers) // ['Internet Explorer', 'Netscape']
products.browsers = 'Firefox' // 如果不小心传入了一个字符串
console.log(products.browsers) // ['Firefox'] <- 也没问题,得到的依旧是一个数组

products.latestBrowser = 'Chrome'
console.log(products.browsers) // ['Firefox', 'Chrome']
console.log(products.latestBrowser) // 'Chrome'

通过属性查找数组中的特定对象

  • 以下代理为数组扩展了一些实用工具。
  • 如你所见,通过 Proxy,我们可以灵活地“定义”属性,而不需要使用 Object.defineProperties 方法。
  • 以下例子可以用于通过单元格来查找表格中的一行。在这种情况下,target 是 table.rows。
let products = new Proxy(
[
{ name: 'Firefox', type: 'browser' },
{ name: 'SeaMonkey', type: 'browser' },
{ name: 'Thunderbird', type: 'mailer' },
],
{
get: function (obj, prop) {
// 默认行为是返回属性值,prop ?通常是一个整数
if (prop in obj) {
return obj[prop]
}

// 获取 products 的 number; 它是 products.length 的别名
if (prop === 'number') {
return obj.length
}

let result,
types = {}

for (let product of obj) {
if (product.name === prop) {
result = product
}
if (types[product.type]) {
types[product.type].push(product)
} else {
types[product.type] = [product]
}
}

// 通过 name 获取 product
if (result) {
return result
}

// 通过 type 获取 products
if (prop in types) {
return types[prop]
}

// 获取 product type
if (prop === 'types') {
return Object.keys(types)
}

return undefined
},
}
)

console.log(products[0]) // { name: 'Firefox', type: 'browser' }
console.log(products['Firefox']) // { name: 'Firefox', type: 'browser' }
console.log(products['Chrome']) // undefined
console.log(products.browser) // [{ name: 'Firefox', type: 'browser' }, { name: 'SeaMonkey', type: 'browser' }]
console.log(products.types) // ['browser', 'mailer']
console.log(products.number) // 3

一个完整的 traps 列表示例

一个完整的 traps 列表示例
var docCookies = new Proxy(docCookies, {
get: function (oTarget, sKey) {
return oTarget[sKey] || oTarget.getItem(sKey) || undefined
},
set: function (oTarget, sKey, vValue) {
if (sKey in oTarget) {
return false
}
return oTarget.setItem(sKey, vValue)
},
deleteProperty: function (oTarget, sKey) {
if (sKey in oTarget) {
return false
}
return oTarget.removeItem(sKey)
},
enumerate: function (oTarget, sKey) {
return oTarget.keys()
},
ownKeys: function (oTarget, sKey) {
return oTarget.keys()
},
has: function (oTarget, sKey) {
return sKey in oTarget || oTarget.hasItem(sKey)
},
defineProperty: function (oTarget, sKey, oDesc) {
if (oDesc && 'value' in oDesc) {
oTarget.setItem(sKey, oDesc.value)
}
return oTarget
},
getOwnPropertyDescriptor: function (oTarget, sKey) {
var vValue = oTarget.getItem(sKey)
return vValue
? {
value: vValue,
writable: true,
enumerable: true,
configurable: false,
}
: undefined
},
})

/* Cookies 测试 */

alert((docCookies.my_cookie1 = 'First value'))
alert(docCookies.getItem('my_cookie1'))

docCookies.setItem('my_cookie1', 'Changed value')
alert(docCookies.my_cookie1)

handler.apply()

  • handler.apply() 方法用于拦截函数的调用。
实例:
function sum(a, b) {
return a + b
}
const handler = {
apply: function (target, thisArg, argumentsList) {
console.log(`Calculate sum: ${argumentsList}`)
// output: "Calculate sum: 1,2"
return target(argumentsList[0], argumentsList[1]) * 10
},
}
const proxy1 = new Proxy(sum, handler)
console.log(sum(1, 2))
// output: 3
console.log(proxy1(1, 2))
// output: 30
语法:
var p = new Proxy(target, {
apply: function (target, thisArg, argumentsList) {},
})

参数

以下是传递给 apply 方法的参数,this 上下文绑定在 handler 对象上。

  • target 目标对象(函数)。
  • thisArg 被调用时的上下文对象。
  • argumentsList 被调用时的参数数组。

返回值

  • apply 方法可以返回任何值。

handler.construct()

  • handler.construct() 方法用于拦截 new 操作符。
  • 为了使 new 操作符在生成的 Proxy 对象上生效,用于初始化代理的目标对象自身必须具有 [[Construct]] 内部方法(即 new target 必须是有效的)。
语法:
var p = new Proxy(target, {
construct: function (target, argumentsList, newTarget) {},
})
实例:
function monster1(disposition) {
this.disposition = disposition
}
const handler1 = {
construct(target, args) {
console.log(`Creating a ${target.name}`)
// output: "Creating a monster1"
return new target(...args)
},
}
const proxy1 = new Proxy(monster1, handler1)
console.log(new proxy1('fierce').disposition)
// output: "fierce"

参数

下面的参数将会传递给 construct 方法,this 绑定在 handler 上。

  • target 目标对象。
  • argumentsList constructor 的参数列表。
  • newTarget 最初被调用的构造函数,就上面的例子而言是 p。

返回值

  • construct 方法必须返回一个对象。

handler.defineProperty()

  • handler.defineProperty() 用于拦截对象的 Object.defineProperty() 操作。
以下代码演示如何拦截对目标对象的 Object.defineProperty() 操作。
var p = new Proxy(
{},
{
defineProperty: function (target, prop, descriptor) {
console.log('called: ' + prop)
return true
},
}
)
var desc = { configurable: true, enumerable: true, value: 10 }
Object.defineProperty(p, 'a', desc) // "called: a"

handler.deleteProperty()

  • handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。
语法:
var p = new Proxy(target, {
deleteProperty: function (target, property) {},
})

参数

  • deleteProperty 方法将会接受以下参数。this 被绑定在 handler 上。
  • target 目标对象。
  • property 待删除的属性名。

返回值

  • deleteProperty 必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。

handler.get()

  • handler.get() 方法用于拦截对象的读取属性操作。
语法:
var p = new Proxy(target, {
get: function (target, property, receiver) {},
})

参数

  • target 目标对象。
  • property 被获取的属性名。
  • receiver Proxy 或者继承 Proxy 的对象

返回值

  • get 方法可以返回任何值。

handler.getOwnPropertyDescriptor()

  • handler.getOwnPropertyDescriptor() 方法是 Object.getOwnPropertyDescriptor() 的钩子。
语法:
var p = new Proxy(target, {
getOwnPropertyDescriptor: function (target, prop) {},
})

参数

  • target 目标对象。
  • prop 返回属性名称的描述。

返回值

  • getOwnPropertyDescriptor 方法必须返回一个 object 或 undefined。

handler.getPrototypeOf()

  • handler.getPrototypeOf() 是一个代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用。
语法:
const p = new Proxy(obj, {
getPrototypeOf(target) {
...
}
});
实例:
const monster1 = {
eyeCount: 4,
}

const monsterPrototype = {
eyeCount: 2,
}

const handler = {
getPrototypeOf(target) {
return monsterPrototype
},
}
const proxy1 = new Proxy(monster1, handler)
console.log(Object.getPrototypeOf(proxy1) === monsterPrototype)
// output: true
console.log(Object.getPrototypeOf(proxy1).eyeCount)
// output: 2

参数

  • 当 getPrototypeOf 方法被调用时,this 指向的是它所属的处理器对象。
  • target 被代理的目标对象。

返回值

  • getPrototypeOf 方法的返回值必须是一个对象或者 null。

handler.has()

  • handler.has() 方法是针对 in 操作符的代理方法。
const handler1 = {
has(target, key) {
if (key[0] === '_') {
return false
}
return key in target
},
}

const monster1 = {
_secret: 'easily scared',
eyeCount: 4,
}

const proxy1 = new Proxy(monster1, handler1)
console.log('eyeCount' in proxy1)
// Expected output: true

console.log('_secret' in proxy1)
// Expected output: false

console.log('_secret' in monster1)
// Expected output: true

handler.isExtensible()

  • handler.isExtensible() 方法用于拦截对对象的 Object.isExtensible()。
语法:
const monster1 = {
canEvolve: true,
}

const handler1 = {
isExtensible(target) {
return Reflect.isExtensible(target)
},
preventExtensions(target) {
target.canEvolve = false
return Reflect.preventExtensions(target)
},
}

const proxy1 = new Proxy(monster1, handler1)
console.log(Object.isExtensible(proxy1))
// output: true
console.log(monster1.canEvolve)
// output: true
Object.preventExtensions(proxy1)
console.log(Object.isExtensible(proxy1))
// output: false
console.log(monster1.canEvolve)
// output: false

handler.ownKeys()

  • handler.ownKeys() 方法用于拦截 Reflect.ownKeys().
const monster1 = {
_age: 111,
[Symbol('secret')]: 'I am scared!',
eyeCount: 4,
}

const handler1 = {
ownKeys(target) {
return Reflect.ownKeys(target)
},
}

const proxy1 = new Proxy(monster1, handler1)

for (const key of Object.keys(proxy1)) {
console.log(key)
// output: "_age"
// output: "eyeCount"
}

handler.preventExtensions()

  • handler.preventExtensions() 方法用于设置对 Object.preventExtensions()的拦截
const monster1 = {
canEvolve: true,
}

const handler1 = {
preventExtensions(target) {
target.canEvolve = false
Object.preventExtensions(target)
return true
},
}

const proxy1 = new Proxy(monster1, handler1)

console.log(monster1.canEvolve)
// output: true
Object.preventExtensions(proxy1)
console.log(monster1.canEvolve)
// output: false

handler.set()

  • handler.set() 方法是设置属性值操作的捕获器。
语法:
const monster1 = { eyeCount: 4 }

const handler1 = {
set(obj, prop, value) {
if (prop === 'eyeCount' && value % 2 !== 0) {
console.log('Monsters must have an even number of eyes')
} else {
return Reflect.set(...arguments)
}
},
}

const proxy1 = new Proxy(monster1, handler1)

proxy1.eyeCount = 1
// output: "Monsters must have an even number of eyes"

console.log(proxy1.eyeCount)
// output: 4

proxy1.eyeCount = 2
console.log(proxy1.eyeCount)
// output: 2

handler.setPrototypeOf()

  • handler.setPrototypeOf() 方法主要用来拦截 Object.setPrototypeOf().
语法:
var p = new Proxy(target, {
setPrototypeOf: function (target, prototype) {},
})

参数

  • 以下参数传递给 setPrototypeOf 方法。

  • target 被拦截目标对象。

  • prototype 对象新原型或为 null.

返回值

  • 如果成功修改了[[Prototype]], setPrototypeOf 方法返回 true,否则返回 false.