在 ECMAScript 5 之前,JavaScript 环境中具有不可枚举和不可写的对象属性,但是开发者不能定义不可枚举或不可写的属性。ECMAScript 5 添加的 Object.defineProperty() 方法允许开发者定义不可枚举和不可写的对象属性。

ECMAScript 6 给予了开发者先前仅内建对象具有的访问 JavaScript 引擎的能力。语言通过代理(proxies)暴露了对象的内部工作方式(inner workings),代理是包装器,可以拦截和改变 JavaScript 引擎的底层(low-level)操作。本章以详细描述代理可以解决的问题作为开头,然后讨论如何有效地创建和使用代理。

数组问题

ECMAScript 6 之前,开发者不能自定义对象模仿 JavaScript 数组对象的行为。当给数组指定项赋值时,会影响数组的 length 属性,也可以通过修改 length 属性从而改变数组。例如:

let colors = ["red", "green", "blue"];

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black"

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"

colors 数组开始有 3 个元素。赋值 "black"colors[3] 会自动递增 length 属性,此时属性值为 4。设置 length 属性值为 2 会移除数组中最后面的两个元素,数组的前两个元素不变。在 ECMAScript 5 中,开发者不能实现这种行为,但是代理可以。

在 ECMAScript 6 中,这种非标准的行为是数组作为 exotic 对象的原因。

代理和反射是什么

调用 new Proxy() 可以创建一个代理,然后使用它取代另一个对象(称为目标对象)。代理虚拟化了(virtualizes)目标对象,以至于使用了代理后,代理和目标对象看起来是同一个对象。

代理允许拦截目标对象的底层操作,这些操作通常由 JavaScript 引擎内部处理。一般使用 trap 拦截这些底层操作。trap 是响应特定操作的函数。

Reflect 对象表示的反射(reflection)API 是一组方法,提供了和代理可以重写的底层操作相同的操作的默认行为。每个代理 trap 有一个 Reflect 方法。这些方法和对应的 trap 具有相同的名字,接收的参数也相同。

可以重写的操作 代理 Trap 默认行为
读属性值 get Reflect.get()
写属性 set Reflect.set()
in 操作符 has Reflect.has()
delete 操作符 deleteProperty Reflect.deleteProperty()
Object.getPrototypeOf() getPrototypeOf Reflect.getPrototypeOf()
Object.setPrototypeOf() setPrototypeOf Reflect.setPrototypeOf()
Object.isExtensible() isExtensible Reflect.isExtensible()
Object.preventExtensions() preventExtensions Reflect.preventExtensions()
Object.getOwnPropertyDescriptor() getOwnPropertyDescriptor Reflect.getOwnPropertyDescriptor()
Object.defineProperty() defineProperty Reflect.defineProperty
Object.keys, Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() ownKeys Reflect.ownKeys()
调用函数 apply Reflect.apply()
使用 new 调用函数 construct Reflect.construct()

每个 trap 重写了某个 JavaScript 对象的内建行为,允许拦截和修改这个行为。如果仍然需要使用内建行为,可以使用对应的反射 API 方法。

创建简单代理

使用 Proxy 构造器创建代理时,需要传入两个参数:目标对象和处理器(handler)。处理器是定义一或多个 trap 的对象。除了 trap 重写的操作之外,代理会对所有操作使用默认行为。创建简单的转发(forwarding)代理,使用不含 trap 的处理器:

let target = {};

let proxy = new Proxy(target, {});

proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

target.name = "target";
console.log(proxy.name);        // "target"
console.log(target.name);       // "target"

在这个例子中,proxy 直接转发所有操作给 target。将 "proxy" 赋值给 name 属性时,target 上会创建 name 属性。代理自身没有这个属性,它只是简单地转发操作给 target。类似地,proxy.nametarget.name 的值相同,因为两者都引用 target.name。这也意味着设置 target.name 的值会让 proxy.name 的值作出相同的改变。

使用 set trap 验证属性

假设想要创建一个对象,这个对象的属性值必须是数值。这意味着每次向这个对象添加新的属性,需要验证属性值,如果值不是数值则必须抛出错误。为了完成这个操作,可以定义 set trap 重写设置一个值的默认行为。 set trap 接收4 个参数:

  1. trapTarget 接收属性的对象(代理的目标对象)
  2. key 将要设置的属性的键(string 或 symbol 类型)
  3. value 写入属性的值
  4. receiver 这个对象上执行操作(通常是代理)

Reflect.set()set trap 对应的反射方法,是这个操作的默认行为。Reflect.set() 方法接收和 set trap 相同的 4 个参数,使得方法在 trap 中易于使用。当属性设置成功,trap 应该返回 true,否则返回 false。(Reflect.set() 方法基于操作是否成功返回对应的值。)

为了验证属性值,使用 set trap,检查传入的 value 参数。例子如下:

let target = {
    name: "target"
};

let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {

        // 忽略对象已有的属性,这些属性不受影响
        if (!trapTarget.hasOwnProperty(key)) {
            if (isNaN(value)) {
                throw new TypeError("Property must be a number.");
            }
        }

        // 设置属性
        return Reflect.set(trapTarget, key, value, receiver);
    }
});

// 添加新属性
proxy.count = 1;
console.log(proxy.count);       // 1
console.log(target.count);      // 1

// 可以向 name 属性赋值,因为它是目标对象的已有属性
proxy.name = "proxy";
console.log(proxy.name);        // "proxy"
console.log(target.name);       // "proxy"

// 抛出错误
proxy.anotherName = "proxy";

