使用 apply 与 construct 陷阱函数的函数代理
在所有的代理陷阱中,只有 apply 与 construct 要求代理目标对象必须是一个函数。回忆一下第三章的内容,函数拥有两个内部方法: 与 ,前者会在函数被直接调用时执行,而后者会在函数被使用 new 运算符调用时执行。apply 与 construct 陷阱函数对应着这两个内部方法,并允许你对其进行重写。当不使用 new 去调用一个函数时,apply 陷阱函数会接收到下列三个参数(Reflect.apply() 也会接收这些参数) :
-
trapTarget:被执行的函数(即代理的目标对象);
-
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);
}
});
// a proxy with a function as its target looks like a function
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 运算符调用会产生一个实例对象 instance。instance 对象会被同时判定为 proxy 与 target 对象的实例,是因为 instanceof 运算符使用了原型链来进行推断,而原型链查找并没有受到这个代理的影响,因此 proxy 对象与 target 对象对于 JS 引擎来说就有同一个原型。
验证函数的参数
apply 与 construct 陷阱函数在函数的执行方式上开启了很多的可能性。例如, 假设你想要保证所有参数都是某个特定类型的,可使用 apply 陷阱函数来进行验证:
// adds together all arguments
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
// throws error
console.log(sumProxy(1, "2", 3, 4));
// also throws error
let result = new sumProxy();
此例使用了 apply 陷阱函数来确保所有的参数都是数值。sum() 函数会将所有传递进来的参数值相加,如果传入参数的值不是数值类型,该函数仍然会尝试加法操作, 这样可能会导致意外的结果。此代码通过将 sum() 函数封装在 sumProxy() 代理中,在函数运行之前拦截了函数调用,以保证每个参数都是数值。出于安全的考虑,这段代码使用 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]
// throws error
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]
// throws error
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]
NumbersProxy 函数允许你调用 Numbers 而无须使用 new,并且让这种调用的效果与使用了 new 的情况保持一致。为此,apply 陷阱函数使用传给自身的参数去对 Reflect.construct() 方法进行了调用,于是 Numbers 内部的 new.target 就被设置为 Numbers,从而避免抛出错误。尽管这只是修改 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]
// throws error
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 来调用,原因是类构造器的内部方法 被明确要求抛出错误。然而代理可以拦截对于 方法的调用,意味着你可以借助代理有效创建一个可被调用的类构造器。例如,如果想让类构造器在缺少 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("Nicholas");
console.log(me.name); // "Nicholas"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
PersonProxy 对象是 Person 类构造器的一个代理。类构造器实际上也是函数, 因此在使用代理时它的行为就像函数一样。apply 陷阱函数重写了默认的行为,返回 trapTarget(这里等于 Person)的一个实例, 此代码使用 trapTarget 以保证通用性,避免了手动指定特定的类。此处还使用了扩展运算符,将 argumentList 展开并传递给 trapTarget 方法。在没有使用 new 的情况下调用 PersonProxy(),获得了 Person 的一个新实例;而若你试图不使用 new 去调用 Person(),构造器仍然会抛出错误。创建一个可被调用的类构造器,是只有使用代理才能做到的。