5.2 JavaScript 对象及 Proxy 的工作原理
一个操作能不能被代理,取决于它对应哪个内部方法;Proxy 是异质对象,它的内部方法由拦截器自定义。
一、常规对象与异质对象
按 ECMAScript 规范,JavaScript 中的对象分为两类,二者覆盖了所有对象:
- 常规对象(ordinary object);
- 异质对象(exotic object):任何不属于常规对象的对象。
一个对象是常规对象,必须同时满足以下三点:
- 部署 11 个必要的内部方法,且必须使用规范 10.1.x 节给出的定义;
- 内部方法 [[Call]] 使用规范 10.2.1 节的定义实现;
- 内部方法 [[Construct]] 使用规范 10.2.2 节的定义实现。
11 个必要的内部方法
| 内部方法 | 作用 | 返回值要求 |
|---|---|---|
[[GetPrototypeOf]] | 获取对象的原型 | 对象或 null |
[[SetPrototypeOf]] | 设置对象的原型 | 布尔值 |
[[IsExtensible]] | 判断对象是否可扩展 | 布尔值 |
[[PreventExtensions]] | 阻止对象扩展 | 布尔值 |
[[GetOwnProperty]] | 获取对象的自身属性 | 属性描述符对象或 undefined |
[[DefineOwnProperty]] | 定义对象的自身属性 | 布尔值 |
[[HasProperty]] | 判断对象是否包含某个属性 | 布尔值 |
[[Get]] | 获取对象的属性值 | 任意类型 |
[[Set]] | 设置对象的属性值 | 布尔值 |
[[Delete]] | 删除对象的属性 | 布尔值 |
[[OwnPropertyKeys]] | 获取对象的属性列表 | 数组 |
一个对象能否作为函数调用,就是靠内部方法区分的:函数对象部署
[[Call]],普通对象不部署。
二、内部方法的多态性
与面向对象中的多态类似:不同类型的对象可能部署相同的内部方法,但逻辑不同。
例如普通对象与 Proxy 对象都部署 [[Get]],但行为不同:
- Proxy 对象的
[[Get]]没有使用规范 10.1.8 节的定义实现,因此 Proxy 是异质对象; - 若创建代理对象时没有指定对应的拦截函数(如没写
get()),代理对象的[[Get]]会调用原始对象的[[Get]]来取值 —— 这就是代理的透明性质。
关键理解:创建代理对象时指定的拦截函数,是用来自定义代理对象本身的内部方法和行为的,而不是去修改被代理对象的内部方法和行为。
[[Call]] 与 [[Construct]] 只有在被代理对象是函数 / 构造函数时才会部署,所以它们对应的拦截器(apply / construct)也只在代理函数时才有意义。
三、示例:拦截 delete 操作
删除属性对应内部方法 [[Delete]],其拦截器是 deleteProperty:
const obj = { foo: 1 }
const p = new Proxy(obj, {
deleteProperty(target, key) {
// deleteProperty 实现的是代理对象 p 的内部方法,
// 要删除被代理对象上的属性,必须用 Reflect.deleteProperty
return Reflect.deleteProperty(target, key)
}
})
console.log(p.foo) // 1
delete p.foo
console.log(p.foo) // undefined
console.log(obj.foo) // undefined(原始对象上的属性同样被删除)
相关
- 5.1 理解 Proxy 和 Reflect:Proxy 拦截基本操作、Reflect 完成默认行为
- 5.3 如何代理 Object:由内部方法推出
get/has/ownKeys/deleteProperty的代理方式