这段代码定义了一个 set trap 验证添加到 target 对象上的新属性的值。执行 proxy.count = 1 时,调用 set trap。 trapTarget 值等于 targetkey 值是 “count",value 值是 1, receiver(这个例子中没有使用) 值是 proxytarget 中没有名为 count 的属性,代理传入 valueisNaN() 进行验证。如果结果是 NaN,则属性值不是数值,抛出错误。由于 count 的值是 1,代理传入和 set trap 相同的 4 个参数调用 Reflect.set() 方法,添加新属性。

当向 proxy.name 赋值一个字符串时,操作成功完成。由于 target 已经有 name 属性,在验证过程中调用的trapTarget.hasOwnProperty() 方法忽略该属性。这确保了仍然支持先前已存在的非数值属性值。

然而,向 proxy.anotherName 赋值字符串时,抛出错误。目标对象中不存在anotherName 属性,所以需要验证它的值。在验证过程中,因为 "proxy" 不是数值,抛出错误。

当写入属性时,可以使用代理的 set trap 拦截这个操作。当读属性时,可以使用 get trap 拦截这个操作。

使用 get Trap 验证 Object Shape

在 JavaScript 中,读一个不存在的属性不会抛出错误。此时,将得到一个 undefined 值。如下所示:

let target = {};

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

在大多数语言中,因为 target.name 属性不存在,读取这个属性会抛出错误。但是 JavaScript 会得到 undefined 值。

对象 shape 是对象可用的一组属性和方法。JavaScript 引擎使用对象 shape 优化代码,通常创建类表示对象。如果可以安全地假定一个对象在它创建之后,它的属性和方法不会改变(使用 Object.preventExtensions()Object.seal()Object.freeze() 方法可以实现这个行为),则访问一个不存在的属性时抛出错误是有帮助的。代理使得对象 shape 的验证容易。

仅当读取属性时,属性验证发生,可以使用 get trap。读取属性时,get trap 调用,即使对象没有对应的属性。它接收 3 个参数:

  1. trapTarget 具有读取的属性的对象(代理的目标对象)。
  2. key 读取的属性的键(string 或 symbol)。
  3. receiver 操作发生的对象(通常是代理)。

这些属性和 set trap 的参数类似,也有不同之处。因为 get trap 不向属性写入值,没有 value 参数。 Reflect.get() 方法接收和 get trap 相同的 3 个参数,返回属性的默认值。

当目标对象上不存在读取的属性时,可以使用 get trap 和 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);
        }
    });

// 添加属性
proxy.name = "proxy";
console.log(proxy.name);            // "proxy"

// 读取不存在的属性抛出错误
console.log(proxy.nme);             // throws error

这个例子中,get trap 拦截了属性的读操作。in 操作符确定 receiver 中是否存在指定属性。in 操作符和 receiver 而不是 trapTarget 一起使用,因为此时 receiver 具有 has trap。在这种情况下使用 trapTarget 会绕过 has trap,得到错误的结果。如果属性不存在,抛出错误;否则,发生默认行为。

这段代码允许添加、写入和读取像 proxy.name 这样的新属性。最后一行因为 nme 这个属性不存在,抛出错误。

使用 has Trap 隐藏存在的属性

in 操作符确定指定对象是否有指定属性,如果自有属性或原型链中属性和指定属性名匹配(string 或 symbol),返回 true。例如:

let target = {
    value: 42;
}

console.log("value" in target);     // true
console.log("toString" in target);  // true

valuetoString 在对象中存在,所以两者使用 in 操作符返回 true。value 属性是自有属性,toString 属性是原型链中的属性(继承自 Object)。代理允许使用 has trap 拦截 in 操作,在 in 操作中返回不同的值。

无论什么时候 in 操作符执行时,has trap 被调用。调用时会传入两个参数:

  1. trapTarget 读取的属性所在的对象(代理的目标对象)。
  2. key 检查的属性的键(string 或 symbol)。

Reflect.has() 方法接收相同的参数,返回默认的 in 操作符的响应。使用 has trap 和 Reflect.has() 方法可以改变 in 操作符对部分属性的行为,其他属性保持默认行为。例如,假设想要隐藏 value 属性,可以这样做:

let target = {
    name: "target",
    value: 42
};

let proxy = new Proxy(target, {
    has(trapTarget, key) {

        if (key === "value") {
            return false;
        } else {
            return Reflect.has(trapTarget, key);
        }
    }
});


console.log("value" in proxy);      // false
console.log("name" in proxy);       // true
console.log("toString" in proxy);   // true

代理的 has trap 检查如果键为 "value",返回 false。否则,调用 Reflect.has() 方法执行默认行为。因此,即使 value 属性实际上在目标对象中存在,in 操作符也会返回 false。name 和 toString 使用 in 操作符时返回 true。

deleteProperty Trap 阻止删除属性

delete 操作符从对象上移除指定属性,如果移除成功,返回 true,否则返回 false。在严格模式下,移除一个不可配置属性会抛出错误;在非严格模式下,移除一个不可配置属性返回 false。例子如下:

let target = {
    name: "target",
    value: 42
};

Object.defineProperty(target, "name", { configurable: false });

console.log("value" in target);     // true

let result1 = delete target.value;
console.log(result1);               // true

console.log("value" in target);     // false

// 下述代码在严格模式下抛出错误
let result2 = delete target.name;
console.log(result2);               // false

console.log("name" in target);      // true

使用 delete 操作符删除了 value 属性,因此,在第三次调用 console.log() 时,in 操作符返回 false。不可配置属性 name 不能删除,所以 delete 操作符返回 false(如果在严格模式下,会抛出错误)。在代理中使用 deleteProperty trap 可以改变这个行为。

对对象的属性使用 delete 操作符时,deleteProperty trap 被调用。这个 trap 接收 2 个参数:

  1. trapTarget 删除的属性所在的对象(代理的目标对象)。
  2. key 删除的属性的键(string 或 symbol)。

