数组问题
在ECMAScript 6出现以前,开发者不能通过自己定义的对象模仿JavaScript数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的length属性,也可以通过length属性修改数组元素。例如:
let colors = ["red", "green", "blue"];console.log(colors.length); //3colors[3] = "black";console.log(colors.length); //4console.log(colors[3]); //blackcolors.length = 2;console.log(colors.length); //2console.log(colors[3]); //undefinedconsole.log(colors[2]); //undefinedconsole.log(colors[1]); // "green"
colors数组一开始有3个元素,将colors[3]赋值为”black”时length属性会自动增加到4,将length属性设置为2时会移除数组的后两个元素而只保留前两个。在ECMAScript 5之前开发者无法自己实现这些行为,但现在通过代理就可以了。
代理和反射
调用new Proxy()可创建代替其他目标(target)对象的代理,它虚拟化了目标,所以二者看起来功能一致。
代理可以拦截JavaScript引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
反射API以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。表12-1总结了代理陷阱的特性。
创建一个简单的代理
用Proxy构造函数创建代理需要传入两个参数:目标(target)和处理程序(handler)。处理程序是定义一个或多个陷阱的对象,在代理中,除了专门为操作定义的陷阱外,其余操作均使用默认特性。不使用任何陷阱的处理程序等价于简单的转发代理,就像这样:
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name);
console.log(target.name);
target.name = "target";
console.log(proxy.name);
console.log(target.name);
这个示例中的代理将所有操作直接转发到目标,将”proxy”赋值给proxy.name属性时会在目标上创建name,代理只是简单地将操作转发给目标,它不会储存这个属性。由于proxy.name和target.name引用的都是target.name,因此二者的值相同,从而为target.name设置新值后,proxy.name也一同变化。当然,没有陷阱的代理不是很有趣,如果定义一个陷阱会发生什么呢?
使用set陷阱验证属性
假设你想创建一个属性值是数字的对象,对象中每新增一个属性都要加以验证,如果属性值不是数字必须抛出错误。为了实现这个任务,可以定义一个set陷阱来覆写设置值的默认特性。set陷阱接受4个参数:
· trapTarget 用于接收属性(代理的目标)的对象。
· 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("属性必须是数字");
}
}
//添加属性
return Reflect.set(target, key, value, receiver);
}
});
//添加一个新属性
proxy.count = 1;
console.log(proxy.count);
console.log(target.count);
//由于目标已有name属性因而可以给它赋值
proxy.name = "proxy";
console.log(proxy.name);
console.log(target.name);
//给属性赋值非数值会抛出错误
proxy.anotherName = "proxy";
用get陷阱验证对象结构(Object Shape)
JavaScript有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取属性的值,就像在这个示例中
let target = {};
console.log(target.name); //undefined
对象结构是指对象中所有可用属性和方法的集合,JavaScript引擎通过对象结构来优化代码,通常会创建类来表示对象,如果你可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误,这对我们很有帮助。代理让对象结构检验变得简单。
因为只有当读取属性时才会检验属性,所以无论对象中是否存在某个属性,都可以通过get陷阱来检测,它接受3个参数:
· trapTarget 被读取属性的源对象(代理的目标)。
· key 要读取的属性键(字符串或Symbol)。
· receiver 操作发生的对象(通常是代理)。由于get陷阱不写入值,所以它复刻了set陷阱中除value外的其他3个参数,Reflect.get()也接受同样3个参数并返回属性的默认值。
如果属性在目标上不存在,则使用get陷阱和Reflect.get()时会抛出错误,就像这样:
let target = {};
console.log(target.name); //undefined
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("属性" + key + "不存在");
}
return Reflect.get(trapTarget, key, receiver);
}
});
//添加一个属性,程序仍然正常运行
proxy.name = "proxy";
console.log(proxy.name);
//如果属性不存在,则抛出错误
console.log(proxy.nme);
此示例中的get陷阱可以拦截属性读取操作,并通过in操作符来判断receiver上是否具有被读取的属性,这里之所以用in操作符检查receiver而不检查trapTarget,是为了防止receiver代理含有has陷阱(下一节讲解)。在这种情况下检查trapTarget可能会忽略掉has陷阱,从而得到错误结果。属性如果不存在会抛出一个错误,否则就使用默认行为。
这段代码展示了如何在没有错误的情况下给proxy添加新属性name,并写入值和读取值。最后一行包含一个输入错误:proxy.nme有可能是proxy.name,由于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陷阱,并传入两个参数:
· trapTarget 读取属性的对象(代理的目标)。
· 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);
console.log("name" in proxy);
console.log("toString" in proxy);
代理中的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);
console.log("value" in target);
// 注意,在严格模式下,下面这行代码会抛出一个错误
let result2 = delete target.name;
console.log(result2);
console.log("name" in target);
用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.defineProperty(trapTarget, key);
}
}
});
// 尝试删除proxy.value
console.log("value" in target); //true
let result1 = delete target.value;
console.log(result1);//false
console.log("value" in target); //true
// 尝试删除proxy.name
console.log("name" in target); //true
let result2 = delete target.name;
console.log(result2); //true
console.log("name" in target); //false
这段代码与has陷阱的示例非常相似,deleteProperty陷阱检查key是否为”value”,如果是的话返回false,否则调用Reflect.deleteProperty()方法来使用默认行为。由于通过代理的操作被捕获,因此value属性无法被删除,但name属性就如期被删除了。如果你希望保护属性不被删除,而且在严格模式下不抛出错误,那么这个方法非常使用。
原型代理陷阱
第4章介绍了ECMAScript 6新增的Object.setPrototypeOf()方法,它被用于作为ECMAScript 5中的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);
console.log(proxy === Object.prototype);
console.log(proxyProto);
// 成功
Object.setPrototypeOf(target, {});
//抛出错误
Object.setPrototypeOf(proxy, {}); 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(proxy === 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(proxy === Object.prototype); //true
console.log(proxyProto);
// 成功
Object.setPrototypeOf(target, {});
//同样也成功
Object.setPrototypeOf(proxy, {});
由于本示例中的getPrototypeOf陷阱和setPrototypeOf陷阱仅使用了默认行为,因此可以交换使用target和paroxy并得到相同结果。由于Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法与Object上的同名方法存在一些重要差异,因此使用它们是很重要的。
