代理(Proxy)和反射(Reflection)

Posted 小火柴的蓝色理想

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代理(Proxy)和反射(Reflection)相关的知识,希望对你有一定的参考价值。

前面的话

  ES5和ES6致力于为开发者提供JS已有却不可调用的功能。例如在ES5出现以前,JS环境中的对象包含许多不可枚举和不可写的属性,但开发者不能定义自己的不可枚举或不可写属性,于是ES5引入了Object.defineProperty()方法来支持开发者去做JS引擎早就可以实现的事情。ES6添加了一些内建对象,赋予开发者更多访问JS引擎的能力。代理(Proxy)是一种可以拦截并改变底层JS引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。本文将详细介绍代理(Proxy)和反射(Reflection)

 

引入

【数组问题】

  在ES6之前,开发者不能通过自己定义的对象模仿JS数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的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个元素,将colors[3]赋值为"black"时,length属性会自动增加到4,将length属性设置为2时,会移除数组的后两个元素而只保留前两个。在ES5之前开发者无法自己实现这些行为,现在通过代理可以实现

 

代理和反射

  调用new Proxy()可创建代替其他目标(target)对象的代理,它虚拟化了目标,所以二者看起来功能一致

  代理可以拦截JS引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数

  反射API以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。下表总结了代理陷阱的特性

  每个陷阱覆写JS对象的一些内建特性,可以用它们拦截并修改这些特性。如果仍需使用内建特性,则可以使用相应的反射API方法

【创建简单代理】

  用Proxy构造函数创建代理需要传入两个参数:目标(target)和处理程序(handler)。处理程序用于定义一个或多个陷阱的对象,在代理中,除了专门为操作定义的陷阱外,其余操作均使用默认特性。不使用任何陷阱的处理程序等价于简单的转发代理

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"赋值给proxy.name属性时会在目标上创建name,代理只是简单地将操作转发给目标,它不会储存这个属性。由于proxy.name和target.name引用的都是target.name,因此二者的值相同,从而为target.name设置新值后,proxy.name也一同变化

 

陷阱代理

【使用set陷阱验证属性】

  假设创建一个属性值是数字的对象,对象中每新增一个属性都要加以验证,如果不是数字必须抛出错误。为了实现这个任务,可以定义一个set陷阱来覆写设置值的默认特性

  set陷阱接受4个参数

trapTaqget 用于接收属性(代理的目标)的对象
key 要写入的属性键(字符串或Symbol类型)
value 被写入属性的值
receiver 操作发生的对象(通常是代理)

  Reflect.set()是set陷阱对应的反射方法和默认特性,它和set代理陷阱一样也接受相同的4个参数,以方便在陷阱中使用。如果属性已设置陷阱应该返回true,如果未设置则返回false。(Reflect.set()方法基于操作是否成功来返回恰当的值)

  可以使用set陷阱并检查传入的值来验证属性值

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";

  这段代码定义了一个代理来验证添加到target的新属性,当执行proxy.count=1时,set陷阱被调用,此时trapTarget的值等于target,key等于"count",value等于1,receiver等于proxy

  由于target上没有count属性,因此代理继续将value值传入isNaN(),如果结果是NaN,则证明传入的属性值不是数字,同时也抛出一个错误。在这段代码中,count被设置为1,所以代理调用Reflect.set()方法并传入陷阱接受的4个参数来添加新属性

  proxy.name可以成功被赋值为一个字符串,这是因为target已经拥有一个name属性,但通过调用trapTarget.hasownproperty()方法验证检查后被排除了,所以目标已有的非数字属性仍然可以被操作。

  然而,将proxy.anotherName赋值为一个字符串时会抛出错误。目标上没有anotherName属性,所以它的值需要被验证,而由于"Proxy"不是一个数字值,因此抛出错误

  set代理陷阱可以拦截写入属性的操作,get代理陷阱可以拦截读取属性的操作

【用get陷阱验证对象结构(Object Shape)】

  JS有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取属性的值