Reflect.deleteProperty() 方法提供了 deleteProperty trap 的默认实现,接收相同的参数。组合使用 Reflect.deleteProperty() 方法和 deleteProperty trap 可以改变 delete 操作符的行为。例如,可以保证不删除 value 属性:

let target = {
    name: "target",
    value: 42
};

let proxy = new Proxy(target, {
    deleteProperty(trapTarget, key) {

        if (key === "value") {
            return false;
        } else {
            return Reflect.deleteProperty(trapTarget, key);
        }
    }
});

// 尝试删除 proxy.value

console.log("value" in proxy);      // true

let result1 = delete proxy.value;
console.log(result1);               // false

console.log("value" in proxy);      // true

// 尝试删除 proxy.name

console.log("name" in proxy);       // true

let result2 = delete proxy.name;
console.log(result2);               // true

console.log("name" in proxy);       // false

这段代码非常类似于 has trap 的例子,deleteProperty trap 检查属性的键是否为 "value",如果是则返回 false;否则,调用 Reflect.deleteProperty() 方法执行默认行为。value 属性不能通过 proxy 删除,因为操作经过 trap,trap 中不会对 value 属性执行删除操作,但 name 属性会被删除。当想要在严格模式下,保证删除属性时不会抛出错误,可以使用这个方法。

代理作为原型时的 Trap

ECMAScript 6 添加的 Object.setPrototypeOf() 方法作为 Object.getPrototypeOf() 方法的补充。代理允许通过 setPrototypeOfgetPrototypeOf trap 拦截这两个方法的执行。在这两种情况下,Object 的方法会在代理上调用对应名称的 trap,允许改变方法的行为。

由于有两个 trap 和原型代理关联,每种类型的 trap 都有一组方法与之关联。setPrototypeOf trap 接收的参数如下:

  1. trapTarget 设置的原型所在的对象(代理的目标对象)。
  2. proto 作为原型使用的对象。

Object.setPrototypeOf()Reflect.setPrototypeOf() 方法接收相同的参数。getPrototypeOf trap 仅接收 trapTarget 参数,Object.setPrototypeOf()Reflect.setPrototypeOf() 方法也有这个参数。

这些 trap 有一些限制。首先,getPrototypeOf trap 必须返回一个对象或 null,返回其他值会导致运行时错误。返回值检查确保 Object.getPrototypeOf() 总是返回所期望的值。类似的,如果操作不成功,setPrototypeOf trap 的返回值必须是 false。当 setPrototypeOf 返回 false 时,Object.setPrototypeOf() 抛出错误。如果 setPrototypeOf 返回除了 false 之外的其他值,Object.setPrototypeOf() 确保操作成功。

下述例子总是返回 null 隐藏代理的原型,不允许改变原型:

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return null;
    },
    setPrototypeOf(trapTarget, proto) {
        return false;
    }
});

let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);

console.log(targetProto === Object.prototype);      // true
console.log(proxyProto === Object.prototype);       // false
console.log(proxyProto);                            // null

// 成功
Object.setPrototypeOf(target, {});

// 抛出错误
Object.setPrototypeOf(proxy, {});

这段代码强调了 target 和 proxy 的不同行为。Object.getPrototypeOf() 方法传入 target 调用时,返回一个值;传入 proxy 调用时,返回 null,因为此时 getPrototypeOf trap 被调用。类似的,Object.setPrototypeOf() 方法传入 target 调用,成功;传入 proxy 调用则抛出错误,由于 setPrototypeOf trap。

如果想要使用这两个 trap 的默认行为,可以使用 Reflect 对象的对应方法。例如,这段代码实现了 getPrototypeOfsetPrototypeOf trap 的默认行为:

let target = {};
let proxy = new Proxy(target, {
    getPrototypeOf(trapTarget) {
        return Reflect.getPrototypeOf(trapTarget);
    },
    setPrototypeOf(trapTarget, proto) {
        return Reflect.setPrototypeOf(trapTarget, proto);
    }
});

let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);

console.log(targetProto === Object.prototype);      // true
console.log(proxyProto === Object.prototype);       // true

// 成功
Object.setPrototypeOf(target, {});

// 成功
Object.setPrototypeOf(proxy, {});

这个例子中,可以相互交替地使用 target 和 proxy,得到相同的结果。因为 getPrototypeOfsetPrototypeOf trap 直接使用了默认实现。重要的是,由于某些重要的不同之处,本例中使用了 Reflect.getPrototypeOf()Reflect.setPrototypeOf() 方法而不是 Object 对象的同名方法。

Reflect.getPrototypeOf()Reflect.setPrototypeOf() 方法看起来类似于 Object.getPrototypeOf()Object.setPrototypeOf() 方法。这两对方法执行类似地操作,有一些不同之处。

Object.getPrototypeOf()Object.setPrototypeOf() 方法是较高层次(higher-level)操作,提供给开发者使用。Reflect.getPrototypeOf()Reflect.setPrototypeOf() 方法是低层次(lower-level)操作,使得开发者可以访问先前仅内部可用的 [[GetPrototypeOf]] 和 [[SetPrototypeOf]] 操作。Reflect.getPrototypeOf() 方法是内部的 [[GetPrototypeOf]] 操作(和一些输入验证)的包装器。Reflect.setPrototypeOf() 方法和 [[SetPrototypeOf]] 操作之间的关系和前述类似。Object 上对应的方法也调用了 [[GetPrototypeOf]] 和 [[SetPrototypeOf]] 操作,同时在调用之前执行了某些操作,检查返回值确定行为。

传入 Reflect.getPrototypeOf() 方法的参数不是对象时,抛出错误。Object.getPrototypeOf() 方法首先将参数强制转换为对象,然后执行操作。如果向这两个方法传入数值,会得到不同的结果:

let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype);  // true

// 抛出错误
Reflect.getPrototypeOf(1);

