# ES6 Proxy

# 前言

  全文共计5万字左右,大约可阅读两小时,并不定时持续更新中。

  此文可能是关于Proxy相对较全的文章之一,总结了Proxy代理几乎所有的用法、示例和注意事项,也有对部分代码的细节分析。结合语法和ECMAScript规范,系统性地阐释了JavaScript对象的内部方法和内部槽,对比了普通对象与代理对象之间的差异和共同点。另外也包括一些运用场景,如何分析代理的错误问题,以及如何优化解决等等。

  建议阅读中根据目录细化拆分,并试着回答以下问题。

  • 什么是trap
  • 内部方法与trap、不变量三者的关系
  • ProxyReflect之间的联系和作用
  • 实例如何在规范层面获取私有属性
  • Proxy的缺点和局限性

# 语法

  ;Proxy (opens new window) 为构造函数,用于创建代理对象。参数target为被代理的对象,handler为配置对象,用于自定义不同的代理行为。

var proxy = new Proxy(target, handler)

  两个参数都不能为非对象,否则将抛出错误。

new Proxy(1, 2)
// Uncaught TypeError: Cannot create proxy with a non-object as target or handler

  ;Proxy构造函数不能继承,原因在于Proxy上没有prototype原型属性。

class P extends Proxy {}
// Uncaught TypeError: Class extends value does not have valid prototype property undefined

  手动添加上prototype属性则可以继承。

Proxy.prototype = {}

class P extends Proxy {}

var p = new P({}, {})
// Proxy {}

# get

  ;get(target, property, receiver) (opens new window) 用于代理对象的读取操作,包括proxy[prop]或者proxy.propObject.create(proxy)[prop]或者Reflect.get(proxy, property)

var object = {}
var handler = {
  get(target, property, receiver) {
    return 15
  },
}
var proxy = new Proxy(object, handler)

proxy.value // 15

  函数参数target为目标对象(被代理的对象),property为属性名称,receiver为代理对象,或者继承代理对象的对象。

  ;receiver为代理对象proxy

var object = {}
var handler = {
  get(target, property, receiver) {
    return receiver
  },
}
var proxy = new Proxy(object, handler)

proxy.value === proxy // true

  对象o不存在value属性,将沿着原型链读取proxyvalue属性,进而被get函数代理,receiver则为继承代理对象proxy的对象o

var object = {}
var handler = {
  get(target, property, receiver) {
    return receiver
  },
}
var proxy = new Proxy(object, handler)
var o = Object.create(proxy)

o.value === o // true

  观察发现,在执行proxy.valueo.value时,点运算符(.)左边的对象是谁,receiver就指向谁。

  换句话说receiver是触发读取操作时的对象。

# set

  ;set(target, property, value, receiver) (opens new window) 用于代理对象属性的赋值操作,包括proxy.prop = value或者proxy[prop] = valueObject.create(proxy)[prop] = value或者Reflect.set(proxy, property, value)。返回布尔值,表示是否设置成功。

var object = {}
var handler = {
  set(target, property, value, receiver) {
    object.value = 15
  },
}
var proxy = new Proxy(object, handler)

proxy.value = 1

proxy.value // 15

  函数参数target为目标对象(被代理的对象),property为属性名称,value为设置的属性值,receiver为代理对象,或者继承代理对象的对象。

  ;receiverget一致,点运算符左边的对象是谁,receiver就是谁,即触发赋值操作时的对象。

  严格模式下若返回false,将抛出错误。

'use strict'

var object = {}
var handler = {
  set(target, property, value, receiver) {
    return false
  },
}
var proxy = new Proxy(object, handler)

proxy.value = 15
// Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'value'

# has

  ;has(target, property) (opens new window) 用于代理判断对象是否有属性,例如inwith,包括prop in proxy或者prop in Object.create(proxy)with(proxy){ prop; }或者with(Object.create(proxy)){ prop; }Reflect.has(proxy, property)。返回布尔值,表示是否有属性。

var object = {}
var handler = {
  has(target, property) {
    return true
  },
}
var proxy = new Proxy(object, handler)

'value' in proxy // true

  参数target为目标对象(被代理的对象),property为属性名。

var object = {}
var handler = {
  has(target, property) {
    return true
  },
}
var proxy = new Proxy(object, handler)

with (proxy) {
  console.log(value)
  // Uncaught TypeError: Cannot read properties of undefined (reading 'log')
}

  简单分析以上代码的报错原因,执行到console.log语句时,引擎会确认proxy上是否有console属性。此时将调用handler.has()方法,默认返回true,表示proxy上存在console属性。

  然后去获取proxy.console的属性值,即undefined,紧接着执行undefined.log,由于undefined上并无log方法,将抛出错误。

  可选链?.比较适用此场景。

with (proxy) {
  console?.log(value)
}

# deleteProperty

  ;deleteProperty(target, property) (opens new window) 用于代理delete操作,包括delete proxy.prop或者delete proxy[prop]Reflect.deleteProperty(proxy, property)。返回布尔值,表示是否删除成功。

var object = {}
var handler = {
  deleteProperty(target, property) {
    return true
  },
}
var proxy = new Proxy(object, handler)

delete proxy.value // true

  参数target为目标对象(被代理的对象),property为属性名。

# ownKeys

  ;ownKeys(target) (opens new window) 用于代理枚举对象属性的操作,包括。

  • for...in
  • Object.keys(proxy) / Object.values(proxy) / Object.entries(proxy)
  • Object.getOwnPropertyNames(proxy)
  • Object.getOwnPropertySymbols(proxy)
  • Reflect.ownKeys(proxy)

# for...in

  ;for...in返回对象自身和继承的可枚举属性,不包括Symbol属性。

var object = { x: 1, [Symbol('y')]: 2 }

