【vue设计与实现】非原始值的响应式方案 2-JavaScript对象及Proxy的工作原理

在JavaScript中有两种对象,其中一种叫做常规对象(ordinary object),另一种叫做异质对象(exotic object)这两种对象包含了JavaScript世界中的所有对象。要了解什么是常规对象,什么是异质对象,这需要我们先了解对象的内部方法和内部槽。
在JavaScript中,函数其实也是对象。假设给出一个对象obj,如何区分它是普通对象还是函数?实际上,在JavaScript中,对象的实际语义是由对象的内部方法(internal method)指定的。所谓内部方法,指的是当我们对一个对象进行操作时在引擎内部调用的方法,这些方法对于JavaScript使用者来说是不可见的。例如,当我们访问对象属性时:

obj.foo

引擎内部会调用[[Get]]这个内部方法来读取属性值。这里补充说明下,在ECMAScript规范中使用[[xxx]]来代表内部方法或内部槽。当然一个对象中不仅[[Get]]这个内部方法,看下面所有必要的内部方法:

内部方法签名描述
[[GetPrototypeof]]()→object │ Null查明为该对象提供继承属性的对象,null代表没有继承属性
[[SetPrototypeof]](Object│ Null)→Boolean将该对象与提供继承属性的另一个对象相关联。传递nuul表示没有继承属性,返回true表示操作成功完成,返回false表示操作失败
[[ISExtensible]]()→Boolean查明是否允许向该对象添加其他属性
[PreventExtensions]]()→Boolean控制能否向该对象添加新属性。如果操作成功则返回true,如果操作失败则返回false
[[GetOwnProperty]](propertyKey)→Undefined │PropertyDescriptor返回该对象自身属性的描述符,其键为propertyKey,如果不存在这样的属性,则返回undefined
[[DefineOwnProperty]](propertyKey,PropertyDescriptor)→Boolean创建或更改自己的属性,其键为propertyKey,以具有由 PropertyDescriptor描述的状态。如果该属性已成功创建或更新,则返回true;如果无法创建或更新该属性,则返回false
[[HasProperty]](propertyKey)→Boolean返回一个布尔值,指示该对象是否已经拥有键为propertyKey的自己的或继承的属性
[[Get]](propertyKey,Receiver)→any从该对象返回键为propertyKey 的属性的值。如果必须运行 ECMAScript代码来检索属性值,则在运行代码时使用Receiver作为this值
[[Set]](propertyKey,value,Receiver)→Boolean将键值为propertyKey 的属性的值设置为value如果必须运行ECMAScript代码来设置属性值,则在运行代码时使用Receiver作为this值。如果成功设置了属性值,则返回true;如果无法设置,则返回false
[[Delete]](propertyKey)→Boolean从该对象中刷除属于自身的键为propertyKey的属性。如果该属性未被删除并且仍然存在,则返回false;如果该属性已被删除或不存在,则返回true
[[OwnPropertyKeys]]()→List of propertyKey返回一个List,其元素都是对象自身的属性键

除此之外还有两个额外的必要内部方法:[[Call]]和[[Construct]]

内部方法签名描述
[[Call]](any, a List of any) -> any将运行的代码与this对象关联。由函数调用触发。该内部方法的参数是一个this值和参数列表
[[Construct]](a List of any,object)→Object创建一个对象。通过new运算符或 super调用触发。该内部方法的第一个参数是一个List,该List的元素是构造函数调用或super调用的参数,第二个参数是最初应用new运算符的对象。实现该内部方法的对象称为构造函数

如果一个对象需要作为函数调用,那么这个对象就必须部署内部方法[[Call]],所以可以通过内部方法和内部槽来区分对象,例如函数对象会部署内部方法[[Call]],而普通对象则不会
内部方法具有多态性,不同类型的对象可能部署了相同的内部方法,却具有不同的逻辑。例如普通对象和Proxy对象都部署了[[Get]]这个内部方法,但它们的逻辑是不同的。
了解了内部方法,就可以解释什么是常规对象,什么是异质对象。满足下面三点就是常规对象:

  1. 对于必要的内部方法(第一个图标列出的内部方法),必须使用ECMA规范10.1x节给出的定义实现
  2. 对于内部方法[[Call]],必须使用ECMA规范10.2.1节给出的定义实现
  3. 对于内部方法[[Construct]],必须使用ECMA规范10.2.2节给出的定义实现

所有不符合这三点要求的对象都是异质对象,而Proxy就是一个异质对象。

下面来分析Proxy对象,在通过代理对象访问属性值时:

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

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

下表示Proxy对象部署的所有内部方法

内部方法处理器函数
[[GetPrototypeof]]getPrototypeof
[[SetPrototypeof]]setPrototypeof
[[IsExtensible]]isExtensible
[[PreventExtensions]]preventExtensions
[[GetOwnProperty]]getOwnPropertyDescriptor
[[DefineOwnProperty]]defineProperty
[[HasProperty]]has
[[Get]]get
[[Set]]set
[[Delete]]deleteProperty
[[OwnPropertyKeys]]ownKeys
[[Call]]apply
[[Construct]]construct

所以当我们要拦截删除属性操作时,可以使用deleteProperty拦截函数实现

const obj = {foo:1}
const p = new Proxy(obj,{
	deleteProperty(target,key){
		return Reflect.deleteProperty(target,key)
	}
})

console.log(p.foo)	//1
delete p.foo
console.log(p.foo)

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值