Object.getPrototypeOf() 方法从数字 1 上检索原型,因为它首先将值转换为 Number 对象,然后返回 Number.prototypeReflect.getPrototypeOf() 方法不会将参数转换为对象,由于 1 不是对象,它抛出错误。

Reflect.setPrototypeOf() 方法和 Object.setPrototypeOf() 方法之间也有些许不同。Reflect.setPrototypeOf() 方法返回布尔值表示操作是否成功,true 表示成功,false 表示失败。如果 Object.setPrototypeOf() 方法操作失败,抛出错误。

setPrototypeOf trap 返回 false 会造成 Object.setPrototypeOf() 方法抛出错误。Object.setPrototypeOf() 方法会将它的第一个参数作为返回值返回,不适合实现代理 setPrototypeOf trap 的默认行为。下述代码演示了两者的不同:

let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1);                   // true

let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2);                   // false
console.log(result2);                               // true

本例中, Object.setPrototypeOf()target1 作为返回值返回,Reflect.setPrototypeOf() 返回 true。这微妙的不同很重要。Object 和 Reflect 之间有许多同名的方法,总是确保在代理 trap 中使用 Reflect 的方法。

对象扩展性 Trap

ECMAScript 5 添加了 Object.preventExtensions()Object.isExtensible() 方法用于修改对象的可扩展性,ECMAScript 6 允许代理通过 preventExtensionsisExtensible trap 拦截这些方法。这两个方法接收一个名为 trapTarget 的参数,表示调用方法的对象。 isExtensible trap 必须返回布尔值表示对象是否可扩展。preventExtensions trap 必须返回布尔值表示操作是否成功。

Reflect.preventExtensions()Reflect.isExtensible() 实现了对应操作的默认行为。两者都返回布尔值,所以可以直接在对应的 trap 中使用。

考虑下面的代码,观察对象可扩展性 trap 的行为,实现了 isExtensible and preventExtensions trap 的默认行为:

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return Reflect.preventExtensions(trapTarget);
    }
});


console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target));       // false
console.log(Object.isExtensible(proxy));        // false

这个例子展示了 Object.preventExtensions()Object.isExtensible() 接收 proxy 和 target 的行为。当然,也可以改变这个行为。例如,如果不想 Object.preventExtensions() 方法对 proxy 生效,可以在 preventExtensions trap 中返回 false:

let target = {};
let proxy = new Proxy(target, {
    isExtensible(trapTarget) {
        return Reflect.isExtensible(trapTarget);
    },
    preventExtensions(trapTarget) {
        return false
    }
});


console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

Object.preventExtensions(proxy);

console.log(Object.isExtensible(target));       // true
console.log(Object.isExtensible(proxy));        // true

此时,对 Object.preventExtensions(proxy) 的调用被忽略,因为 preventExtensions trap 返回 false。操作并没有转发给 target,所以 Object.isExtensible() 返回 true。

Object.isExtensible() 方法和 Reflect.isExtensible() 方法相似,除了当接收一个非对象参数时的表现不同。当接收非对象参数时,Object.isExtensible() 返回 false,Reflect.isExtensible() 抛出错误。例子如下:

let result1 = Object.isExtensible(2);
console.log(result1);                       // false

// 抛出错误
let result2 = Reflect.isExtensible(2);

底层方法比对应的较高层方法有更严格的错误检查。

Object.preventExtensions() 方法总是返回传入它的参数,即使这个参数不是对象。Reflect.preventExtensions() 方法则当参数不是对象时抛出错误,参数是对象时,如果执行成功返回 true,否则返回 false。例如:

let result1 = Object.preventExtensions(2);
console.log(result1);                               // 2

let target = {};
let result2 = Reflect.preventExtensions(target);
console.log(result2);                               // true

// 抛出错误
let result3 = Reflect.preventExtensions(2);

属性描述符 Trap

Object.defineProperty() 方法用于定义对象属性的性质。Object.getOwnPropertyDescriptor() 方法则可以查询属性的性质。

使用 definePropertygetOwnPropertyDescriptor trap 可以拦截对 Object.defineProperty()Object.getOwnPropertyDescriptor() 方法的调用。defineProperty trap 接收如下参数:

  1. trapTarget 定义的属性所位于的对象(代理的目标对象)。
  2. key 属性的键,string 或 symbol。
  3. descriptor 属性的描述符(descriptor)对象。

当操作成功时, defineProperty trap 返回 true,否则返回 false。getOwnPropertyDescriptor trap 接收参数 trapTargetkey,返回描述符。 Reflect.defineProperty()Reflect.getOwnPropertyDescriptor() 方法接收和对应的 trap 完全相同的参数。下述例子仅实现了每个 trap 的默认行为:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        return Reflect.defineProperty(trapTarget, key, descriptor);
    },
    getOwnPropertyDescriptor(trapTarget, key) {
        return Reflect.getOwnPropertyDescriptor(trapTarget, key);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy"
});

console.log(proxy.name);            // "proxy"

let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

console.log(descriptor.value);      // "proxy"

defineProperty trap 返回 true 时,Object.defineProperty() 执行成功;返回 false 时,Object.defineProperty() 抛出错误。可以使用这个功能限制 Object.defineProperty() 可以定义的属性的类型。例如,如果不允许定义 symbol 属性,可以检查属性的键类型是否为 string,是则返回 true,否则返回 false:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {

        if (typeof key === "symbol") {
            return false;
        }

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy"
});

console.log(proxy.name);                    // "proxy"

let nameSymbol = Symbol("name");

// 抛出错误
Object.defineProperty(proxy, nameSymbol, {
    value: "proxy"
});

也可以让 Object.defineProperty() 静默地失败,此时返回 true 但并不调用 Reflect.defineProperty() 方法。这种方式会抑制错误,实际上不会定义属性。