let target = {};
console.log(target.name); // undefined

  在大多数其他语言中,如果target没有name属性,尝试读取target.name会抛出一个错误。但JS却用undefined来代替target.name属性的值。这个特性会导致重大问题,特别是当错误输入属性名称的时候,而代理可以通过检查对象结构来回避这个问题

  对象结构是指对象中所有可用属性和方法的集合,JS引擎通过对象结构来优化代码,通常会创建类来表示对象,如果可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误。代理让对象结构检验变得简单

  因为只有当读取属性时才会检验属性,所以无论对象中是否存在某个属性,都可以通过get陷阱来检测,它接受3个参数

trapTarget 被读取属性的源对象(代理的目标)
key 要读取的属性键(字符串或Symbol)
receiver 操作发生的对象(通常是代理)

  由于get陷阱不写入值,所以它复刻了set陷阱中除value外的其他3个参数,Reflect.get()也接受同样3个参数并返回属性的默认值

  如果属性在目标上不存在,则使用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);
    }
});
// 添加属性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 读取不存在属性会抛出错误
console.log(proxy.nme); // 抛出错误

  此示例中的get陷阱可以拦截属性读取操作,并通过in操作符来判断receiver上是否具有被读取的属性,这里之所以用in操作符检查receiver而不检查trapTarget,是为了防止receiver代理含有has陷阱。在这种情况下检查trapTarget可能会忽略掉has陷阱,从而得到错误结果。属性如果不存在会抛出一个错误,否则就使用默认行为

  这段代码展示了如何在没有错误的情况下给proxy添加新属性name,并写入值和读取值。最后一行包含一个输入错误:proxy.nme有可能是proxy.namer,由于nme是一个不存在的属性,因而抛出错误

【使用has陷阱隐藏已有属性】

  可用in操作符来检测给定对象是否含有某个属性,如果自有属性或原型属性匹配这个名称或Symbol返回true

let target = {
    value: 42;
}
console.log("value" in target); // true
console.log("toString" in target); // true

  value是一个自有属性,tostring是一个继承自Object的原型属性,二者在对象上都存在,所以用in操作符检测二者都返回true。在代理中使用has陷阱可以拦截这些in操作并返回一个不同的值

  每当使用in操作符时都会调用has陷阱,并传入两个参数

trapTaqget读取属性的对象(代理的目标)
key要检查的属性键(字符串或Symbol)

  Reflect.has()方法也接受这些参数并返回in操作符的默认响应,同时使用has陷阱和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陷阱会检查key是否为"value",如果是的话返回false,若不是则调用Reflect.has()方法返回默认行为。结果是即使target上实际存在value属性,但用in操作符检查还是会返回false,而对于name和tostring则正确返回true

【用deleteProperty陷阱防止删除属性】

  delete操作符可以从对象中移除属性,如果成功则返回true,不成功则返回false。在严格模式下,如果尝试删除一个不可配置(nonconfigurable)属性则会导致程序抛出错误,而在非严格模式下只是返回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陷阱来改变这个行为

  每当通过delete操作符删除对象属性时,deleteProperty陷阱都会被调用,它接受两个参数

trapTarget 要删除属性的对象(代理的目标)
key 要删除的属性键(字符串或Symbol)

  Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,并且接受同样的两个参数。结合二者可以改变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陷阱的示例非常相似,deleteProperty陷阱检查key是否为"value",如果是的话返回false,否则调用Reflect.deleteProperty()方法来使用默认行为。由于通过代理的操作被捕获,因此value属性无法被删除,但name属性就如期被删除了。如果希望保护属性不被删除,而且在严格模式下不抛出错误,那么这个方法非常使用

【原型代理陷阱】

  Object.setPrototypeOf()方法被用于作为ES5中的Object.getPrototypeOf()方法的补充。通过代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以拦截这两个方法的执行过程,在这两种情况下,Object上的方法会调用代理中的同名陷阱来改变方法的行为

  两个陷阱均与代理有关,但具体到方法只与每个陷阱的类型有关,setPrototypeOf陷阱接受以下这些参数

trapTarget 接受原型设置的对象(代理的目标)
proto 作为原型使用的对象

  传入Object.setPrototypeOf()方法和Reflect.setPrototypeOf()方法的均是以上两个参数,另一方面,getPrototypeOf陷阱中的Object.getPrototypeOf()方法和Reflect.getPrototypeOf()方法只接受参数trapTarget