Object.defineProperty(object, 'z', {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

Object.setPrototypeOf(object, { m: 1, [Symbol('n')]: 2 })

for (var key in object) {
  console.log(key)
  // x
  // m
}

  代理for...in时,有三种属性会被ownKeys过滤,包括。

  • 目标对象上不存在的属性
  • Symbol属性
  • 目标对象上不可枚举(non-enumerable)的属性
var y = Symbol('y')
var object = { x: 1, [y]: 2 }

Object.defineProperty(object, 'z', {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

var handler = {
  ownKeys(target) {
    return ['w', 'x', y, 'z']
  },
}
var proxy = new Proxy(object, handler)

for (var key in proxy) {
  console.log(key)
  // x
}

# Object.keys

  ;Object.keys返回对象自身的可枚举属性,不包括Symbol属性。

var object = { x: 1, [Symbol('y')]: 2 }

Object.defineProperty(object, 'z', {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

Object.keys(object) // ['x']

  代理Object.keys时,与for...in一致,也会被ownKeys过滤。

var y = Symbol('y')
var object = { x: 1, [y]: 2 }

Object.defineProperty(object, 'z', {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

var handler = {
  ownKeys(target) {
    return ['w', 'x', y, 'z']
  },
}
var proxy = new Proxy(object, handler)

Object.keys(proxy) // ['x']
Object.values(proxy) // [1]
Object.entries(proxy) // [['x', 1]]

  注意Object.valuesObject.entries也会被ownKeys代理,且会被过滤。

# Object.getOwnPropertyNames

  ;Object.getOwnPropertyNames返回对象自身的属性,不包括Symbol属性。

var object = { x: 1, [Symbol('y')]: 2 }

Object.defineProperty(object, 'z', {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

Object.getOwnPropertyNames(object) // ['x', 'z']

  与for...inObject.keys的区别在于,代理Object.getOwnPropertyNames可返回目标对象不存在的属性。

var object = { x: 1 }

var handler = {
  ownKeys(target) {
    return ['x', 'y']
  },
}
var proxy = new Proxy(object, handler)

Object.getOwnPropertyNames(proxy) // ['x', 'y']

# Object.getOwnPropertySymbols

  ;Object.getOwnPropertySymbols返回对象自身的Symbol属性。

var object = { x: 1, [Symbol('y')]: 2 }

Object.defineProperty(object, Symbol('z'), {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

Object.getOwnPropertySymbols(object) // [Symbol(y), Symbol(z)]

  代理Object.getOwnPropertySymbols也可返回目标对象不存在的Symbol属性。

var x = Symbol('x')
var object = { [x]: 1 }

var handler = {
  ownKeys(target) {
    return [x, Symbol('y')]
  },
}
var proxy = new Proxy(object, handler)

Object.getOwnPropertySymbols(proxy) // [Symbol(x), Symbol(y)]

# Reflect.ownKeys

  ;Reflect.ownKeys返回对象自身所有属性。

var object = { x: 1, [Symbol('y')]: 2 }

Object.defineProperty(object, 'z', {
  value: 3,
  configurable: true,
  writable: true,
  enumerable: false,
})

Reflect.ownKeys(object) // ['x', 'z', Symbol(y)]

  代理Reflect.ownKeys也可返回目标对象不存在的属性。

var y = Symbol('y')
var object = { x: 1, [y]: 2 }

var handler = {
  ownKeys(target) {
    return ['v', Symbol('w'), 'x', y]
  },
}
var proxy = new Proxy(object, handler)

Reflect.ownKeys(proxy) // ['v', Symbol(w), 'x', Symbol(y)]

# 小结

语句 特性 ownKeys是否过滤 ownKeys是否可返回目标对象不存在的属性
for...in 对象自身和继承的可枚举属性,不含Symbol属性
Object.keys 对象自身的可枚举属性,不含Symbol属性
Object.getOwnPropertyNames 对象自身的所有属性,不含Symbol属性
Object.getOwnPropertySymbols 对象自身的所有Symbol属性
Reflect.ownKeys 对象自身的所有属性

  关系图。

# getOwnPropertyDescriptor

  ;getOwnPropertyDescriptor(target, property) (opens new window) 用于代理获取对象属性描述符的操作,包括Object.getOwnPropertyDescriptor(proxy, prop)或者Reflect.getOwnPropertyDescriptor(proxy, prop)Object.getOwnPropertyDescriptors(proxy)。返回值为描述符对象或者undefined

var object = { value: 1 }
var handler = {
  getOwnPropertyDescriptor(target, property) {
    return {
      value: 2,
    }
  },
}
var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// {
//   value: 2,
//   configurable: false,
//   writable: false,
//   enumerable: false,
// }

  参数target为目标对象(被代理的对象),property为属性名。

  ;Object.getOwnPropertyDescriptors内部依赖于Object.getOwnPropertyDescriptor,则也可代理Object.getOwnPropertyDescriptors

var object = { x: 1, y: 2 }
var handler = {
  getOwnPropertyDescriptor(target, property) {
    return {
      value: 2,
    }
  },
}
var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptors(proxy)
// {
//   x: {
//     value: 2,
//     configurable: true,
//     writable: false,
//     enumerable: false,
//   },
//   y: {
//     value: 2,
//     configurable: true,
//     writable: false,
//     enumerable: false,
//   },
// }

# defineProperty

  ;defineProperty(target, property, descriptor) (opens new window) 用于代理定义属性的操作,包括Object.defineProperty(proxy, prop, descriptor)或者Reflect.defineProperty(proxy, prop, descriptor)Object.defineProperties(proxy, descriptors)proxy.prop = value或者proxy[prop] = value。返回值为布尔值,表示操作是否成功。

var object = {}
var handler = {
  defineProperty(target, property, descriptor) {
	console.log(descriptor)
	// {
	//   configurable: true,
	//   enumerable: true,
	//   value: 1,
	//   writable: true,
	// }

    return true
  },
}
var proxy = new Proxy(object, handler)

proxy.value = 1

  参数target为目标对象(被代理的对象),property为属性名,descriptor为属性描述符。

  若handlersetdefineProperty都存在,将优先执行set

var object = {}
var handler = {
  defineProperty(target, property, descriptor) {
    console.log('defineProperty')

    return true
  },
  set(target, property, value, receiver) {
    console.log('set')

    return false
  },
}
var proxy = new Proxy(object, handler)

proxy.value = 1
// 'set'

  与set一致,严格模式下若返回false,将抛出错误。

'use strict'

var object = {}
var handler = {
  defineProperty(target, property, descriptor) {
    return false
  },
}
var proxy = new Proxy(object, handler)

proxy.value = 1
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'value'

# preventExtensions

  ;preventExtensions(target) (opens new window) 用于代理阻止对象拓展的操作,包括Object.preventExtensions(proxy)或者Reflect.preventExtensions(proxy)。返回布尔值,表示阻止成功。

var object = {}
var handler = {
  preventExtensions(target) {
    console.log('preventExtensions')

    return Reflect.preventExtensions(target)
  },
}
var proxy = new Proxy(object, handler)

Object.preventExtensions(proxy)
// 'preventExtensions'

# isExtensible

  ;isExtensible(target) (opens new window) 用于代理判断对象是否可拓展的操作,包括Object.isExtensible(proxy)或者Reflect.isExtensible(proxy)。返回值将转换为布尔值,表示是否可拓展。

var object = {}
var handler = {
  isExtensible(target) {
    return true
  },
}
var proxy = new Proxy(object, handler)

Object.isExtensible(proxy) // true

# getPrototypeOf

  ;getPrototypeOf(target) (opens new window) 用于代理获取对象原型的操作,包括。

  • Object.getPrototypeOf(proxy)
  • Reflect.getPrototypeOf(proxy)
  • proxy.__proto__
  • Object.prototype.isPrototypeOf(proxy)
  • proxy instanceof Object
var object = {}
var handler = {
  getPrototypeOf(target) {
    return new Number(1)
  },
}
var proxy = new Proxy(object, handler)

Object.getPrototypeOf(proxy) // Number {1}
Reflect.getPrototypeOf(proxy) // Number {1}
proxy.__proto__ // Number {1}
Number.prototype.isPrototypeOf(proxy) // true
proxy instanceof Number // true

# setPrototypeOf

  ;setPrototypeOf(target, prototype) (opens new window) 用于代理设置原型的操作,包括Object.setPrototypeOf(proxy, prototype)或者Reflect.setPrototypeOf(proxy, prototype)。返回布尔值,表示是否设置成功。

var object = {}
var handler = {
  setPrototypeOf(target, prototype) {
    return true
  },
}
var proxy = new Proxy(object, handler)

Reflect.setPrototypeOf(proxy, {}) // true

  参数target为目标对象(被代理的对象),prototype为原型对象或者null

# apply

  ;apply(target, thisArg, args) (opens new window) 用于代理函数的普通调用,包括proxy(args)proxy.call(thisArg, args)或者proxy.apply(thisArg, args)Reflect.apply(proxy, thisArg, args)

var func = function (x, y) {
  return x + y
}
var handler = {
  apply(target, thisArg, args) {
    return Reflect.apply(target, thisArg, args) * 3
  },
}
var proxy = new Proxy(func, handler)

proxy(1, 2) // 9
proxy.call(null, 3, 4) // 21
Reflect.apply(proxy, null, [5, 6]) // 33

  参数target为目标对象(被代理的对象),thisArg为被调用时的上下文对象thisargs为被调用时的参数数组。

Function.prototype.apply.call(proxy, null, [3, 4]) // 21

  分析以上语句,也就是Function.prototype.apply函数执行了call方法,函数内部this指向了proxy,参数为null [3, 4],即Function.prototype.apply(null, [3, 4])(内部thisproxy),等价于运行proxy.apply(null, [3, 4])

# construct

  ;construct(target, args, newTarget) (opens new window) 用于代理构造函数的new调用,包括new proxy(args)或者Reflect.construct(proxy, args, newTarget)。返回值为对象。

var F = function (x, y) {
  this.x = x
  this.y = y
}
var handler = {
  construct(target, args, newTarget) {
	console.log(newTarget === Ctr) // true

    return Reflect.construct(target, args, newTarget)
  },
}
var Ctr = new Proxy(F, handler)

new Ctr(1, 2) // F {x: 1, y: 2}

  参数target为目标对象(被代理的对象),args为被调用时的参数数组,newTargetnew命令作用的构造函数。

# Proxy.revocable

  ;Proxy.revocable (opens new window) 用于创建可撤销的代理对象。

var revocable = Proxy.revocable(target, handler)

  返回值为对象,包括proxyrevoke属性,proxy为新生成的代理对象,revoke为撤销代理的方法。

var object = {}
var handler = {
  get() {
    return 1
  },
}
var { proxy, revoke } = Proxy.revocable(object, handler)

console.log(proxy)
// {
//   [[Handler]]: Object,
//   [[Target]]: Object,
//   [[IsRevoked]]: false,
// }

revoke()

console.log(proxy)
// {
//   [[Handler]]: null,
//   [[Target]]: null,
//   [[IsRevoked]]: true,
// }

  代理对象被撤销后,执行任何的可代理操作都将抛出错误。

var object = {}
var handler = {
  get() {
    return 1
  },
}
var { proxy, revoke } = Proxy.revocable(object, handler)

proxy.value // 1

revoke()

proxy.value // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked

# 约束

# 内部方法

  ;JavaScript中创建的对象,会被引擎赋予很多 内部方法 (opens new window),在开发者层面是不可见的。

  普通对象上共有11个内部方法。

  • [[GetPrototypeOf]]()
  • [[SetPrototypeOf]](V)
  • [[IsExtensible]]()
  • [[PreventExtensions]]()
  • [[GetOwnProperty]](P)
  • [[DefineOwnProperty]](P, Desc)
  • [[HasProperty]](P)
  • [[Get]](P, Receiver)
  • [[Set]](P, V, Receiver)
  • [[Delete]](P)
  • [[OwnPropertyKeys]]()

  内部方法什么时候执行呢?

  举个例子,在删除对象属性时,引擎会去执行对象上的[[Delete]](P)内部方法。

delete object.prop

  函数作为一类特殊对象,还拓展了部分内部方法。在普通调用时,引擎会去执行[[Call]]内部方法。作为构造函数,被new调用时,引擎会去执行[[Construct]]内部方法。

  例如箭头函数只拓展了[[Call]],没有拓展[[Construct]]内部方法。因此只能普通调用,不能通过new调用,实际也就是箭头函数不能作为构造函数的根本原因。

# 对比

  那开发者能调用内部方法吗?

  答案是不能,内部方法是引擎层面的东西。

  虽然不能调用,但是有替代实现,即ES6新引入的Proxy代理函数。我们可以用JavaScript代码,自定义刚刚那13个内部方法。

  以获取属性值为例,对比普通对象和代理对象的差异。

var object = {}
object.value

var handler = { get() {} }
var proxy = new Proxy({}, handler)
proxy.value

  普通对象在获取属性值时,引擎会去执行对象上的[[Get]](P, Receiver)内部方法。而代理对象在获取属性值时,将会去执行handler对象上的get方法。

  ;handler对象中的13个方法刚好对应了引擎层面的13个内部方法。

Handler Method Internal Method
getPrototypeOf [[GetPrototypeOf]]()
setPrototypeOf [[SetPrototypeOf]](V)
isExtensible [[IsExtensible]]()
preventExtensions [[PreventExtensions]]()
getOwnPropertyDescriptor [[GetOwnProperty]](P)
defineProperty [[DefineOwnProperty]](P, Desc)
has [[HasProperty]](P)
get [[Get]](P, Receiver)
set [[Set]](P, V, Receiver)
deleteProperty [[Delete]](P)
ownKeys [[OwnPropertyKeys]]()
apply [[Call]]
construct [[Construct]]

# 不变量

  你可能发现了图示中的invariant,是什么东西呢?

  ;ECMA-262规范中的第 6.1.7.3 (opens new window) 小节给出了答案。

6.1.7.3 Invariants of the Essential Internal Methods
The Internal Methods of Objects of an ECMAScript engine must conform to the list of invariants specified below. Ordinary ECMAScript Objects as well as all standard exotic objects in this specification maintain these invariants. ECMAScript Proxy objects maintain these invariants by means of runtime checks on the result of traps invoked on the [[ProxyHandler]] object.
Any implementation provided exotic objects must also maintain these invariants for those objects. Violation of these invariants may cause ECMAScript code to have unpredictable behaviour and create security issues. However, violation of these invariants must never compromise the memory safety of an implementation.

  大致语义为,ECMAScript中内部方法有一些限制规则,称之为不变量(invariant)。目的是为了避免代码出现不可预测的行为从而导致产生安全问题。

  我们来细读规范中[[Construct]]()的不变量。

[[Construct]]()
The normal return type is Object.
The target must also have a [[Call]] internal method.

  第一条是方法返回类型必须为对象,第二条是目标对象必须有[[Call]]内部方法。

  接着再来看看刚才引用中加粗的部分,什么意思呢?

  即是说ECMAScript代理对象在运行trap时,会检测返回结果是否符合对应的不变量,以保持与内部方法的统一性。

trap,即Handler Method,也就是配置对象handler上的方法

  换句话说,不管是内部方法(Internal Method)还是代理方法(Handler Method),都要符合规范中特定的不变量。

  所以handler.construct()方法也要符合[[Construct]]()的不变量,例如第一条中返回值必须为对象。在运行时,不符合将抛出错误。

var handler = {
  construct() {
    return 1
  },
}
var P = new Proxy(function () {}, handler)

var p = new P()
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')

# get

  若目标对象的属性是不可配置(non-configurable)且不可写(non-writable)的,则返回值必须与目标对象的属性值相同。

var object = {}

Object.defineProperty(object, 'value', {
  value: 2,
  configurable: false,
  writable: false,
})

var handler = {
  get() {
    return 3
  },
}

var proxy = new Proxy(object, handler)

proxy.value
// Uncaught TypeError: 'get' on proxy: property 'value' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '2' but got '3')

  若目标对象的属性为不可配置(non-configurable)且没有get描述符,则返回值必须为undefined

var object = {}

Object.defineProperty(object, 'value', {
  configurable: false,
  set() {},
})

var handler = {
  get() {
    return 3
  },
}

var proxy = new Proxy(object, handler)

proxy.value
// Uncaught TypeError: 'get' on proxy: property 'value' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '3')

  个人认为,属性为不可配置且不可写,或者为不可配置且没有get,获取属性值时都将返回固定值。而属性读取的代理操作,实际也就没有意义,为了一致性则约束返回值与原值相同。

# set

  若目标对象的属性是不可配置(non-configurable)且不可写(non-writable)的,则必须返回false

var object = {}

Object.defineProperty(object, 'value', {
  value: 2,
  configurable: false,
  writable: false,
})

var handler = {
  set() {
    return true
  },
}

var proxy = new Proxy(object, handler)

proxy.value = 15
// Uncaught TypeError: 'set' on proxy: trap returned truish for property 'value' which exists in the proxy target as a non-configurable and non-writable data property with a different value

  若目标对象的属性为不可配置(non-configurable)且没有set描述符,则必须返回false

var object = {}

Object.defineProperty(object, 'value', {
  configurable: false,
  get() {},
})

var handler = {
  set() {
    return true
  },
}

var proxy = new Proxy(object, handler)

proxy.value = 15
// Uncaught TypeError: 'set' on proxy: trap returned truish for property 'value' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter

  类似的,属性为不可配置且不可写,或者为不可配置且没有set,都意味着原对象的属性值无法修改。而属性修改的代理操作,无论如何都不会成功,为了一致性则必须返回false

# has

  若目标对象的属性为不可配置(non-configurable),则必须返回true

var object = {}

Object.defineProperty(object, 'value', {
  configurable: false,
})

var handler = {
  has(target, property) {
    return false
  },
}

var proxy = new Proxy(object, handler)

'value' in proxy
// Uncaught TypeError: 'has' on proxy: trap returned falsish for property 'value' which exists in the proxy target as non-configurable

  属性为不可配置,原对象的属性不可能被删除,也就意味着原对象肯定有此属性。而是否有此属性的代理操作,也就没有任何意义了,为了一致性则必须返回true

# deleteProperty

  若目标对象的属性为不可配置(non-configurable),则必须返回false

var object = {}

Object.defineProperty(object, 'value', {
  configurable: false,
})

var handler = {
  deleteProperty(target, property) {
    return true
  },
}

var proxy = new Proxy(object, handler)

delete proxy.value
// Uncaught TypeError: 'deleteProperty' on proxy: trap returned truish for property 'value' which is non-configurable in the proxy target

  属性为不可配置,原对象的属性不可能被删除,而删除属性的代理操作,无论如何都不会成功,为了一致性则必须返回false

# ownKeys

  返回的结果列表中,必须要包含所有不可配置(non-configurable)的自身属性。

var object = {}

Object.defineProperty(object, 'x', {
  configurable: false,
})

var handler = {
  ownKeys(target) {
    return []
  },
}

var proxy = new Proxy(object, handler)

Reflect.ownKeys(proxy)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'x'

  若目标对象不可拓展,则必须返回所有的自身属性。

var object = { x: 1 }

Object.preventExtensions(object)

var handler = {
  ownKeys(target) {
    return []
  },
}

var proxy = new Proxy(object, handler)

Reflect.ownKeys(proxy)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'x'

  并且不可返回目标对象不存在的属性。

var object = {}

Object.preventExtensions(object)

var handler = {
  ownKeys(target) {
    return ['x']
  },
}

var proxy = new Proxy(object, handler)

Reflect.ownKeys(proxy)
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible

# getOwnPropertyDescriptor

  若目标对象的属性为不可配置(non-configurable),不可返回undefined,必须返回属性描述符。

var object = {}

Object.defineProperty(object, 'value', {
  value: 1,
  configurable: false,
})

var handler = {
  getOwnPropertyDescriptor(target, property) {
    return undefined
  },
}

var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned undefined for property 'value' which is non-configurable in the proxy target

  并且描述符必须与目标对象的一致。

var object = {}

Object.defineProperty(object, 'value', {
  value: 1,
  configurable: false,
})

var handler = {
  getOwnPropertyDescriptor(target, property) {
    return {
      value: 2,
      configurable: false,
    }
  },
}

var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'value' that is incompatible with the existing property in the proxy target

  若目标对象的属性可配置,返回的属性描述符中configurable必须为true

var object = {}

Object.defineProperty(object, 'value', {
  value: 1,
  configurable: true,
})

var handler = {
  getOwnPropertyDescriptor(target, property) {
    return {
      configurable: false,
    }
  },
}

var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'value' which is either non-existent or configurable in the proxy target

  若目标对象的属性不存在,返回的属性描述符中configurable必须为true

var object = {}
var handler = {
  getOwnPropertyDescriptor(target, property) {
    return {
      configurable: false,
    }
  },
}
var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'value' which is either non-existent or configurable in the proxy target

  若目标对象不可拓展,且属性存在,不可返回undefined

var object = { value: 1 }

Object.preventExtensions(object)

var handler = {
  getOwnPropertyDescriptor(target, property) {
    return undefined
  },
}

var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned undefined for property 'value' which exists in the non-extensible proxy target

  若目标对象不可拓展,且属性不存在,必须返回undefined

var object = {}

Object.preventExtensions(object)

var handler = {
  getOwnPropertyDescriptor(target, property) {
    return {
      value: 1,
    }
  },
}

var proxy = new Proxy(object, handler)

Object.getOwnPropertyDescriptor(proxy, 'value')
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'value' that is incompatible with the existing property in the proxy target

# defineProperty

  若目标对象不可拓展,必须返回false,表示不可添加新属性。

var object = {}

Object.preventExtensions(object)

var handler = {
  defineProperty(target, property, descriptor) {
    return true
  },
}

var proxy = new Proxy(object, handler)

proxy.value = 1
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for adding property 'value'  to the non-extensible proxy target

  若目标对象的属性不存在,定义的属性描述符configurable必须为true

var object = {}
var handler = {
  defineProperty(target, property, descriptor) {
    return true
  },
}
var proxy = new Proxy(object, handler)

Object.defineProperty(proxy, 'value', {
  configurable: false,
})
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for defining non-configurable property 'value' which is either non-existent or configurable in the proxy target

  若目标对象的属性为可配置(configurable),定义的属性描述符configurable必须为true

var object = {}

Object.defineProperty(object, 'value', {
  configurable: true,
})

var handler = {
  defineProperty(target, property, descriptor) {
    return true
  },
}
var proxy = new Proxy(object, handler)

Object.defineProperty(proxy, 'value', {
  configurable: true,
})
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for defining non-configurable property 'value' which is either non-existent or configurable in the proxy target

# preventExtensions

  若目标对象是可拓展的,必须返回false

var object = {}
var handler = {
  preventExtensions(target) {
    return true
  },
}
var proxy = new Proxy(object, handler)

Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible

# isExtensible

  ;Object.isExtensible(proxy)返回值必须与Object.isExtensible(target)一致。

var object = {}
var handler = {
  isExtensible(target) {
    return false
  },
}
var proxy = new Proxy(object, handler)

Object.isExtensible(proxy)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')

# getPrototypeOf

  若目标对象不可拓展,必须返回目标对象的实际原型。

var object = {}

Object.preventExtensions(object)

var handler = {
  getPrototypeOf(target) {
    return {}
  },
}

var proxy = new Proxy(object, handler)

Object.getPrototypeOf(proxy)
// Uncaught TypeError: 'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype

  对象不可拓展,不能修改原型的指向,也就意味着原型是固定的。而代理获取原型的操作,也就没有任何意义了,为了一致性则必须返回对象的实际原型。

# setPrototypeOf

  若目标对象不可拓展,必须返回false

var object = {}

Object.preventExtensions(object)

var handler = {
  setPrototypeOf(target, prototype) {
    return true
  },
}

var proxy = new Proxy(object, handler)

Reflect.setPrototypeOf(proxy, {})
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target

  目标对象不可拓展,不能修改原型指向。而代理设置原型的操作,无论如何都不会成功,为了一致性则必须返回false

# apply

  无。

# construct

  必须返回对象。

var F = function () {}
var handler = {
  construct(target, thisArg, args) {
    return 1
  },
}
var Ctr = new Proxy(F, handler)

new Ctr()
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')

# 小结

Handler Method 代理行为 不变量
get(target, property, receiver)
  • proxy[prop]
  • proxy.prop
  • Object.create(proxy)[prop]
  • Reflect.get(proxy, property)
  • 目标对象属性不可配置且不可写,返回值必须与目标对象的属性值相同
  • 目标对象属性不可配置且无get描述符,返回值必须为undefined
set(target, property, value, receiver)
  • proxy.prop = value
  • proxy[prop] = value
  • Object.create(proxy)[prop] = value
  • Reflect.set(proxy, property, value)
  • 目标对象属性不可配置且不可写,必须返回false
  • 目标对象属性不可配置且无set描述符,必须返回false
has(target, property)
  • prop in proxy
  • prop in Object.create(proxy)
  • with(proxy){ prop; }
  • with(Object.create(proxy)){ prop; }
  • Reflect.has(proxy, property)
  • 目标对象属性不可配置,必须返回true
deleteProperty(target, property)
  • delete proxy.prop
  • delete proxy[prop]
  • Reflect.deleteProperty(proxy, property)
  • 目标对象属性不可配置,必须返回false
ownKeys(target)
  • for...in
  • Object.keys(proxy) / Object.values(proxy) / Object.entries(proxy)
  • Object.getOwnPropertyNames(proxy)
  • Object.getOwnPropertySymbols(proxy)
  • Reflect.ownKeys(proxy)
  • 返回的结果列表,必须包含所有不可配置的自身属性
  • 目标对象不可拓展,必须返回所有的自身属性,且不可返回目标对象不存在的属性
getOwnPropertyDescriptor(target, property)
  • Object.getOwnPropertyDescriptor(proxy, prop)
  • Reflect.getOwnPropertyDescriptor(proxy, prop)
  • Object.getOwnPropertyDescriptors(proxy)
  • 目标对象属性不可配置,不可返回undefined,必须返回属性描述符,且描述符必须与目标对象的一致
  • 目标对象属性可配置,返回的属性描述符中configurable必须为true
  • 目标对象属性不存在,返回的属性描述符中configurable必须为true
  • 目标对象不可拓展,且属性存在,不可返回undefined
  • 目标对象不可拓展,且属性不存在,必须返回undefined
defineProperty(target, property, descriptor)
  • Object.defineProperty(proxy, prop, descriptor)
  • Reflect.defineProperty(proxy, prop, descriptor)
  • Object.defineProperties(proxy, descriptors)
  • proxy.prop = value
  • proxy[prop] = value
  • 目标对象不可拓展,必须返回false,即不可添加新属性
  • 目标对象属性不存在,定义的属性描述符configurable必须为true
  • 目标对象属性可配置,定义的属性描述符configurable必须为true
preventExtensions(target)
  • Object.preventExtensions(proxy)
  • Reflect.preventExtensions(proxy)
  • 目标对象可拓展,必须返回false
isExtensible(target)
  • Object.isExtensible(proxy)
  • Reflect.isExtensible(proxy)
  • Object.isExtensible(proxy)返回值必须与Object.isExtensible(target)一致
getPrototypeOf(target)
  • Object.getPrototypeOf(proxy)
  • Reflect.getPrototypeOf(proxy)
  • proxy.__proto__
  • Object.prototype.isPrototypeOf(proxy)
  • proxy instanceof Object
  • 目标对象不可拓展,必须返回目标对象的实际原型
setPrototypeOf(target, prototype)
  • Object.setPrototypeOf(proxy, prototype)
  • Reflect.setPrototypeOf(proxy, prototype)
  • 目标对象不可拓展,必须返回false
apply(target, thisArg, args)
  • proxy(args)
  • proxy.call(thisArg, args)
  • proxy.apply(thisArg, args)
  • Reflect.apply(proxy, thisArg, args)
-
construct(target, args, newTarget)
  • new proxy(args)
  • Reflect.construct(proxy, args, newTarget)
  • 必须返回对象

# 应用场景

# 数组负索引

function negative(array) {
  return new Proxy(array, {
    get(target, property, receiver) {
      if (typeof property === 'string' && property < 0) {
        property = target.length + Number(property)
      }

      return Reflect.get(target, property, receiver)
    },
  })
}

var array = negative([1, 2, 3])
array[-1] // 3

# 隐藏内部属性

var object = { _id: 1 }
var handler = {
  get(target, property, receiver) {
    if (property[0] === '_') return undefined

    return Reflect.get(target, property, receiver)
  },
}
var proxy = new Proxy(object, handler)

proxy._id // undefined

# 创建只读对象

function readonly(target) {
  return new Proxy(target, {
    set() {
      return true
    },
    deleteProperty() {
      return false
    },
    defineProperty() {
      return true
    },
    setPrototypeOf() {
      return false
    },
  })
}

var object = readonly({ value: 1 })
object.value = 2

object.value // 1

参考 vue (opens new window) 响应式api核心方法 readonly (opens new window) 创建只读代理

# 支持链式属性

function chaining(target) {
  return new Proxy(function () {}, {
    get(_, property) {
      return chaining((target ?? {})[property])
    },
    apply() {
      return target
    },
  })
}

var object = {x: 1}

console.log(object) // {x: 1}
console.log(object.x) // 1
console.log(object.x.y) // undefined
console.log(object.x.y.z) // Uncaught TypeError: Cannot read properties of undefined (reading 'z')

console.log(chaining(object)()) // {x: 1}
console.log(chaining(object).x()) // 1
console.log(chaining(object).x.y()) // undefined
console.log(chaining(object).x.y.z()) // undefined

# 属性值校验

function validate(object) {
  return new Proxy(object, {
    set(target, property, value, receiver) {
      if (property === 'age') {
        if (!Number.isInteger(value)) {
          throw new TypeError('The age is not an integer')
        }
      }

      return Reflect.set(target, property, value, receiver)
    },
  })
}

var person = validate({ age: 18 })
person.age = 18.5 // Uncaught TypeError: The age is not an integer

# 管道

function pipe(value) {
  var funcs = []
  var proxy = new Proxy(function () {}, {
    get(_, property) {
      funcs.push(window[property])

      return proxy
    },
    apply() {
      return funcs.reduce((result, func) => func(result), value)
    },
  })

  return proxy
}

var double = n => n * 2
var pow = n => n * n

pipe(3).double.pow() // 36

# 数据监听

<span>1</span>
<button>按钮</button>

function watch(object, callback) {
  return new Proxy(object, {
    set(target, property, value, receiver) {
      callback(property, value)

      return Reflect.set(target, property, value, receiver)
    },
  })
}

var object = { value: 1 }
var proxy = watch(object, (key, value) => {
  if (key === 'value') document.querySelector('span').innerText = value
})

document.querySelector('button').addEventListener('click', () => {
  proxy.value++
})

# 常见问题

# 为什么 Proxy 大多与 Reflect 结合使用?

  以属性赋值时打印日志为例。

var proxy = new Proxy({}, {
  set(...args) {
    console.log('setter')

    return Reflect.set(...args)
  },
})

proxy.value = 1 // setter

  ;Reflectset语义透传到目标对象上,保持了默认的赋值行为。

  以上也可写成。

var proxy = new Proxy({}, {
  set(target, property, value) {
    console.log('setter')

    target[property] = value

    return true
  },
})

  两者没有太大区别,但是注意像set/get此类trap相对容易,有些trap例如ownKeys,开发者几乎很难编写出来。

  相对合理的是,所有trap的默认行为都统一由Reflect来做。换句话说,无论Proxy代理何种操作,总是可以在Reflect上找到对应的默认行为,也说明了为什么trapReflect方法是一对一的。

  另外set/get中额外的参数receiver,用来保证this的正确传递。

var object = { get value() { return this } }
var x = new Proxy(object, {
  get(target, property, receiver) {
    return Reflect.get(target, property, receiver)
  },
})
var y = new Proxy(object, {
  get(target, property, receiver) {
    // or target[property]
    return Reflect.get(target, property)
  },
})

x.value === x // true
y.value === y // false

  综上所述。

  • 保持默认行为,透传语义,且正确传递this
  • 函数式编程,语义清晰,代码可自解释
  • 语言内部操作统一部署至Reflect,有利于管理和维护

# Proxy 如何代理 Map?

  ;JavaScript对象除了会被引擎赋予内部方法,也会赋予内部槽。内部槽不是属性,类似属性,用于在对象上记录状态或数据。

  以Map类型为例,引擎会赋予实例 [[MapData]] (opens new window) 内部槽,将各数据项存储在[[MapData]]上。

var map = new Map()

map.set(1, 2)

map // Map(1) {1 => 2}

  运行map.set(1, 2)时,由于实例上没有set方法,将沿着原型链寻找原型方法,则相当于运行Map.prototype.set.call(map, 1, 2)。紧接着Map.prototype.set方法内部将访问this实例对象的[[MapData]]内部槽,即this.[[MapData]],也就是map.[[MapData]],然后添加数据项。

var map = new Map()
var proxy = new Proxy(map, {})

proxy.set(1, 2) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver #<Map>

  而运行proxy.set(1, 2)时,handler为空proxy未代理任何操作,将获取map.set方法,则相当于Map.prototype.set.call(proxy, 1, 2)Map.prototype.set即访问内部槽proxy.[[MapData]]。由于proxy对象没有此内部槽,将抛出错误。

  那如何解决呢?

  可将set方法内部this固定为map

var map = new Map()
var proxy = new Proxy(map, {
  get(target, property, receiver) {
    var value = Reflect.get(target, property, receiver)

    if (target instanceof Map) {
      value = value.bind(target)
    }

    return value
  },
})

proxy.set(1, 2)

map // Map(1) {1 => 2}

# 内置对象

  以上可总结出,即使Proxy未代理任何操作,也会将目标对象上方法或访问器内部this的指向修改为代理对象。

var object = {
  method() {
    return this
  },
  get value() {
    return this
  },
}
var handler = {}
var proxy = new Proxy(object, handler)

proxy.value === proxy // true
proxy.method() === proxy // true

  进一步的,在目标对象方法或访问器内部,若严格依赖this实例对象上的内部槽,而代理对象没有,运行可能会造成错误。

  例如代理Set实例,proxy上没有 [[SetData]] (opens new window) 内部槽。

var set = new Set()
var proxy = new Proxy(set, {})

proxy.add(1) // Uncaught TypeError: Method Set.prototype.add called on incompatible receiver #<Set>

  优化为。

var set = new Set()
var proxy = new Proxy(set, {
  get(target, property, receiver) {
    var value = Reflect.get(target, property, receiver)

    if (target instanceof Set) {
      value = value.bind(target)
    }

    return value
  },
})

proxy.add(1)

set // Set(1) {1}

  或者代理Promise实例,proxy上没有 [[PromiseState]] (opens new window) 内部槽。

var promise = Promise.resolve()
var proxy = new Proxy(promise, {})

proxy.then(() => console.log(1))
// Uncaught TypeError: Method Promise.prototype.then called on incompatible receiver #<Promise>

  优化为。

var promise = Promise.resolve()
var proxy = new Proxy(promise, {
  get(target, property, receiver) {
    var value = Reflect.get(target, property, receiver)

    if (target instanceof Promise) {
      value = value.bind(target)
    }

    return value
  },
})

proxy.then(() => console.log(1)) // 1

  又或者代理Dateproxy上没有 [[DateValue]] (opens new window) 内部槽。

var date = new Date()
var proxy = new Proxy(date, {})

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

  优化为。

var date = new Date()
var proxy = new Proxy(date, {
  get(target, property, receiver) {
    var value = Reflect.get(target, property, receiver)

    if (target instanceof Date) {
      value = value.bind(target)
    }

    return value
  },
})

proxy.getFullYear() // 2022

# 私有属性

  类的私有属性中也存在类似问题。

class User {
  #name = 'xxx'
  
  getName() {
    return this.#name
  }
}
var user = new User()
var proxy = new Proxy(user, {})

proxy.getName() // Uncaught TypeError: Cannot read private member #name from an object whose class did not declare it

  先来看看ECMA-262规范中与私有属性有关的内部槽。

6.1.7.2 Object Internal Methods and Internal Slots
All objects have an internal slot named [[PrivateElements]], which is a List of PrivateElements. This List represents the values of the private fields, methods, and accessors for the object. Initially, it is an empty List.

  大致语义为所有对象都有[[PrivateElements]]内部槽,是一个PrivateElements列表,记录对象的私有属性、方法和访问器。

  ;PrivateElements又是什么呢?

6.2.9 The PrivateElement Specification Type
...
Values of the PrivateElement type are Record values whose fields are defined by Table 9. Such values are referred to as PrivateElements.

  ;PrivateElement是一种规范类型,字段包括。

  • [[Key]]:属性、方法或访问器的名称
  • [[Kind]]:种类,属性、方法或访问器的一种
  • [[Value]]:值
  • [[Get]]:访问器getter
  • [[Set]]:访问器setter

  ;PrivateElement字段与字段值一起称为PrivateElements

  以user实例为例,私有属性#name对应的PrivateElements为。

{
  [[Key]]: #name,
  [[Kind]]: field,
  [[Value]]: 'xxx',
}

  内部槽为。

{
  [[PrivateElements]]: [
    {
      [[Key]]: #name,
      [[Kind]]: field,
      [[Value]]: 'xxx',
    }
  ]
  ...
}

  而在this.#name获取私有属性时,将执行 PrivateElementFind (opens new window) 抽象方法,返回#name对应的PrivateElements。然后执行 PrivateGet (opens new window) 抽象方法,返回PrivateElements上的[[Value]]字段值。

  代理user导致getName方法内部this指向proxyproxy虽为对象且有[[PrivateElements]]内部槽,但是[[PrivateElements]]值为空列表,获取属性时抽象方法PrivateElementFind会返回empty空值。继续执行抽象方法PrivateGet,空值将抛出错误。

  类似的优化为。

class User {
  #name = 'xxx'

  getName() {
    return this.#name
  }
}
var user = new User()
var proxy = new Proxy(user, {
  get(target, property, receiver) {
    var value = Reflect.get(target, property, receiver)

    if (typeof value === 'function') {
      value = value.bind(target)
    }

    return value
  },
})

proxy.getName() // xxx

# 性能局限

  ;Proxy也存在一些性能问题。

function normal(count) {
  var object = { value: 0 }

  console.time('normal')
  for (var i = 0; i < count; i++) {
    object.value++
  }
  console.timeEnd('normal')
}

function defaultTrap(count) {
  var object = { value: 0 }
  var proxy = new Proxy(object, {})

  console.time('defaultTrap')
  for (var i = 0; i < count; i++) {
    proxy.value++
  }
  console.timeEnd('defaultTrap')
}

function expressionTrap(count) {
  var object = { value: 0 }
  var proxy = new Proxy(object, {
    get(target, property) {
      return target[property]
    },
    set(target, property, value) {
      target[property] = value

      return true
    },
  })

  console.time('expressionTrap')
  for (var i = 0; i < count; i++) {
    proxy.value++
  }
  console.timeEnd('expressionTrap')
}

function reflectTrap(count) {
  var object = { value: 0 }
  var proxy = new Proxy(object, {
    get(target, property, receiver) {
      return Reflect.get(target, property, receiver)
    },
    set(target, property, value, receiver) {
      return Reflect.set(target, property, value, receiver)
    },
  })

  console.time('reflectTrap')
  for (var i = 0; i < count; i++) {
    proxy.value++
  }
  console.timeEnd('reflectTrap')
}

function defineProperty(count) {
  var object = { value: 0 }
  var proxy = {}

  Object.defineProperty(proxy, 'value', {
    get() {
      return object.value
    },
    set(val) {
      object.value = val
    },
  })

  console.time('defineProperty')
  for (var i = 0; i < count; i++) {
    proxy.value++
  }
  console.timeEnd('defineProperty')
}

var count = 1000000

normal(count)
defaultTrap(count)
expressionTrap(count)
reflectTrap(count)
defineProperty(count)

  ;node版本v16.14.0测试结果,其中reflectTrap相较于normal性能差了很多。

normal: 3.681ms
defaultTrap: 386.346ms
expressionTrap: 61.572ms
reflectTrap: 430.464ms
defineProperty: 4.848ms

# Polyfill 原理是什么?

  ;Proxy兼容性 (opens new window) 相对较好,但IE浏览器的任何版本都不支持。

  相关polyfill库内部都是用Object.defineProperty模拟Proxy语法,es6-proxy-polyfill相对较新,逻辑更加清晰,符合函数式编程。

polyfill 兼容 traps 支持度 说明
proxy-polyfill (opens new window) IE9+ getsetapplyconstruct 对象、函数 对象仅代理已存在属性,无法代理新属性
es6-proxy-polyfill (opens new window) IE6+ getsetapplyconstruct 对象、函数和数组 非数组对象,仅代理已存在属性,无法代理新属性。IE8及以下依赖object-defineproperty-ie库支持

  以下将分析es6-proxy-polyfill/src/index.js核心部分。

# 内部类

  构造函数Internal创建内部实例,保存目标对象target和配置对象handler。外部统一调用实例方法来修改target,包括GETSETCALLCONSTRUCT

var PROXY_TARGET = '[[ProxyTarget]]'
var PROXY_HANDLER = '[[ProxyHandler]]'
var GET = '[[Get]]'
var SET = '[[Set]]'
var CALL = '[[Call]]'
var CONSTRUCT = '[[Construct]]'

function Internal(target, handler) {
  this[PROXY_TARGET] = target
  this[PROXY_HANDLER] = handler
}

Internal.prototype[GET] = function () {}
Internal.prototype[SET] = function () {}
Internal.prototype[CALL] = function () {}
Internal.prototype[CONSTRUCT] = function () {}

  以SET修改属性值为例。

Internal.prototype[SET] = function (property, value, receiver) {
  var target = this[PROXY_TARGET], handler = this[PROXY_HANDLER]

  if (handler.set == undefined) {
    target[property] = value
  } else if (typeof handler.set === 'function') {
    var result = handler.set(target, property, value, receiver)

    return Boolean(result)
  }
}

  第一种情况在未指定handler.set时,默认修改目标对象。

var object = { x: 1 }
var handler = {}
var proxy = new Proxy(object, handler)

var internal = new Internal(object, handler)
internal[SET]('x', 2)

object // {x: 2}

  第二种情况若指定了handler.set,则正确传递参数由handler.set来修改目标对象。

var object = { x: 1 }
var handler = {
  set(target, property, value) {
    return Reflect.set(target, property, value * 3)
  },
}
var proxy = new Proxy(object, handler)

var internal = new Internal(object, handler)
internal[SET]('x', 2)

object // {x: 6}

# 观察函数

  ;observeProperty观察对象已存在属性的读写操作,返回属性描述符。

function observeProperty(obj, prop, internal) {
  var desc = getOwnPropertyDescriptor(obj, prop)

  return {
    get: function () { console.log('get') },
    set: function (value) { console.log('set') },
    enumerable: desc.enumerable,
    configurable: desc.configurable,
  }
}

  先不考虑getset内逻辑,修改为log函数。

  ;observeProperties观察对象自身所有属性。

function observeProperties(obj, internal) {
  var names = getOwnPropertyNames(obj), descMap = {}

  for (var i = names.length - 1; i >= 0; --i) {
    descMap[names[i]] = observeProperty(obj, names[i], internal)
  }
  
  return descMap
}

  配合Object.defineProperties,可拦截对象属性的读写操作。

var object = { x: 1, y: 2 }

var descMap = observeProperties(object)
Object.defineProperties(object, descMap)

object.x // get
object.y = 2 // set

  ;observeProto观察对象原型链上所有属性,返回属性描述符。

function observeProto(internal) {
  var descMap = {}, proto = internal[PROXY_TARGET]

  while ((proto = getPrototypeOf(proto))) {
    var props = observeProperties(proto, internal)
    objectAssign(descMap, props)
  }

  return descMap
}

  例如观察数组原型。

var object = [1, 2, 3]
var handler = {}

var internal = new Internal(object, handler)
observeProto(internal)

  返回属性描述符,由数组每一级的原型对象的属性构成。

  暂时不关注observeProto作用,接着往下看。

# 主函数

  代理类型包括数组、对象和函数三种。

function ProxyPolyfill(target, handler) {
  ...
  return createProxy(new Internal(target, handler))
}

function createProxy(internal) {
  var proxy, target = internal[PROXY_TARGET]

  if (typeof target === 'function') {
    proxy = proxyFunction(internal)
  } else if (target instanceof Array) {
    proxy = proxyArray(internal)
  } else {
    proxy = proxyObject(internal)
  }
  return proxy
}

window.Proxy = ProxyPolyfill

  分类型的原因是什么呢?

  返回值proxy类型与目标对象target类型存在一些关联。

  例如代理函数,若支持proxy()函数式调用,proxy类型一定为函数。

var proxy = new Proxy(function () {}, {})

proxy(1, 2)

# 代理对象

  我们来手动编写一个代理对象函数proxyObject

function proxyObject(internal) {
  var descMap, proxy = {}, target = internal[PROXY_TARGET]

  descMap = observeProperties(target, internal)
  Object.defineProperties(proxy, descMap)

  return proxy
}

  观察并拦截了对象自身所有属性。

var object = { x: 1, y: 2 }
var handler = {}

var internal = new Internal(object, handler)
var proxy = proxyObject(internal)

proxy.x // get
proxy.y = 2 // set

  然后考虑补全observeProperty函数内setget

  即修改和读取目标对象的属性,我们可以借助内部实例的SETGET方法。

function observeProperty(obj, prop, internal) {
  var desc = getOwnPropertyDescriptor(obj, prop)

  return {
    get: function () {
      return internal[GET](prop, this)
    },
    set: function (value) {
      internal[SET](prop, value, this)
    },
    ...
  }
}

  特别注意setgetthis实例默认指向调用者,恰好传至内部实例receiver参数。

proxy.x // 1
proxy.y = 3
object // {x: 1, y: 3}

# 代理数组

  ;proxyObject也适用于数组类型。

var object = [1, 2, 3]
var handler = {}

var internal = new Internal(object, handler)
var proxy = proxyObject(internal)

proxy[2] // 3

  唯一差异在于无法代理数组的原型方法。

proxy.join(',') // Uncaught TypeError: proxy.join is not a function

  因为proxy并没有join属性,运行肯定报错。

{
  0: 1,
  1: 2,
  2: 3,
  length: 3,
}

  那就很简单了,proxy添加join属性。

function proxyArray(internal) {
  var descMap, target = internal[PROXY_TARGET]
  var proxy = {
    get join() {
      console.log('get')
    },
    set join(value) { 
      console.log('set')
    },
  }

  descMap = observeProperties(target, internal)
  Object.defineProperties(proxy, descMap)

  return proxy
}

  拦截了join属性。

var object = [1, 2, 3]
var handler = {}

var internal = new Internal(object, handler)
var proxy = proxyArray(internal)

proxy.join // get

  仍然借助内部实例方法,补全getset

var proxy = {
  get join() {
    return internal[GET]('join', this)
  },
  set join(value) {
    internal[GET]('join', value, this)
  },
}

  还存在一个问题,即无法支持别的数组方法,例如popreverse等。

  刚才的observeProto就派上用场了,修改下proxyArray

function proxyArray(internal) {
  var descMap, proxy = {}, target = internal[PROXY_TARGET]

  descMap = observeProto(internal)
  delete descMap.length
  Object.defineProperties(proxy, descMap)

  descMap = observeProperties(target, internal)
  Object.defineProperties(proxy, descMap)

  return proxy
}

  已经支持数组原型上的所有方法了。

var object = [1, 2, 3]
var handler = {}

var internal = new Internal(object, handler)
var proxy = proxyArray(internal)

proxy.pop() // 3
proxy
// {
//   0: 1,
//   1: 2,
//   at: ƒ at(),
//   concat: ƒ concat(),
//   ...
//   hasOwnProperty: ƒ hasOwnProperty(),
//   ...
//   isPrototypeOf: ƒ isPrototypeOf(),
//   ...
//   valueOf: ƒ valueOf(),
//   values: ƒ values(),
// }

  现在来看看es6-proxy-polyfill内部proxyArray,与我们编写的版本大同小异。

function proxyArray(internal) {
  var descMap, newProto, target = internal[PROXY_TARGET]

  descMap = observeProto(internal)
  newProto = objectCreate(getPrototypeOf(target), descMap)

  descMap = observeProperties(target, internal)

  return objectCreate(newProto, descMap)
}

  差异仅在es6-proxy-polyfill未将原型方法作为proxy的属性,个人猜测是为了不破坏proxy自身属性,将原型方法合并为对象放在proxy的原型链头部,并将其指向了目标对象的原型链,形成一条新原型链newProto,如橙色箭头指向。

# 代理函数

# CALL / CONSTRUCT

  以下为内部实例上CALL方法,其中target.apply(thisArg, argList)用于将数组argList转换为参数数列。

Internal.prototype[CALL] = function (thisArg, argList) {
  var target = this[PROXY_TARGET], handler = this[PROXY_HANDLER]

  if (handler.apply == undefined) {
    return target.apply(thisArg, argList)
  }
  if (typeof handler.apply === 'function') {
    return handler.apply(target, thisArg, argList)
  }
}

  类似的在未指定handler.apply时,默认调用目标对象。

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

var internal = new Internal(object, handler)
internal[CALL](null, [1, 2]) // 3

  若指定了handler.apply,则正确传递参数由handler.apply来调用目标对象。

var object = function (x, y) { return x + y }
var handler = {
  apply(target, thisArg, args) {
    return Reflect.apply(target, thisArg, args) * 3
  },
}
var proxy = new Proxy(object, handler)

var internal = new Internal(object, handler)
internal[CALL](null, [1, 2]) // 9

  接着来看下CONSTRUCT方法。

Internal.prototype[CONSTRUCT] = function (argList, newTarget) {
  var newObj, target = this[PROXY_TARGET], handler = this[PROXY_HANDLER]

  if (handler.construct == undefined) {
    newObj = evaluateNew(target, argList)
  } else if (typeof handler.construct === 'function') {
    newObj = handler.construct(target, argList, newTarget)
  }

  return newObj
}

  也是类似的,在未指定handler.construct时,则默认new目标对象。

var object = function (x, y) {
  this.x = x
  this.y = y
}
var handler = {}
var proxy = new Proxy(object, handler)

var internal = new Internal(object, handler)
internal[CONSTRUCT]([1, 2], function F() {}) // object {x: 1, y: 2}

  注意ES6构造函数中数组转参数数列很容易。

new target(...argList)

  但在ES5中较困难,借助eval或者new Function拼接参数才可实现。

function evaluateNew(F, argList) {
  argList = Array.prototype.slice.call(argList)

  var executor = new Function('Ctor', 'return new Ctor(' + argList + ')')

  return executor(F, argList)
}

  若指定了handler.construct,则正确传递参数由handler.construct来调用目标对象。

var object = function (x, y) {
  this.x = x
  this.y = y
}
var handler = {
  construct(target, args, newTarget) {
    return Reflect.construct(target, args, newTarget)
  },
}
var proxy = new Proxy(object, handler)

var internal = new Internal(object, handler)
internal[CONSTRUCT]([1, 2], function F() {}) // F {x: 1, y: 2}
# proxyFunction

  主函数部分提及到,proxy()支持函数式调用,则返回值一定为函数。

function proxyFunction(internal) {
  function P() {
    console.log('function')
  }

  return P
}

  拦截了函数式调用。

var object = function (x, y) { return x + y }
var handler = {}

var internal = new Internal(object, handler)
var proxy = proxyFunction(internal)

proxy(1, 2) // function

  借助内部实例,修改P函数。

function P() {
  return internal[CALL](this, arguments)
}

proxy(1, 2) // 3

  如何知晓外部是否new调用呢?

  判断函数Pthis是否为P函数实例即可。

function P() {
  return this instanceof P
    ? internal[CONSTRUCT](arguments, P)
    : internal[CALL](this, arguments)
}

# 小结

  以上分析了es6-proxy-polyfill核心部分,包括代理对象、数组、函数三种类型,处理方式均大同小异,都是依赖Object.defineProperty或者Object.create拦截属性的修改和读取,注意Object.create第二个参数与Object.defineProperty作用类似,关于Object.create更多细节参考 MDN (opens new window)

  外部读取属性、修改属性或者执行方法,都是借助内部实例原型上的方法。配置对象在未指定对应trap时,默认操作目标对象。若指定了对应trap,则正确传递参数由trap处理目标对象。

  在代理数组时之所以支持数组方法,例如proxy.join(',')。原理是将数组每一级原型的属性取出并合并成一个对象,作为proxy原型链的头部,且将头部指向了目标对象。

# Proxy 与类型转换

  简单代理属性的读取操作。

var object = {}
var handler = {
  get(target, property, receiver) {
    var result = Reflect.get(target, property, receiver)

    console.log(property, result)

    return result
  },
}
var proxy = new Proxy(object, handler)

  显式地将proxy转换为字符串。

proxy.toString()
// toString ƒ toString()
// Symbol(Symbol.toStringTag) undefined

  发现handler.get触发了两次,其中第一次是读取proxy.toString属性,即object.toString属性,沿着原型链查找就是Object.prototype.toString

proxy.toString === Object.prototype.toString

proxy.toString()等价于Object.prototype.toString.call(proxy)

  第二次是如何呢?

  我们可以在ECMA-262规范中第 20.1.3.6 (opens new window) 小节寻找答案。

20.1.3.6 Object.prototype.toString ( )
...
3.Let O be ! ToObject(this value).
...
13.Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
14.Else, let builtinTag be "Object".
15.Let tag be ? Get(O, @@toStringTag).
16.If Type(tag) is not String, set tag to builtinTag.
17.Return the string-concatenation of "[object ", tag, and "]".

  ;toString步骤大致为。

  • this转换为对象O,并判断O是否有特殊的内部槽,若没有builtinTag默认为Object
  • 读取对象O[Symbol.toStringTag]属性值为tag,若tag不是字符串,则tag赋为builtinTag
  • 返回由"[object "tag"]"拼接而成的字符串

  忽略内部槽判断,手动编写toString

Object.prototype.toString = function () {
  var O = Object(this)
  var builtinTag = 'Object'
  var tag = O[Symbol.toStringTag]

  if (typeof tag !== 'string') {
    tag = builtinTag
  }

  return `[object ${tag}]`
}

内部槽是引擎层面的东西,JavaScript代码无法实现

  故在proxy.toString()运行时,toString内部读取了proxy[Symbol.toStringTag]属性,代理导致第二次触发handler.get方法。

  再来看看proxy隐式转换。

proxy + 1
// Symbol(Symbol.toPrimitive) undefined
// valueOf ƒ valueOf()
// toString ƒ toString()
// Symbol(Symbol.toStringTag) undefined

  为什么触发了四次handler.get呢?

  参考 Symbol.toPrimitive 我们知道,形如x + y的表达式,将分别对xy执行ToPrimitive(input, preferredType)抽象运算,转化为原始值。

  ;ToPrimitive(proxy, default)简略过程为。

  • 判断是否有proxy[Symbol.toPrimitive]方法,返回undefined表示没有
  • 执行proxy.valueOf()方法,返回proxy对象
  • 执行proxy.toString()方法,返回[object Object](原始值)
  • 拼接两原始值返回[object Object]1

# 小结

  • 构造函数Proxy没有prototype原型属性,无法继承
  • 为了内部方法的统一性,不变量也会限制trap的返回结果
  • 内置对象或者私有属性严格依赖this实例的内部槽,代理可能出错
  • Proxy也有局限性,例如无法代理=====等操作
  • Proxy存在性能问题,即使没有任何trap

# 参考

# 🎉 写在最后

🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star (opens new window) ✨支持一下哦!

手动码字,如有错误,欢迎在评论区指正💬~

你的支持就是我更新的最大动力💪~

GitHub (opens new window) / Gitee (opens new window)GitHub Pages (opens new window)掘金 (opens new window)CSDN (opens new window) 同步更新,欢迎关注😉~

最后更新时间: 11/27/2022, 9:20:44 PM