为确保使用 Object.defineProperty()Object.getOwnPropertyDescriptor() 方法行为的一致性,传递给 defineProperty trap 的描述符对象需要正则化。基于同样的原因,从 getOwnPropertyDescriptor trap 返回的对象总是已验证的。

无论传递什么样的对象作为 Object.defineProperty() 的第三个参数,这个对象的 enumerable, configurable, value, writable, get, 和 set 属性有效,会传递给 defineProperty trap。例如:

let proxy = new Proxy({}, {
    defineProperty(trapTarget, key, descriptor) {
        console.log(descriptor.value);              // "proxy"
        console.log(descriptor.name);               // undefined

        return Reflect.defineProperty(trapTarget, key, descriptor);
    }
});


Object.defineProperty(proxy, "name", {
    value: "proxy",
    name: "custom"
});

调用 defineProperty trap 时,descriptor 对象不是引用了传给 Object.defineProperty() 方法的第 3 个参数,而是一个仅包含允许的属性的新对象。 Reflect.defineProperty() 方法也会忽略描述符对象中的非标准属性。

getOwnPropertyDescriptor trap 的返回值必须是 null、undefined 或对象。如果返回的是对象,则这个对象的自有属性必须是: enumerable, configurable, value, writable, getset。如果返回的对象的自有属性具有上述的这些属性之外的属性,抛出错误,正如下述代码所示:

let proxy = new Proxy({}, {
    getOwnPropertyDescriptor(trapTarget, key) {
        return {
            name: "proxy"
        };
    }
});

// 抛出错误
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

Object.defineProperty()Reflect.defineProperty() 除了返回值不同,其他作用相同。 Object.defineProperty() 返回传入给它的第一个参数,Reflect.defineProperty() 返回布尔值表示操作是否成功。例如:

let target = {};

let result1 = Object.defineProperty(target, "name", { value: "target "});

console.log(target === result1);        // true

let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });

console.log(result2);                   // true

由于 defineProperty trap 返回一个布尔值,必要的时候,最好使用Reflect.defineProperty() 实现默认行为。

传入 Object.getOwnPropertyDescriptor() 方法的第一个参数如果是原始值(primitive value),则会强制将其转换成对象,然后再执行操作。当传入 Reflect.getOwnPropertyDescriptor() 方法的第一个参数是原始值时,抛出错误。例子如下:

let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1);       // undefined

// 抛出错误
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");

因为将 2 强制转换成对象后,没有名为 name 的属性,所以输出 descriptor1 的结果为 undefined。

ownKeys Trap

ownKeys trap 拦截内部的 [[OwnPropertyKeys]] 方法,可以重写返回由属性名构成的数组这一行为。有 4 个方法会用到这个数组:Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign()Object.assign() 使用这个数组来确定复制哪些属性)方法。

Reflect.ownKeys() 方法实现了 ownKeys trap 的默认行为,返回所有自有属性的键名构成的数组,包括 string 和 symbol 类型的键。 Object.getOwnProperyNames()Object.keys() 方法返回自有属性中仅由 string 类型的键名构成的数组,Object.getOwnPropertySymbols() 方法返回自有属性中仅由 symbol 类型的键名构成的数组。 Object.assign() 方法返回所有自有属性的键名构成的数组,包括 string 和 symbol 类型的键。

ownKeys trap 接收一个参数,这个参数是目标对象,必须返回一个数组或类数组对象,否则抛出错误。例如,使用 ownKeys trap 可以在使用 Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign() 方法时过滤掉不想要的指定属性的键。假设不想包含以下划线开头的属性,通常这些属性是私有属性,可以使用 ownKeys trap 过滤这些属性。如下所示:

let proxy = new Proxy({}, {
    ownKeys(trapTarget) {
        return Reflect.ownKeys(trapTarget).filter(key => {
            return typeof key !== "string" || key[0] !== "_";
        });
    }
});

let nameSymbol = Symbol("name");

proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";

let names = Object.getOwnPropertyNames(proxy),
    keys = Object.keys(proxy),
    symbols = Object.getOwnPropertySymbols(proxy);

console.log(names.length);      // 1
console.log(names[0]);          // "name"

console.log(keys.length);      // 1
console.log(keys[0]);          // "name"

console.log(symbols.length);    // 1
console.log(symbols[0]);        // "Symbol(name)"

ownKeys trap 也会影响到 for-in 循环语句。这个 trap 决定哪些键会在循环内部出现。

函数代理中的 apply and construct Trap

在所有的 trap 中,只有 applyconstruct trap 需要代理的目标对象是函数。函数有两个内部方法 [[Call]] 和 [[Construct]]。当不使用 new 操作符调用函数时,执行 [[Call]] 方法;使用 new 操作符调用函数时,执行 [[Construct]] 方法。applyconstruct trap 分别对应这两种调用方式,可以重写对应的内部方法。当没有使用 new 操作符调用函数时,apply trap 和 Reflect.apply() 接收的参数如下:

  1. trapTarget 执行的函数(代理的目标对象)。
  2. thisArg 在调用过程中,函数内部的 this 表示的值。
  3. argumentsList 传递给函数的参数构成的数组。

使用 new 操作符调用函数时,construct trap被调用,接收的参数如下:

  1. trapTarget 执行的函数(代理的目标对象)。
  2. argumentsList 传递给函数的参数构成的数组。

Reflect.construct() 方法的前 2 个参数与之相同,有可选的第三个参数 newTarget,表示函数中 new.target 的值。

applyconstruct trap 完全控制了代理的目标函数的行为。模仿函数的默认行为,可以这样做:

let target = function() { return 42 },
    proxy = new Proxy(target, {
        apply: function(trapTarget, thisArg, argumentList) {
            return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList);
        }
    });

