使用 get 陷阱函数进行对象外形验证

JS 语言有趣但有时却令人困惑的特性之一,就是读取对象不存在的属性时并不会抛出错误,而会把 undefined 当作该属性的值,例如:

let target = {};

console.log(target.name);       // undefined

在多数语言中,试图读取 target.name 属性都会抛出错误,因为该属性并不存在; 但 JS 语言却会使用 undefined。如果你曾经在大型代码库上进行过工作,那么你可能明白这种行为会导致严重的问题,尤其是当属性名称存在书写错误时。使用代理进行对象外形验证,可以帮你从这个错误中拯救出来。

对象外形(Object Shape)指的是对象已有的属性与方法的集合,JS 引擎使用对象外形来进行代码优化,经常会创建一些类来表示对象。如果你能大胆假设某个对象总是拥有与起始时相同的属性与方法(可以通过 Object.preventExtensions() 方法、Object.seal() 方法或 Object.freeze() 方法来达到这种效果),那么在访问不存在的属性时抛出错误在这种场合就会非常有用。代理能够让对象外形验证变得轻而易举。

由于该属性验证只须在读取属性时被触发,因此只要使用 get 陷阱函数。该陷阱函数会在读取属性时被调用,即使该属性在对象中并不存在,它能接受三个参数:

  1. trapTarget:将会被读取属性的对象(即代理的目标对象);

  2. key:需要读取的属性的键(字符串类型或符号类型);

  3. receiver:操作发生的对象(通常是代理对象)。

这些参数借鉴了 set 陷阱函数的参数,只有一个明显的不同,也就是没有使用 value 参数,因为 get 陷阱函数并不需要为属性写入数据。Reflect.get() 方法同样接收这三个参数,并且默认会返回属性的值。

你可以使用 get 陷阱函数与 Reflect.get() 方法在目标属性不存在时抛出错误,就像这样:

let proxy = new Proxy({}, {
        get(trapTarget, key, receiver) {
            if (!(key in receiver)) {
                throw new TypeError("Property " + key + " doesn't exist.");
            }

            return Reflect.get(trapTarget, key, receiver);
        }
    });

// adding a property still works
proxy.name = "proxy";
console.log(proxy.name);            // "proxy"

// nonexistent properties throw an error
console.log(proxy.nme);             // throws error

在本例中,get 陷阱函数拦截了属性读取操作,它使用 in 运算符来判断 receiver 对象上是否已存在对应属性。receiver 并没有使用 trapTarget,而是用了 in ,这是因为 receiver 本身就是拥有一个 has 陷阱函数的代理对象(has 陷阱函数会在下一节介绍),在此处使用 trapTarget 会跳过 has 陷阱函数,并可能给你一个错误的结果。如果要查找的属性不存在,那么就会抛出错误;否则会执行默认的行为。

这段代码允许添加新的属性(例如 proxy.name)以供写入,并且此后可以正常读取该属性的值。最后一行代码有一个拼写错误:proxy.nme 应当是 proxy.name, 由于 nme 属性并不存在,程序抛出了一个错误。