原型代理陷阱的运行机制

  原型代理陷阱有一些限制。首先,getPrototypeOf陷阱必须返回对象或null,否则将导致运行时错误,返回值检查可以确保Object.getPrototypeOf()返回的总是预期的值;其次,在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返回值时,由于getPrototypeOf陷阱被调用,返回的是null;同样,Object.setPrototypeOf()成功为target设置原型,而给proxy设置原型时,由于setPrototypeOf陷阱被调用,最终抛出一个错误

  如果使用这两个陷阱的默认行为,则可以使用Reflect上的相应方法。例如,下面的代码实现了getPrototypeOf和setPrototypeOf陷阱的默认行为

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, {});

  由于本示例中的getPrototypeOf陷阱和setPrototypeOf陷阱仅使用了默认行为,因此可以交换使用target和paro×y并得到相同结果。由于Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法与Object上的同名方法存在一些重要差异,因此使用它们是很重要的

为什么有两组方法

  令人困惑的是,Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法疑似Object.getPrototypeOf()方法和Object.setPrototypeOf()方法,尽管两组方法执行相似的操作,但两者间仍有一些不同之处

  Object.getPrototypeOf()和Object.setPrototypeOf()是给开发者使用的高级操作;而Reflect.getPrototypeOf()方法和Reflect.setprototypeOf()方法则是底层操作,其赋予开发者可以访问之前只在内部操作的[[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.prototype;而由于Reflect.getPrototypeOf()方法不强制转化值的类型,而且1又不是一个对象,故会抛出一个错误

  Reflect.setPrototypeOf()方法与Object.setPrototypeOf()方法也不尽相同。具体而言,Reflect.setPrototypeOf()方法返回一个布尔值来表示操作是否成功,成功时返回true,失败则返回false;而Object.setPrototypeOf()方法一旦失败则会抛出一个错误

  当setPrototypeOf代理陷阱返回false时会导致Object.setPrototypeOf()抛出一个错误。Object.setPrototypeOf()方法返回第一个参数作为它的值,因此其不适合用于实现setPrototypeOf代理陷阱的默认行为

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上还有更多看似重复的方法,但是在所有代理陷阱中一定要使用Reflect上的方法

【对象可扩展性陷阱】

  ES5已经通过Object.preventExtensions()方法和Object.isExtensible()方法修正了对象的可扩展性,ES6可以通过代理中的preventExtensions和isExtensible陷阱拦截这两个方法并调用底层对象。两个陷阱都接受唯一参数trapTarget对象,并调用它上面的方法。isExtensible陷阱返回的一定是一个布尔值,表示对象是否可扩展;preventExtensions陷阱返回的也一定是布尔值,表示操作是否成功

  Reflect.preventExtensions()方法和 Reflect.IsExtensible()方法实现相应陷阱中默认行为,二者都返回布尔值

两个基础示例

  以下这段代码是对象可扩展性陷阱的实际应用,实现了isExtensible和preventExtensions陷阱的默认行为

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陷阱中返回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陷阱返回了false,所以操作不会转发到底层目标,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.getPrototypeOf()方法与Reflect.getPrototypeOf()方法之间的差异,因为相比高级功能方法而言,底层的具有更严格的错误检査

  Object.preventExtensions()方法和Reflect.preventExtensions()方法同样非常相似。无论传入Object.preventExtensions()方法的参数是否为一个对象,它总是返回该参数;而如果Reflect.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);

  在这里,即使值2不是一个对象,Object.preventExtensions()方法也将其透传作为返回值,而Reflect.preventExtensions()方法则会抛出错误,只有当传入对象时它才返回true

【属性描述符陷阱】

  ES5最重要的特性之一是可以使用Object.defineProperty()方法定义属性特性(property attribute)。在早期版本的JS中无法定义访问器属性,无法将属性设置为只读或不可配置。直到Object.defineProperty()方法出现之后才支持这些功能,并且可以通过Object.getOwnPropertyDescriptor()方法来获取这些属性

  在代理中可以分别用defineProperty陷阱和getOwnPropertyDescriptor陷阱拦截 Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法的调用。definePropepty陷阱接受以下参数

trapTarget 要定义属性的对象(代理的目标)
key 属性的键(字符串或Symbol)
descriptor 属性的描述符对象

  defineProperty陷阱需要在操作成功后返回true,否则返回false。getOwnPropertyDescriptor陷阱只接受trapTarget和key两个参数,最终返回描述符。Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法与对应的陷阱接受相同参数。这个示例实现的是每个陷阱的默认行为

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"

  这段代码通过Object.defineProperty()方法在代理上定义了属性"name",该属性的描述符可通过Object.getOwnPropertyDescriptor()方法来获取

给Object.defineProperty()添加限制

  defineProperty陷阱返回布尔值来表示操作是否成功。返回true时,Object.defineProperty()方法成功执行;返回false时,Object.defineProperty()方法抛出错误。这个功能可以用来限制Object.defineProperty()方法可定义的属性类型,例如,如果希望阻止Symbol类型的属性,则可以当属性键为symbol时返回false

  当key是Symbol类型时defineProperty代理陷阱返回false,否则执行默认行为。调用Object.defineProperty()并传入"name",因此键的类型是字符串所以方法成功执行;调用Object.defineProperty()方法并传入nameSymbol,defineProperty陷阱返回false所以抛出错误

  [注意]如果让陷阱返回true并且不调用Reflect.defineProperty()方法,则可以让Object.defineProperty()方法静默失效,这既消除了错误又不会真正定义属性

描述符对象限制

  为确保Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法的行为一致,传入defineProperty陷阱的描述符对象已规范化。从getOwnPropertyDescriptor陷阱返回的对象由于相同原因被验证

  无论将什么对象作为第三个参数传递给Object.defineProperty()方法,都只有属性enumerable、configurable、value、writable、get和set将出现在传递给defineProperty陷阱的描述符对象中

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"
});

  在这段代码中,调用Object.defineProperty()时传入包含非标准name属性的对象作为第三个参数。当defineProperty陷阱被调用时,descriptor对象有value属性却没有name属性,这是因为descriptor不是实际传入Object.defineProperty()方法的第三个参数的引用,而是一个只包含那些被允许使用的属性的新对象。Reflect.defineProperty()方法同样也忽略了描述符上的所有非标准属性

  getOwnPropertyDescriptor陷阱的限制条件稍有不同,它的返回值必须是null、undefined或一个对象。如果返回对象,则对象自己的属性只能是enumepable、configurable、value、writable、get和set,在返回的对象中使用不被允许的属性会抛出一个错误