// 目标对象是函数的代理对象看起来是一个函数
console.log(typeof proxy);                  // "function"

console.log(proxy());                       // 42

var instance = new proxy();
console.log(instance instanceof proxy);     // true
console.log(instance instanceof target);    // true

instanceof 使用原型链判断类型,原型链查找不受代理的影响。

applyconstruct trap 可以改变函数执行的方式。假设想要验证所有参数是否为指定类型,可以在 apply trap 中检查:

// 相加所有参数
function sum(...values) {
    return values.reduce((previous, current) => previous + current, 0);
}

let sumProxy = new Proxy(sum, {
        apply: function(trapTarget, thisArg, argumentList) {

            argumentList.forEach((arg) => {
                if (typeof arg !== "number") {
                    throw new TypeError("All arguments must be numbers.");
                }
            });

            return Reflect.apply(trapTarget, thisArg, argumentList);
        },
        construct: function(trapTarget, argumentList) {
            throw new TypeError("This function can't be called with new.");
        }
    });

console.log(sumProxy(1, 2, 3, 4));          // 10

// 抛出错误
console.log(sumProxy(1, "2", 3, 4));

// 也会抛出错误
let result = new sumProxy();

也可以让一个函数必须使用 new 操作符调用,验证它的参数必须是数值:

function Numbers(...values) {
    this.values = values;
}

let NumbersProxy = new Proxy(Numbers, {

        apply: function(trapTarget, thisArg, argumentList) {
            throw new TypeError("This function must be called with new.");
        },

        construct: function(trapTarget, argumentList) {
            argumentList.forEach((arg) => {
                if (typeof arg !== "number") {
                    throw new TypeError("All arguments must be numbers.");
                }
            });

            return Reflect.construct(trapTarget, argumentList);
        }
    });

let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

// 抛出错误
NumbersProxy(1, 2, 3, 4);

不使用代理,使用 new.target 也可以完成这个操作。

new.target 表示使用 new 操作符调用的函数,通过检查这个值可以判断函数是否使用了 new 操作符调用:

function Numbers(...values) {

    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }

    this.values = values;
}

let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

// 抛出错误
Numbers(1, 2, 3, 4);

假设 Numbers() 函数定义在无法修改的代码中,已知代码中使用了 new.target,如果想要在调用函数时不执行 new.target 相关的语句,可以使用 apply trap:

function Numbers(...values) {

    if (typeof new.target === "undefined") {
        throw new TypeError("This function must be called with new.");
    }

    this.values = values;
}


let NumbersProxy = new Proxy(Numbers, {
        apply: function(trapTarget, thisArg, argumentsList) {
            return Reflect.construct(trapTarget, argumentsList);
        }
    });


let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

NumbersProxy() 函数允许调用 Numbers() 函数时不使用 new 操作符。此时的行为相当于使用了 new 操作符调用。因为 apply trap 中调用了 Reflect.construct() 方法,这个方法是函数使用 new 操作符调用时的默认行为。此时 Numbers() 中的 new.target 等于函数自身。

可以向 Reflect.construct() 方法传入第 3 个参数,表示 new.target 的值。当在函数中需要检查 new.target 的值时可以使用这种方式。在抽象基类构造器中,new.target 的值必须不等于类构造器自身,如下所示:

class AbstractNumbers {

    constructor(...values) {
        if (new.target === AbstractNumbers) {
            throw new TypeError("This function must be inherited from.");
        }

        this.values = values;
    }
}

class Numbers extends AbstractNumbers {}

let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values);           // [1,2,3,4]

// 抛出错误
new AbstractNumbers(1, 2, 3, 4);

调用 new AbstractNumbers() 时,new.target 等于 AbstractNumbers,调用 new Numbers() 时,new.target 等于 Numbers。使用代理手动赋值给 new.target 可以绕过这个限制:

class AbstractNumbers {

    constructor(...values) {
        if (new.target === AbstractNumbers) {
            throw new TypeError("This function must be inherited from.");
        }

        this.values = values;
    }
}

let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
        construct: function(trapTarget, argumentList) {
            return Reflect.construct(trapTarget, argumentList, function() {});
        }
    });


let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values);               // [1,2,3,4]

上述代码中,new.target 的值是一个空函数。

类构造器必须使用 new 操作符调用,因为类构造器的内部方法 [[Call]] 执行时会抛出错误。代理可以拦截对 [[Call]] 方法的调用,使用代理可以创建一个可回调的类构造器。例如,如果不想使用 new 操作符调用类构造器,可以使用 apply trap 创建新实例。例子如下:

class Person {
    constructor(name) {
        this.name = name;
    }
}

let PersonProxy = new Proxy(Person, {
        apply: function(trapTarget, thisArg, argumentList) {
            return new trapTarget(...argumentList);
        }
    });


let me = PersonProxy("Nicholas");
console.log(me.name);                   // "Nicholas"
console.log(me instanceof Person);      // true
console.log(me instanceof PersonProxy); // true

上述代码中,不使用 new 操作符调用 PersonProxy() 时,会返回一个目标对象的新实例。此时如果不使用 new 操作符调用 Person(),仍然会抛出错误。

可撤销代理

一般情况下,代理创建之后不能和它的目标对象解绑。当代理不再需要的时候,需要撤销。比如,有一个对象遍及整个 API,为了安全性和可维护性,使用某些功能时不允许访问这个对象。此时可以撤销代理。

使用 Proxy.revocable() 方法可以创建一个可撤销代理,它接收和 Proxy() 构造器相同的参数:一个目标对象和一个处理器。作为返回值的对象有如下属性:

  1. proxy 可撤销的代理对象。
  2. revoke 调用这个函数可以撤销代理。

