Skip to content

Proxy

Proxy 用于修改默认行为。属于元编程(meta programming),即对编程语言进行编程。

Proxy :目标对象之前设置拦截,外界对该对象的访问,都必须先通过这层拦截。

js
var obj = new Proxy(
  {},
  {
    get: function (target, propKey, receiver) {
      console.log(`getting ${propKey}!`)
      return Reflect.get(target, propKey, receiver)
    },
    set: function (target, propKey, value, receiver) {
      console.log(`setting ${propKey}!`)
      return Reflect.set(target, propKey, value, receiver)
    }
  }
)
js
obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

ES6 提供 Proxy构造函数,生产 Proxy 实例

js
var proxy = new Proxy(target, handler)

target:所要拦截的目标对象

handler:定制拦截行为,处理函数

Proxy 使用

  1. 没有定制拦截行为,访问proxy等于访问target
js
var target = {}
var handler = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
target.a // "b"
  1. 将 Proxy 对象,设置到object.proxy属性,可以在object对象上调用
js
var object = { proxy: new Proxy(target, handler) }
  1. Proxy 实例也可作为其他对象的原型对象
js
var proxy = new Proxy(
  {},
  {
    get: function (target, propKey) {
      return 35
    }
  }
)

let obj = Object.create(proxy)
obj.time // 35
  1. 拦截多个操作
js
var handler = {
  get: function (target, name) {
    if (name === 'prototype') {
      return Object.prototype
    }
    return 'Hello, ' + name
  },

  apply: function (target, thisBinding, args) {
    return args[0]
  },

  construct: function (target, args) {
    return { value: args[1] }
  }
}

var fproxy = new Proxy(function (x, y) {
  return x + y
}, handler)

fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === 'Hello, foo' // true

Proxy 实例方法

Proxy.get

get(target, propKey, receiver):拦截对象属性的读取

  • target:目标对象

  • propKey:属性名

  • reciver(可选):Proxy 或继承 Proxy 对象

get方法可以继承

js
let proto = new Proxy(
  {},
  {
    get(target, propertyKey, receiver) {
      console.log('GET ' + propertyKey)
      return target[propertyKey]
    }
  }
)

let obj = Object.create(proto)
obj.foo // "GET foo"

第三个参数:指向原始读操作对象,一般为 Proxy 实例

getReceiver 属性:获取 receiver

js
const proxy = new Proxy(
  {},
  {
    get: function (target, key, receiver) {
      return receiver
    }
  }
)

proxy.getReceiver === proxy // true
js
const d = Object.create(proxy)
d.a === d // true

d对象本身没有a属性,d会去原型proxy对象寻找,此时 receiver指向d,代表原始读操作那个对象

如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性

js
const target = Object.defineProperties(
  {},
  {
    foo: {
      value: 123,
      writable: false,
      configurable: false
    }
  }
)

const handler = {
  get(target, propKey) {
    return 'abc'
  }
}

const proxy = new Proxy(target, handler)

proxy.foo
// TypeError: Invariant check failed

应用:实现数组负数索引

js
function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey)
      if (index < 0) {
        propKey = String(target.length + index)
      }
      return Reflect.get(target, propKey, receiver)
    }
  }

  let target = []
  target.push(...elements)
  return new Proxy(target, handler)
}

let arr = createArray('a', 'b', 'c')
arr[-1] // c

应用:实现属性链式操作

js
var pipe = function (value) {
  var funcStack = []
  var oproxy = new Proxy(
    {},
    {
      get: function (pipeObject, fnName) {
        // 非get, 函数入栈
        if (fnName === 'get') {
          return funcStack.reduce(function (val, fn) {
            // double(3) - 6
            // pow(6) - 36
            // reverseInt(36) - 63
            return fn(val)
          }, value)
        }
        funcStack.push(window[fnName])
        return oproxy
      }
    }
  )

  return oproxy
}

var double = (n) => n * 2
var pow = (n) => n * n
var reverseInt = (n) => n.toString().split('').reverse().join('') | 0

pipe(3).double.pow.reverseInt.get // 63

应用:生成各种 DOM 节点的通用函数dom(渲染函数)

js
const dom = new Proxy(
  {},
  {
    get(target, property) {
      // attrs - 属性
      // children - 子元素
      return function (attrs = {}, ...children) {
        const el = document.createElement(property)
        // 添加属性
        for (let prop of Object.keys(attrs)) {
          el.setAttribute(prop, attrs[prop])
        }
        for (let child of children) {
          // 为字符串,创建文本节点
          if (typeof child === 'string') {
            child = document.createTextNode(child)
          }
          el.appendChild(child)
        }
        return el
      }
    }
  }
)

const el = dom.div(
  {},
  'Hello, my name is ',
  dom.a({ href: '//example.com' }, 'Mark'),
  '. I like:',
  dom.ul({}, dom.li({}, 'The web'), dom.li({}, 'Food'), dom.li({}, "…actually that's it"))
)

document.body.appendChild(el)

Proxy.Set

set(target, propKey, value, receiver):拦截对象属性的设置

  • target:目标对象
  • propKey:属性名
  • value:属性值
  • receiver(可选):...