let proxy = new Proxy({}, {
    getOwnPropertyDescriptor(trapTarget, key) {
        return {
            name: "proxy"
        };
    }
});
// 抛出错误
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");

  属性描述符中不允许有name属性,当调用Object.getOwnPropertyDescriptor()时,getOwnPropertyDescriptor的返回值会触发一个错误。这条限制可以确保无论代理中使用了什么方法,Object.getOwnPropertyDescriptor()返回值的结构总是可靠的

重复的描述符方法

  再一次在ES6中看到这些令人困惑的相似方法:看起来Object.defineProperty()方法和Object.getOwnPropertyDescriptor()方法分别与Reflect.defineProperty()方法和Reflect.getOwnPropertyDescriptor()方法做了同样的事情。这4个方法也有一些微妙但却很重要的差异

  Object.defineProperty()方法和Reflect.defineProperty()方法只有返回值不同:Object.defineProperty()方法返回第一个参数,而Reflect.defineProperty()的返回值与操作有关,成功则返回true,失败则返回false

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

  调用Object.defineProperty()时传入target,返回值是target;调用Reflect.defineProperty()时传入target,返回值是true,表示操作成功。由于defineProperty代理陷阱需要返回一个布尔值,因此必要时最好用Reflect.defineProperty()来实现默认行为

  调用Object.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,内部将这个值强制转换为一个对象;另一方面,若调用Reflect.getOwnPropertyDescriptor()方法时传入原始值作为第一个参数,则抛出一个错误