调用 revoke() 函数后,不能通过 proxy 执行任何操作。此时使用 proxy 时触发的任何 trap 都会抛出错误。例如:

let target = {
    name: "target"
};

let { proxy, revoke } = Proxy.revocable(target, {});

console.log(proxy.name);        // "target"

revoke();

// 抛出错误
console.log(proxy.name);

解决数组问题

使用代理和反射 API 可以创建一个对象,使得它的行为和内建的 Array 对象添加和移除属性时的行为相同。代理可以模仿的行为如下:

let colors = ["red", "green", "blue"];

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black"

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"

本例中有两个特别重要的行为需要注意:

  1. colors[3] 赋值时,length 属性递增到 4。
  2. 设置 length 属性为 2 时,数组的最后两项被删除。

整数作为属性的键是数组特有的。ECMAScript 6 标准给定了判断属性的键是否为数组索引的方式:

一个字符串属性名 P 是数组索引当且仅当 ToString(ToUint32(T)) 等于 PToUint32(P) 不等于 \(2^{32} - 1\)

这个操作的实现方式如下:

// 将值转换为无符号 32 位整数
function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

前述的两个数组行为依赖于属性赋值,意味着仅需要使用 set trap。下述例子实现了使用的数组索引大于 length - 1 时,递增 length 属性:

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

function createMyArray(length=0) {
    return new Proxy({ length }, {
        set(trapTarget, key, value) {

            let currentLength = Reflect.get(trapTarget, "length");

            // 设置 length
            if (isArrayIndex(key)) {
                let numericKey = Number(key);

                if (numericKey >= currentLength) {
                    Reflect.set(trapTarget, "length", numericKey + 1);
                }
            }

            // 设置元素
            return Reflect.set(trapTarget, key, value);
        }
    });
}

let colors = createMyArray(3);
console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";

console.log(colors.length);         // 3

colors[3] = "black";

console.log(colors.length);         // 4
console.log(colors[3]);             // "black"

当设置 length 属性的值小于先前的值时,移除数组中的元素。这不仅涉及到改变 length 属性的值,也要删除数组中先前存在的元素。例如,如果数组的 length 值为 4,然后将值修改为 2,则索引 2 和 3 所在的数组元素需要删除。这个操作可以在 set trap 中完成。将先前的例子修改后如下所示:

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

function createMyArray(length=0) {
    return new Proxy({ length }, {
        set(trapTarget, key, value) {

            let currentLength = Reflect.get(trapTarget, "length");

            // 判断使用的索引和 length 之间的关系
            if (isArrayIndex(key)) {
                let numericKey = Number(key);

                if (numericKey >= currentLength) {
                    Reflect.set(trapTarget, "length", numericKey + 1);
                }
            // 设置 length 时判断其值
            } else if (key === "length") {

                if (value < currentLength) {
                    for (let index = currentLength - 1; index >= value; index--) {
                        Reflect.deleteProperty(trapTarget, index);
                    }
                }

            }

            // 修改元素
            return Reflect.set(trapTarget, key, value);
        }
    });
}

let colors = createMyArray(3);
console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length);         // 4

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
console.log(colors[0]);             // "red"

实现了上述两个行为,可以很容易地创建一个模仿内建数组行为的对象。将这些行为封装成一个类更好。

创建一个使用代理的类的最简单的方式是定义这个类,从构造器中返回代理。使用这种方式,当实例化类时,返回的对象是代理而不是这个类的实例(实例是构造器中 this 的值)。此时实例是代理的目标对象,返回的是代理。实例完全私有,不能直接访问它,可以通过代理间接访问实例。

从类的构造器中返回类的代理的例子如下:

class Thing {
    constructor() {
        return new Proxy(this, {});
    }
}

let myThing = new Thing();
console.log(myThing instanceof Thing);      // true

创建一个数组类相对直接。此时,在类构造器中使用代理代码。完整的例子如下:

function toUint32(value) {
    return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}

function isArrayIndex(key) {
    let numericKey = toUint32(key);
    return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}

class MyArray {
    constructor(length=0) {
        this.length = length;

        return new Proxy(this, {
            set(trapTarget, key, value) {

                let currentLength = Reflect.get(trapTarget, "length");

                // 判断属性键是否是索引
                if (isArrayIndex(key)) {
                    let numericKey = Number(key);

                    if (numericKey >= currentLength) {
                        Reflect.set(trapTarget, "length", numericKey + 1);
                    }
                } else if (key === "length") {

                    if (value < currentLength) {
                        for (let index = currentLength - 1; index >= value; index--) {
                            Reflect.deleteProperty(trapTarget, index);
                        }
                    }

                }

                // 设置元素值
                return Reflect.set(trapTarget, key, value);
            }
        });

    }
}


let colors = new MyArray(3);
console.log(colors instanceof MyArray);     // true

console.log(colors.length);         // 3

colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";

console.log(colors.length);         // 4

colors.length = 2;

console.log(colors.length);         // 2
console.log(colors[3]);             // undefined
console.log(colors[2]);             // undefined
console.log(colors[1]);             // "green"
console.log(colors[0]);             // "red"

虽然从类的构造器中返回代理很容易,但这也意味着对于每个实例都会创建一个新的代理对象。也可以让所有实例共享同一个代理:将代理作为原型。

使用代理作为原型

将代理作为原型时,仅当默认操作涉及到原型执行时,trap 才会被调用。这限制了作为原型的代理的能力。考虑如下例子:

let target = {};
let newTarget = Object.create(new Proxy(target, {

    // 从不调用
    defineProperty(trapTarget, name, descriptor) {

        // 调用时造成错误
        return false;
    }
}));

Object.defineProperty(newTarget, "name", {
    value: "newTarget"
});

console.log(newTarget.name);                    // "newTarget"
console.log(newTarget.hasOwnProperty("name"));  // true