set代理应当返回一个布尔值。严格模式下,set代理返回false或者undefined,都会报错。

某个属性不可写(writable),set不起作用

第四参数 receiver,原始的操作行为所在的那个对象

js
const handler = {
  set: function (obj, prop, value, receiver) {
    obj[prop] = receiver
    return true
  }
}
const proxy = new Proxy({}, handler)
const myObj = {}
Object.setPrototypeOf(myObj, proxy)

myObj.foo = 'bar'
myObj.foo === myObj // true

应用:防止内部属性被外部读写

js
const handler = {
  get(target, key) {
    invariant(key, 'get')
    return target[key]
  },
  set(target, key, value) {
    invariant(key, 'set')
    target[key] = value
    return true
  }
}
function invariant(key, action) {
  if (key[0] === '_') {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`)
  }
}
const target = {}
const proxy = new Proxy(target, handler)
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property

Proxy.apply

apply(target, object, args):拦截函数的调用、callapply操作

  • target:目标对象
  • object:目标对象上下文(this
  • args:目标对象参数数组
js
var twice = {
  apply(target, ctx, args) {
    return Reflect.apply(...arguments) * 2
  }
}
function sum(left, right) {
  return left + right
}
var proxy = new Proxy(sum, twice)
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
// 直接调用`Reflect.apply`方法,也会被拦截
Reflect.apply(proxy, null, [9, 10]) // 38

Proxy.has

has(target, propKey):拦截HasProperty操作,判断是否具有某个属性。

  • target:目标对象
  • propKey:需查询属性名
  • 返回值:布尔值

注意项

  1. 对象不可配置(configurable),或禁止扩展,has 拦截将报错

  2. 拦截HasProperty操作,而不是HasOwnProperty操作,不管自身还是继承属性

  3. has()拦截只对in运算符生效,对for...in循环不生效

js
let stu1 = { name: '张三', score: 59 }
let stu2 = { name: '李四', score: 99 }

let handler = {
  has(target, prop) {
    if (prop === 'score' && target[prop] < 60) {
      console.log(`${target.name} 不及格`)
      return false
    }
    return prop in target
  }
}

let oproxy1 = new Proxy(stu1, handler)
let oproxy2 = new Proxy(stu2, handler)

'score' in oproxy1
// 张三 不及格
// false

'score' in oproxy2
// true

for (let a in oproxy1) {
  console.log(oproxy1[a])
}
// 张三
// 59

for (let b in oproxy2) {
  console.log(oproxy2[b])
}
// 李四
// 99

补充:禁止扩展对象

js
var obj = { a: 10 }
Object.preventExtensions(obj) // 禁止扩展

应用:隐藏属性,不被 in 发现

js
var handler = {
  has(target, key) {
    if (key[0] === '_') {
      return false
    }
    return key in target
  }
}
var target = { _prop: 'foo', prop: 'foo' }
var proxy = new Proxy(target, handler)
'_prop' in proxy // false

Proxy.construct

construct(target, args):拦截 Proxy 实例作为构造函数调用的(new)操作

  • target:目标对象,由于拦截构造函数,所以必须是函数

  • args:构造函数的参数数组

  • newTarget:创造实例对象时,new命令作用的构造函数

  • 返回值:返回必须为一个对象,否则会报错

  • this:指向handler,而非实例对象

    js
    const handler = {
      construct: function (target, args) {
        console.log(this === handler)
        return new target(...args)
      }
    }
    
    let p = new Proxy(function () {}, handler)
    new p() // true

Proxy.deleteProperty

deleteProperty(target, propKey):拦截delete的操作

  • target:目标对象
  • propKey:属性名
  • 返回值:返回一个布尔值。方法抛出错误或返回false,表示当前属性无法被delete

注意点

  1. 拦截后需要手动delete
  2. 目标对象自身的不可配置(configurable)的属性,无法删除,否则报错

@TODO

Proxy.defineProperty

defineProperty(target, propKey, propDesc):拦截Object.defineProperty操作

  • target:目标对象
  • propKey:属性名
  • propDesc:定义或修改的属性描述
  • 返回一个布尔值

只是拦截,需要重写逻辑

对象不可扩展(non-extensible):defineProperty()不能增加不存在属性

属性不可写(writable)或不可配置(configurable):defineProperty()方法不得改变这两个设置

Proxy.getOwnPropertyDescriptor

getOwnPropertyDescriptor(target, propKey):返回属性的描述对象或undefined。拦截Object.getOwnPropertyDescriptor()

Proxy.getPrototypeOf

getPrototypeOf(target):拦截获取对象原型,返回一个对象(必须是objectnull)。

拦截下面操作

  • Object.prototype.__proto__
  • Object.prototype.isPrototypeOf()
  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • instanceof

目标对象不可扩展(non-extensible), getPrototypeOf()方法必须返回目标对象的原型对象

Proxy.isExtensible

isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。

注意事项:

  1. 只能返回布尔值,其他值会自动转为布尔

  2. 强限制:返回值必须与目标对象的isExtensible属性保持一致,否则抛出错误

js
Object.isExtensible(proxy) === Object.isExtensible(target)

Proxy.ownKeys

ownKeys(target):拦截对象自身属性的读取操作

拦截下面操作

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in循环

注意事项:

  1. 必须返回数组,且数组成员为字符串Symbol。如果有其他类型的值,或者返回的根本不是数组,就会报错。
js
var obj = {}

var p = new Proxy(obj, {
  ownKeys: function (target) {
    return [123, true, undefined, null, {}, []]
  }
})

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 123 is not a valid property name
  1. for...in 循环拦截,没有 a,b 属性,不会有任何输出
js
const obj = { hello: 'world' }
const proxy = new Proxy(obj, {
  ownKeys: function () {
    return ['a', 'b']
  }
})

for (let key in proxy) {
  console.log(key) // 没有任何输出
}
  1. 不可配置成员必须返回
js
var obj = {}
Object.defineProperty(obj, 'a', {
  configurable: false,
  enumerable: true,
  value: 10
})

var p = new Proxy(obj, {
  ownKeys: function (target) {
    return ['b']
  }
})

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
  1. 目标对象是不可扩展的(non-extensible),必须返回原对象的所有属性,且不能包含多余属性
js
var obj = {
  a: 1
}

Object.preventExtensions(obj)

var p = new Proxy(obj, {
  ownKeys: function (target) {
    return ['a', 'b']
  }
})

Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
  1. Object.keys()自动过滤以下三类属性,不会返回
  • 目标对象上不存在的属性
  • 属性名为 Symbol 值
  • 不可遍历(enumerable)的属性
js
let target = {
  a: 1,
  b: 2,
  c: 3,
  [Symbol.for('secret')]: '4'
}

Object.defineProperty(target, 'key', {
  enumerable: false,
  configurable: true,
  writable: true,
  value: 'static'
})

let handler = {
  ownKeys(target) {
    return ['a', 'd', Symbol.for('secret'), 'key']
  }
}

let proxy = new Proxy(target, handler)

Object.keys(proxy)
// ['a']

Proxy.preventExtensions

preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

限制:只有目标对象不可扩展(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true

解决:在proxy.preventExtensions()方法里面,调用一次Object.preventExtensions()

js
var proxy = new Proxy(
  {},
  {
    preventExtensions: function (target) {
      console.log('called')
      Object.preventExtensions(target)
      return true
    }
  }
)

Object.preventExtensions(proxy)
// "called"
// Proxy {}

Proxy.setPrototypeOf

setPrototypeOf(target, proto):返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。拦截Object.setPrototypeOf(proxy, proto)

注意:只能返回布尔值,否则会被自动转为布尔值

目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型

Proxy.revocable()

Proxy.revocable():返回一个可取消的 Proxy 实例

js
let target = {}
let handler = {}

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

proxy.foo = 123
proxy.foo // 123

revoke()
proxy.foo // TypeError: Revoked

使用场景:目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

this 问题

Proxy 代理针对目标对象的访问,不是目标对象的透明代理。

即不做任何拦截,也无法保证与目标对象行为一致。

主要原因

Proxy 代理下,目标对象内部的this关键字会指向 Proxy 代理

js
const target = {
  m: function () {
    console.log(this === proxy)
  }
}
const handler = {}

const proxy = new Proxy(target, handler)

target.m() // false
proxy.m() // true

常见

  1. 由于 this 指向的变化,导致 Proxy 无法代理目标对象
js
const _name = new WeakMap()

class Person {
  constructor(name) {
    _name.set(this, name)
  }
  get name() {
    return _name.get(this)
  }
}

const jane = new Person('Jane')
jane.name // 'Jane'

const proxy = new Proxy(jane, {})
proxy.name // undefined
  1. 有些原生对象的内部属性,只有通过正确的this才能拿到
js
const target = new Date()
const handler = {}
const proxy = new Proxy(target, handler)

proxy.getDate()
// TypeError: this is not a Date object.

解决:通过 this 绑定原始对象

js
const target = new Date('2015-01-01')
const handler = {
  get(target, prop) {
    if (prop === 'getDate') {
      return target.getDate.bind(target)
    }
    return Reflect.get(target, prop)
  }
}
const proxy = new Proxy(target, handler)

proxy.getDate() // 1
  1. Proxy 拦截函数内部的this,指向的是handler对象
js
const handler = {
  get: function (target, key, receiver) {
    console.log(this === handler)
    return 'Hello, ' + key
  },
  set: function (target, key, value) {
    console.log(this === handler)
    target[key] = value
    return true
  }
}

const proxy = new Proxy({}, handler)

proxy.foo
// true
// Hello, foo

proxy.foo = 1
// true

实例:Web 服务的客户端

js
function createWebService(baseUrl) {
  return new Proxy(
    {},
    {
      get(target, propKey, receiver) {
        return () => httpGet(baseUrl + '/' + propKey)
      }
    }
  )
}
js
const service = createWebService('http://example.com/data')

service.employees().then((json) => {
  const employees = JSON.parse(json)
  // ···
})

相关链接

[-] Proxy