let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// 抛出错误
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");

  由于Object.getOwnPropertyDescriptor()方法将数值2强制转换为一个不含name属性的对象,因此它返回undefined,这是当对象中没有指定的name属性时的标准行为。然而当调用Reflect.getOwnPropertyDescriptor()时立即抛出一个错误,因为该方法不接受原始值作为第一个参数

【ownKeys陷阱】

  ownKeys代理陷阱可以拦截内部方法[[OwnPropertyKeys]],我们通过返回个数组的值可以覆写其行为。这个数组被用于Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Object.assign()4个方法,Object.assign()方法用数组来确定需要复制的属性

  ownKeys陷阱通过Reflect.ownKeys()方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和Symbol类型的都包含在内。Object.getOwnPropertyNames()方法和Object.keys()方法返回的结果将Symbol类型的属性名排除在外,Object.getOwnPropertySymbols()方法返回的结果将字符串类型的属性名排除在外。Object.assign()方法支持字符串和Symbol两种类型

  ownKeys陷阱唯一接受的参数是操作的目标,返回值必须是一个数组或类数组对象,否则就抛出错误。当调用Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()或Object.assign()方法时,可以用ownKeys陷阱来过滤掉不想使用的属性键。假设不想引入任何以下划线字符(在JS中下划线符号表示字段是私有的)开头的属性名称,则可以用ownKeys陷阱过滤掉那些键

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陷阱,它首先调用Reflect.ownKeys()获取目标的默认键列表;接下来,用filter()过滤掉以下划线字符开始的字符串。然后,将3个属性添加到proxy对象:name、_name和nameSymbol。调用Object.getOwnPropertyNames()和Object.Keys()时传入proxy, 只返回name属性;同样,调用Object.getOwnPropertySymbols()时传入proxy,只返回nameSymbol。由于_name属性被过滤掉了,因此它不出现在这两次结果中

  尽管ownKeys代理陷阱可以修改一小部分操作返回的键,但不影响更常用的操作,例如for-of循环和Object.keys()方法,这些不能使用代理来更改。ownKeys陷阱也会影响for-in循环,当确定循环内部使用的键时会调用陷阱

【函数代理中的apply和construct陷阱】

  所有代理陷阱中,只有apply和construct的代理目标是一个函数。函数有两个内部方法[[Call]]和[[Construct]],apply陷阱和construct陷阱可以覆写这些内部方法。若使用new操作符调用函数,则执行[[Construct]]方法;若不用,则执行[[Construct]方法,此时会执行apply陷阱,它和Reflect.apply()都接受以下参数

trapTaqget 被执行的函数(代理的目标)
thisArg 函数被调用时内部this的值
argumentsList 传递给函数的参数数组

  当使用new调用函数时调用的construct陷阱接受以下参数

trapTarget 被执行的函数(代理的目标)
argumentsList 传递给函数的参数数组

  Reflect.construct()方法也接受这两个参数,其还有一个可选的第三个参数newTarget。若给定这个参数,则该参数用于指定函数内部new.target的值
  有了apply和construct陷阱,可以完全控制任何代理目标函数的行为

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

  在这里,有一个返回数字42的函数,该函数的代理分别使用apply陷阱和construct陷阱来将那些行为委托给Reflect.apply()方法和Reflect.construct()方法。最终结果是代理函数与目标函数完全相同,包括在使用typeof时将自己标识为函数。不用new调用代理时返回42,用new调用时创建一个instance对象,它同时是代理和目标的实例,因为instanceof通过原型链来确定此信息,而原型链查找不受代理影响,这也就是代理和目标好像有相同原型的原因

验证函数参数

  apply陷阱和construct陷阱增加了一些可能改变函数执行方式的可能性,例如,假设验证所有参数都属于特定类型,则可以在apply陷阱中检查参数

// 将所有参数相加
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();

  此示例使用apply陷阱来确保所有参数都是数字,sum()函数将所有传入的参数相加。如果传入非数字值,函数仍将尝试操作,可能导致意外结果发生。通过在sumProxy()代理中封装sum(),这段代码拦截了函数调用,并确保每个参数在被调用前一定是数字。为了安全起见,代码还使用construct陷阱来确保函数不会被new调用

  还可以执行相反的操作,确保必须用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);

  在这个示例中,apply陷阱抛出一个错误,而construct陷阱使用Reflect.construct()方法来验证输入并返回一个新实例。当然,也可以不借助代理而用new.target来完成相同的事情