创建 newTarget 对象时,将代理作为原型。代理的目标对象 target 实际上是 newTarget 的原型,因为代理是透明的。仅当 newTarget 上的操作传递给 target 执行时,trap 会被调用。

Object.defineProperty() 方法在 newTarget 上创建了自有属性 name。在一个对象上定义属性不涉及这个对象的原型,所以 defineProperty trap 不会被调用。

代理作为原型时有严格的限制,少数几个 trap 仍然有用。

调用内部方法 [[Get]] 读取属性时,首先在对象的自有属性中查找指定属性,如果没有,在对象的原型中查找,如果还没有,则沿着原型链查找,直到找到了或所有的原型都查找过了。

创建了 get trap 后,无论什么时候读取一个给定属性,这个属性在自有属性中不存在时,trap 会在原型上调用。访问的属性不能保证在对象中存在时,可以使用 get trap 阻止不想发生的行为。如下创建一个对象,无论什么时候访问这个对象中不存在的属性时,抛出错误:

let target = {};
let thing = Object.create(new Proxy(target, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
}));

thing.name = "thing";

console.log(thing.name);        // "thing"

// 抛出错误
let unknown = thing.unknown;

重要的是需要理解 trapTargetreceiver 是不同的对象。将代理作为原型使用时,trapTarget 是原型对象自身,receiver 是实例对象。在这个例子中,trapTargettarget 对象,receiverthing 对象。

内部方法 [[Set]] 也会检查自有属性和原型(如果必要的话)。给一个对象属性赋值时,如果这个属性是对象的自有属性,则修改这个属性的值;如果自有属性中没有这个属性,会在原型中查找该属性。即使会在原型中查找,但默认情况下,仍然会在对象上创建这个属性,然后赋值。创建的属性是对象的自有属性。原型中是否有这个属性不重要。

下面的例子显示了默认行为:

let target = {};
let thing = Object.create(new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        return Reflect.set(trapTarget, key, value, receiver);
    }
}));

console.log(thing.hasOwnProperty("name"));      // false

// 调用了 set trap
thing.name = "thing";

console.log(thing.name);                        // "thing"
console.log(thing.hasOwnProperty("name"));      // true

// 没调用 set trap
thing.name = "boo";

console.log(thing.name);                        // "boo"

thing.name 赋值 "thing" 时,set trap 被调用,因为 thing 没有名为 name 的自有属性。在 set trap 中,trapTarget 等于 targetreceiver 等于 thing。最终会在 thing 上创建一个名为 name 的自有属性。Reflect.set() 实现了这个默认行为。

has trap 拦截了 in 操作符的操作。当给定一个属性名时,in 操作符首先在对象的自有属性中查找,如果自有属性中不存在,则去原型中查找,如果依然不存在,则沿着原型链查找,直到找到了或在所有原型上都找过了。

仅当查找操作在原型上执行时,has trap 才会被调用。将代理作为原型时,当给定属性名在自有属性中不存在时这个行为才会发生。例如:

let target = {};
let thing = Object.create(new Proxy(target, {
    has(trapTarget, key) {
        return Reflect.has(trapTarget, key);
    }
}));

// 触发 has trap
console.log("name" in thing);                   // false

thing.name = "thing";

// 不触发 has trap
console.log("name" in thing);                   // true

类不能直接修改原型使得它的原型是代理,因为它的 prototype 属性不可写入。通过继承创建一个类,使得它的原型是代理。先使用构造器函数创建一个类型,然后重写原型,将原型修改为代理。例子如下:

function NoSuchProperty() {
    // empty
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

let thing = new NoSuchProperty();

// 抛出错误,get trap 中执行
let result = thing.name;

NoSuchProperty 函数作为基类。函数的 prototype 属性没有限制,所以可以使用代理作为原型。

下一步是创建一个类继承 NoSuchProperty。如下所示:

function NoSuchProperty() {
    // empty
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

let shape = new Square(2, 6);

let area1 = shape.length * shape.width;
console.log(area1);                         // 12

// 抛出错误,因为 wdth 属性不存在
let area2 = shape.length * shape.wdth;

这段代码中,作为原型的代理位于 Square 类的原型链中。代理不是 shape 的原型。如下所示:

function NoSuchProperty() {
    // empty
}

// 引用作为原型的代理
let proxy = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

NoSuchProperty.prototype = proxy;

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }
}

let shape = new Square(2, 6);

let shapeProto = Object.getPrototypeOf(shape);

console.log(shapeProto === proxy);                  // false

let secondLevelProto = Object.getPrototypeOf(shapeProto);

console.log(secondLevelProto === proxy);            // true

shape 的原型是 Square.prototypeSquare.prototype 的原型是代理,继承自 NoSuchProperty

只有当查找操作到达原型时,才会调用 get trap。如果查找在 Square.prototype 上完成时,get trap 不会调用。如下所示:

function NoSuchProperty() {
    // empty
}

NoSuchProperty.prototype = new Proxy({}, {
    get(trapTarget, key, receiver) {
        throw new ReferenceError(`${key} doesn't exist`);
    }
});

class Square extends NoSuchProperty {
    constructor(length, width) {
        super();
        this.length = length;
        this.width = width;
    }

    getArea() {
        return this.length * this.width;
    }
}

let shape = new Square(2, 6);

let area1 = shape.length * shape.width;
console.log(area1);                         // 12

let area2 = shape.getArea();
console.log(area2);                         // 12

// 抛出错误,wdth 属性不存在
let area3 = shape.length * shape.wdth;

此时, getArea() 方法自动成为 Square.prototype 的属性。

参考

[1] Zakas, Understanding ECMAScript 6, 2017.

 posted on 2024-06-21 15:37  x-yun  阅读(12)  评论(0编辑  收藏  举报