非原始值响应式方案(二)

JavaScript对象和proxy的工作原理

我们经常听到这样一句话:“JavaScript中一切皆对象。”那么,到底什么是对象呢?根据ECMAScript规范,在JavaScript中有两种对象,其中一种叫作常规对象,另外一种叫做异质对象。这两种对象包含了JavaScript中所有的对象,任何不属于常规对象的都是异质对象。那么到底什么是常规对象,什么是异质对象呢?

满足以下3点的都是常规对象:

  • 对于表5-1列出的内部方法,必须使用ECMA规范10.1.x节给出的定义实现;
  • 对于内部方法[[Call]],必须使用ECMA规范10.2.1节给出的定义实现;
  • 对于内部方法[[Construct]],必须使用ECMA规范10.2.2给出的定义实现。

而不符合这3点要求的对象都是异质对象。例如Proxy对象的内部方法[[Get]]没有使用ECMA规范的10.1.8节给出的定义实现,所以Proxy是一个异质对象。

接下来,我们就具体看看Proxy对象。既然Proxy也是对象,那么它本身也部署了上述必要的内部方法,当我们通过代理对象访问属性值时:

1
2
const p = new Proxy(obj, { /** ... */ })
p.foo

实际上,引擎会调用部署在对象p上的内部方法[[Get]]。到这一步,其实代理对象和普通对象没有什么区别。他们的区别在于内部方法的[[Get]]的实现,这里就体现了内部方法的多态性,即不同的对象部署相同的内部方法,但是它们的行为可能不同,具体的不同表现在,如果在创建代理对象的时候没有指定对应的拦截函数,例如没有指定get()拦截函数,那么当我们通过代理对象访问属性值时,代理对象的内部方法[[Get]]会调用原始对象的内部方法[[Get]]来获取属性值,这其实就是代理透明性质。

现在明白了,创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身(这里指的是对象P)的内部方法和行为的,而不是用来指定被代理对象(这里指的是对象obj)的内部方法和行为的。下面这张表格列出了Proxy对象部署的所有内部方法以及用来自定义内部方法和行为的拦截函数名字。

内部方法 处理器函数
[[GetPropertyOf]] getPropertyOf
[[SetPropertyOf]] setPropertyOf
[[IsExtensible]] isExtensible
[[PrevenExtensions]] prevenExtensions
[[GetOwnProperty]] getOwnPropertyDescriptor
[[DefineOwnProperty]] defineProperty
[[HasProperty]] has
[[Get]] get
[[Set]] set
[[Delete]] deleteProperty
[[OwnPropertyKeys]] ownKeys
[[Call]] apply
[[Construct]] construct
其中,[[Call]][[Construct]]这两个内部方法只有当被代理对象是函数和构造函数时才会部署。

由上表可知,当我们要拦截删除属性时,可以使用deleteProperty拦截函数实现:

1
2
3
4
5
6
7
8
9
const obj = { foo: 1 }
const p = new Proxy(obj, {
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key)
}
})
console.log(obj.foo) // 1
delete p.foo
console.log(p.foo) // 未定义

这里要强调的是,deleteProperty实现的是代理对象p的内部方法和行为,所以为了删除被代理对象上的属性值,我们需要使用Reflect.deleteProperty(target, key)来完成。