不用new调用构造函数

  new.target元属性是用new调用函数时对该函数的引用,所以可以通过检查new.target的值来确定函数是否是通过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);

  在这段代码中,不用new调用Numbers()会抛出一个错误。如果目标是防止用new调用函数,则这样编写代码比使用代理简单得多。但有时不能控制要修改行为的函数,在这种情况下,使用代理才有意义

  假设Numbers()函数定义在无法修改的代码中,知道代码依赖new.target,希望函数避免检查却仍想调用函数。在这种情况下,用new调用时的行为已被设定,所以只能使用apply陷阱

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]

  apply陷阱用传入的参数调用Reflect.construct(),就可以让Numbersproxy()函数无须使用new就能实现用new调用Numbers()的行为。Numbers()内部的new.target等于Numbers(),所以不会有错误抛出。尽管这个修改new.target的示例非常简单,但这样做显得更加直接

覆写抽象基类构造函数

  进一步修改new.target,可以将第三个参数指定为Reflect.construct()作为赋值给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]

  AbstractNumbersProxy使用construct陷阱来拦截对new AbstractNumbersProxy()方法的调用。然后传入陷阱的参数来调用Reflect.construct()方法,并添加一个空函数作为第三个参数。这个空函数被用作构造函数内部new.target的值。由于new.target不等于AbstractNumbers,因此不会抛出错误,构造函数可以完全执行

可调用的类构造函数

  必须用new来调用类构造函数,因为类构造函数的内部方法[[Call]]被指定来抛出一个错误。但是代理可以拦截对[[Call]]方法的调用,这意味着可以通过使用代理来有效地创建可调用类构造函数。例如,如果希望类构造函数不用new就可以运行,那么可以使用apply陷阱来创建一个新实例

class Person {
    constructor(name) {
        this.name = name;
    }
}
let PersonProxy = new Proxy(Person, {
    apply: function(trapTarget, thisArg, argumentList) {
        return new trapTarget(...argumentList);
    }
});
let me = PersonProxy("huochai");
console.log(me.name); // "huochai"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true

  PersonProxy对象是Person类构造函数的代理,类构造函数是函数,所以当它们被用于代理时就像函数一样。apply陷阱覆写默认行为并返回trapTarget的新实例,该实例与pepson相等。用展开运算符将argumentList传递给trapTarget来分别传递每个参数。不使用new调用PersonProxy()可以返回一个person的实例,如果尝试不使用new调用person(),则构造函数将抛出一个错误。创建可调用类构造函数只能通过代理来进行

 

可撤销代理

  通常,在创建代理后,代理不能脱离其目标。但是可能存在希望撤销代理的情况,然后代理便失去效力。无论是出于安全目的通过API提供一个对象,还是在任意时间点切断访问,撤销代理都非常有用

  可以使用proxy.revocable()方法创建可撤销的代理,该方法采用与Proxy构造函数相同的参数:目标对象和代理处理程序,返回值是具有以下属性的对象

proxy 可被撤销的代理对象
revoke 撤销代理要调用的函数

  当调用revoke()函数时,不能通过proxy执行进一步的操作。任何与代理对象交互的尝试都会触发代理陷阱抛出错误

let target = {
    name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // "target"
revoke();
// 抛出错误
console.log(proxy.name);

  此示例创建一个可撤销代理,它使用解构功能将proxy和revoke变量赋值给Proxy.revocable()方法返回的对象上的同名属性。之后,proxy对象可以像不可撤销代理对象一

以上是关于代理(Proxy)和反射(Reflection)的主要内容,如果未能解决你的问题,请参考以下文章

类的加载机制和反射——使用反射生成JDK动态代理

Java提高班反射和动态代理(JDK Proxy和Cglib)

java深入分析Java反射-动态代理 proxy

java核心学习(四十) 使用反射生成JDK动态代理

java反射和动态代理有啥关系

Nginx反代配置