Open xingbofeng opened 7 years ago
最近在看新出的Nicholas的大作《深入理解ES6》时,发现有一些语法还是平时基本没用到,但是也是写在了最新的ECMAScript标准中的语法,所以想用这篇文章记录一下这些可能被我们忽略的ES6语法。
Nicholas
ECMAScript
ECMAScript6添加了一些内建对象,赋予开发者更多访问JavaScript引擎的能力。代理(Proxy)是一种可以拦截并改变底层JavaScript引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。
ECMAScript6
JavaScript
Proxy
代理可以拦截JavaScript引擎内部目标的底层对象操作,这些底层操作被拦截后会触发相应特定操作的陷阱函数。
反射API以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。
Reflect
每个陷阱覆写JavaScript对象的一些内建特性,可以用它们拦截并修改这些特性。如果仍需使用内建特性,则可以使用相应的反射API方法。创建代理会让代理和反射API的关系变得清楚。
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.name和target.name引用的都是target.name,因此二者的值相同,从而为target.name设置新值后,proxy.name也一同变化了。
proxy.name
target.name
假设要创建一个属性值是数字的对象,对象中每次新增一个属性都要加以验证,如果不是数字必须抛出错误。为了实现这个任务,可以定义一个set陷阱来覆写设置值的默认特性。set陷阱接受四个参数:
set
trapTarget
key
Symbol
value
receiver
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(trapTarget, 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.anthorName = 'proxy';
这段代码定义了一个代理来验证添加到target的新属性,当执行proxy.count = 1时,set陷阱被调用,此时trapTarget的值等于target,key等于count,value等于1,receiver等于proxy。由于target上没有count属性,因此代理继续将value值传入isNaN(),如果结果是true,则证明传入的属性值不是数字,同时也抛出一个错误。在这段代码中,count被设置为1,所以代理调用Reflect.set()方法并传入陷阱接受的4个参数来添加新属性。
target
proxy.count = 1
set陷阱
count
1
proxy
isNaN()
true
Reflect.set()
4
proxy.name可以成功被赋值为一个字符串,这是因为target已经拥有一个name属性,但通过调用trapTarget.hasOwnProperty()方法验证检查后被排除了,所以目标已有的非数字属性仍然可以被操作。
name
trapTarget.hasOwnProperty()
然而,将proxy.anotherName赋值为一个字符串时会抛出错误。目标上没有anotherName属性,所以它的值需要被验证,而由于proxy不是一个数字值,因此抛出错误。
proxy.anotherName
anotherName
JavaScript有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取属性的值。
undefined
let target = {}; console.log(target.name); // undefined
在大多数其他语言中,如果target没有name属性,尝试读取target.name会抛出一个错误。但JavaScript却用undefined来代替target.name属性的值。这个特性会导致重大问题,特别是当错误输入属性名称的时候,而代理可以通过检查对象结构来回避这个问题
对象结构是指对象中所有可用属性和方法的集合,JavaScript引擎通过对象结构来优化代码,通常会创建类来表示对象,如果可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误。代理让对象结构检验变得简单。
因为只有当读取属性时才会检验属性,所以无论对象中是否存在某个属性,都可以通过get陷阱来检测,它接受3个参数。
get陷阱
3
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); // 'proxy' // 读取不存在属性会抛出错误 console.log(proxy.nme); // 抛出错误
此示例中的get陷阱可以拦截属性读取操作,并通过in操作符来判断receiver上是否具有被读取的属性,这里之所以用in操作符检查receiver而不检查trapTarget,是为了防止receiver代理含有has陷阱。在这种情况下检查trapTarget可能会忽略掉has陷阱,从而得到错误结果。属性如果不存在会抛出一个错误,否则就使用默认行为。
in
has陷阱
这段代码展示了如何在没有错误的情况下给proxy添加新属性name,并写入值和读取值。最后一行包含一个输入错误:proxy.nme,由于nme是一个不存在的属性,因而抛出错误。
proxy.nme
可以用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操作并返回一个不同的值
toString
Object
每当使用in操作符时都会调用has陷阱,并传入两个参数。
trapTaqget
Reflect.has()方法也接受这些参数并返回in操作符的默认响应,同时使用has陷阱和Reflect.has()可以改变一部分属性被in检测时的行为,并恢复另外一些属性的默认行为。例如,可以像这样隐藏之前示例中的value属性:
Reflect.has()
let target = { name: 'target', value: 42, }; let proxy = new Proxy(target, { has(trapTarget, key) { // 拦截in操作符,隐藏了value属性 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。
'value'
false
delect操作符可以从对象中移除属性,如果成功则返回true,不成功则返回false。在严格模式下,如果尝试删除一个不可配置(configurable为false)属性则会导致程序抛出错误,而在非严格模式下只是返回false。
delect
configurable
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
console.log()
deleteProperty陷阱
每当通过delete操作符删除对象属性时,deleteProperty陷阱都会被调用,它接受两个参数:
Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,并且接受同样的两个参数。结合二者可以改变delete的具体表现行为,例如,可以像这样来确保value属性不会被删除。
Reflect.deleteProperty()
deleteProperty
let target = { name: 'target', value: 42 }; let proxy = new Proxy(target, { deleteProperty(trapTarget, key) { // 拦截delect操作符,不允许删除value属性 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
所有代理陷阱中,只有apply和construct的代理目标是一个函数。函数有两个内部方法[[Call]]和[[Construct]],apply陷阱和construct陷阱可以覆写这些内部方法。若使用new操作符调用函数,则执行[[Construct]]方法;若不用,则执行[[Construct]]方法,此时会执行apply陷阱,它和Reflect.apply()都接受以下参数:
apply
construct
[[Call]]
[[Construct]]
new
Reflect.apply()
Reflect.apply()和apply陷阱支持下面这些参数:
thisArg
argumentsList
而使用new调用函数时调用的construct陷阱和Reflect.construct()则接受以下参数:
Reflect.construct()
有了apply和construct陷阱,可以完全控制任何代理目标函数的行为。
let target = function() { return 42; }; let 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通过原型链来确定此信息,而原型链查找不受代理影响,这也就是代理和目标好像有相同原型的原因。
42
typeof
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('所有参数都必须是Number类型!'); } }); return Reflect.apply(trapTarget, thisArg, argumentList); }, construct: function(trapTarget, argumentList) { throw new TypeError('该函数不能通过new来调用!'); } }); console.log(sumProxy(1, 2, 3, 4)); // 10 // 抛出错误 console.log(sumProxy(1, '2', 3, 4)); // 同样抛出错误 let result = new sumProxy();
new.target元属性,是用new调用函数时对该函数的引用,所以可以通过检查new.target的值来确定函数是否是通过new来调用的。
new.target
function Numbers(...values) { if (typeof new.target === 'undefined') { throw new TypeError('该函数必须通过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()
但是假设Numbers()函数定义在无法修改的代码中(如看不到的源码),知道代码依赖new.target,希望函数避免检查却仍想调用函数。在这种情况下,用new调用时的行为已被设定,所以只能使用apply陷阱了。
function Numbers(...values) { if (typeof new.target === 'undefined') { throw new TypeError('该函数必须通过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的示例非常简单,但这样做显得更加直接。
Numbersproxy()
进一步修改new.target,可以将第三个参数指定为Reflect.construct()作为赋值给new.target的特定值。这项技术在函数根据已知值检查new.target时很有用,例如创建抽象基类构造函数。在一个抽象基类构造函数中,new.target理应不同于类的构造函数,就像在这个示例中:
class AbstractNumbers { constructor(...values) { // 判断new的调用函数是否是自身,如果是,则抛出错误 if (new.target === AbstractNumbers) { throw new TypeError('此函数必须被继承'); } 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);
AbstractNumbersProxy使用construct陷阱来拦截对new AbstractNumbersProxy()方法的调用。然后传入陷阱的参数来调用Reflect.construct()方法,并添加一个空函数作为第三个参数。这个空函数被用作构造函数内部new.target的值。由于new.target不等于AbstractNumbers,因此不会抛出错误,构造函数可以完全执行。
AbstractNumbersProxy
new AbstractNumbersProxy()
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('Nicholas'); console.log(me.name); // 'Nicholas' console.log(me instanceof Person); // true console.log(me instanceof PersonProxy); // true
通常,在创建代理后,代理不能脱离其目标。但是可能存在希望撤销代理的情况,然后代理便失去效力。无论是出于安全目的通过API提供一个对象,还是在任意时间点切断访问,撤销代理都非常有用
API
可以使用proxy.revocable()方法创建可撤销的代理,该方法采用与Proxy构造函数相同的参数:目标对象和代理处理程序,返回值是具有以下属性的对象
proxy.revocable()
revoke
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.name返回'target',因为它直接透传了target.name的值。然而,一旦revoke()函数被调用,代理不再是函数,尝试访问proxy.name会抛出一个错误,正如任何会触发代理上陷阱的其他操作一样。
Proxy.revocable()
'target'
revoke()
/** * 验证用户登录态 * @param {String} code 登录返回的状态码 */ const validateLogin = (code) => { if (code === 603) { document.getElementsByClassName('login-text')[0].innerText = '此帐号已在其它地方登录,请重新扫码登录'; document.getElementsByClassName('TimeoutDialog')[0].style.display = 'block'; } else if (code >= 600 && code <= 602) { document.getElementsByClassName('login-text')[0].innerText = '登录超时,请重新扫码登录'; document.getElementsByClassName('TimeoutDialog')[0].style.display = 'block'; } }; function setProxy(func) { return new Proxy(func, { apply: function (trapTarget, thisArg, argumentList) { // argumentList[0]是Promise.then的第一个参数,为一个函数 if (argumentList[0]) { // 在这个函数调用时,拿到函数参数,即response,之后去校验登录态 argumentList[0] = new Proxy(argumentList[0], { apply: function(trapTarget1, thisArg1, argumentList1) { // 只要能取到code,则证明是走业务逻辑的Promise if (argumentList1[0] && argumentList1[0].data && argumentList1[0].data.code !== void 0) { // 如果有code,则校验登录态 validateLogin(argumentList1[0].data.code); } // 否则直接调用它 return Reflect.apply(trapTarget1, thisArg1, argumentList1); } }); } return Reflect.apply(trapTarget, thisArg, argumentList); } }); };
这样我们悄无声息地在Promise.then的参数函数中,校验了用户登录态。
Promise.then
最近在看新出的
Nicholas
的大作《深入理解ES6》时,发现有一些语法还是平时基本没用到,但是也是写在了最新的ECMAScript
标准中的语法,所以想用这篇文章记录一下这些可能被我们忽略的ES6语法。反射(Reflect)和代理(Proxy)
ECMAScript6
添加了一些内建对象,赋予开发者更多访问JavaScript
引擎的能力。代理(Proxy
)是一种可以拦截并改变底层JavaScript
引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。代理可以拦截
JavaScript
引擎内部目标的底层对象操作,这些底层操作被拦截后会触发相应特定操作的陷阱函数。反射API以
Reflect
对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect
方法。每个陷阱覆写
JavaScript
对象的一些内建特性,可以用它们拦截并修改这些特性。如果仍需使用内建特性,则可以使用相应的反射API方法。创建代理会让代理和反射API的关系变得清楚。创建一个简单代理
这个示例中的代理将所有操作直接转发到目标。由于
proxy.name
和target.name
引用的都是target.name
,因此二者的值相同,从而为target.name
设置新值后,proxy.name
也一同变化了。使用set陷阱验证属性(代理模式之保护代理)
假设要创建一个属性值是数字的对象,对象中每次新增一个属性都要加以验证,如果不是数字必须抛出错误。为了实现这个任务,可以定义一个
set
陷阱来覆写设置值的默认特性。set
陷阱接受四个参数:trapTarget
用于接受属性(代理的目标)的对象key
要写入的属性键(字符串或Symbol
类型)value
被写入属性的值receiver
操作发生的对象(通常是代理)这段代码定义了一个代理来验证添加到
target
的新属性,当执行proxy.count = 1
时,set陷阱
被调用,此时trapTarget
的值等于target
,key
等于count
,value
等于1
,receiver
等于proxy
。由于target
上没有count
属性,因此代理继续将value
值传入isNaN()
,如果结果是true
,则证明传入的属性值不是数字,同时也抛出一个错误。在这段代码中,count
被设置为1
,所以代理调用Reflect.set()
方法并传入陷阱接受的4
个参数来添加新属性。proxy.name
可以成功被赋值为一个字符串,这是因为target
已经拥有一个name
属性,但通过调用trapTarget.hasOwnProperty()
方法验证检查后被排除了,所以目标已有的非数字属性仍然可以被操作。然而,将
proxy.anotherName
赋值为一个字符串时会抛出错误。目标上没有anotherName
属性,所以它的值需要被验证,而由于proxy
不是一个数字值,因此抛出错误。用get陷阱验证对象结构
JavaScript
有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined
代替被读取属性的值。在大多数其他语言中,如果
target
没有name
属性,尝试读取target.name
会抛出一个错误。但JavaScript
却用undefined
来代替target.name
属性的值。这个特性会导致重大问题,特别是当错误输入属性名称的时候,而代理可以通过检查对象结构来回避这个问题对象结构是指对象中所有可用属性和方法的集合,
JavaScript
引擎通过对象结构来优化代码,通常会创建类来表示对象,如果可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误。代理让对象结构检验变得简单。因为只有当读取属性时才会检验属性,所以无论对象中是否存在某个属性,都可以通过
get陷阱
来检测,它接受3
个参数。trapTarget
被读取属性的源对象(代理的目标)key
要读取的属性键(字符串或Symbol)receiver
操作发生的对象(通常是代理)此示例中的
get陷阱
可以拦截属性读取操作,并通过in
操作符来判断receiver
上是否具有被读取的属性,这里之所以用in
操作符检查receiver
而不检查trapTarget
,是为了防止receiver
代理含有has陷阱
。在这种情况下检查trapTarget
可能会忽略掉has陷阱
,从而得到错误结果。属性如果不存在会抛出一个错误,否则就使用默认行为。这段代码展示了如何在没有错误的情况下给
proxy
添加新属性name
,并写入值和读取值。最后一行包含一个输入错误:proxy.nme
,由于nme是一个不存在的属性,因而抛出错误。使用has陷阱隐藏已有属性
可以用
in
操作符来检测给定对象中是否含有某个属性,如果自有属性或原型属性匹配这个名称或Symbol
就返回true
。value
是一个自有属性,toString
是一个继承自Object
的原型属性,二者在对象上都存在,所以用in
操作符检测二者都返回true
。在代理中使用has陷阱
可以拦截这些in
操作并返回一个不同的值每当使用
in
操作符时都会调用has陷阱
,并传入两个参数。trapTaqget
读取属性的对象(代理的目标)key
要检查的属性键(字符串或Symbol
)Reflect.has()
方法也接受这些参数并返回in
操作符的默认响应,同时使用has陷阱
和Reflect.has()
可以改变一部分属性被in检测时的行为,并恢复另外一些属性的默认行为。例如,可以像这样隐藏之前示例中的value
属性:代理中的
has陷阱
会检查key
是否为'value'
,如果是的话返回false
,若不是则调用Reflect.has()
方法返回默认行为。结果是即使target
上实际存在value
属性,但用in
操作符检查还是会返回false
,而对于name
和toString
则正确返回true
。用delectProperty陷阱防止删除属性
delect
操作符可以从对象中移除属性,如果成功则返回true
,不成功则返回false
。在严格模式下,如果尝试删除一个不可配置(configurable
为false
)属性则会导致程序抛出错误,而在非严格模式下只是返回false
。用
delete
操作符删除value
属性后,第三个console.log()
调用中的in
操作最终返回false
。不可配置属性name
无法被删除,所以delete
操作返回false
(如果这段代码运行在严格模式下会抛出错误)。在代理中,可以通过deleteProperty陷阱
来改变这个行为。每当通过
delete
操作符删除对象属性时,deleteProperty陷阱
都会被调用,它接受两个参数:trapTarget
要删除属性的对象(代理的目标)key
要删除的属性键(字符串或Symbol
)Reflect.deleteProperty()
方法为deleteProperty
陷阱提供默认实现,并且接受同样的两个参数。结合二者可以改变delete
的具体表现行为,例如,可以像这样来确保value
属性不会被删除。函数代理中的apply和construct陷阱
所有代理陷阱中,只有
apply
和construct
的代理目标是一个函数。函数有两个内部方法[[Call]]
和[[Construct]]
,apply
陷阱和construct
陷阱可以覆写这些内部方法。若使用new
操作符调用函数,则执行[[Construct]]
方法;若不用,则执行[[Construct]]
方法,此时会执行apply
陷阱,它和Reflect.apply()
都接受以下参数:Reflect.apply()
和apply
陷阱支持下面这些参数:trapTaqget
被执行的函数(代理的目标)thisArg
函数被调用时内部this的值argumentsList
传递给函数的参数数组而使用
new
调用函数时调用的construct
陷阱和Reflect.construct()
则接受以下参数:trapTarget
被执行的函数(代理的目标)argumentsList
传递给函数的参数数组有了
apply
和construct
陷阱,可以完全控制任何代理目标函数的行为。在这里,有一个返回数字
42
的函数,该函数的代理分别使用apply
陷阱和construct
陷阱来将那些行为委托给Reflect.apply()
方法和Reflect.construct()
方法。最终结果是代理函数与目标函数完全相同,包括在使用typeof
时将自己标识为函数。不用new
调用代理时返回42
,用new
调用时创建一个instance
对象,它同时是代理和目标的实例,因为instanceof
通过原型链来确定此信息,而原型链查找不受代理影响,这也就是代理和目标好像有相同原型的原因。验证函数参数
apply
陷阱和construct
陷阱增加了一些可能改变函数执行方式的可能性,例如,假设验证所有参数都属于特定类型,则可以在apply
陷阱中检查参数:不用new调用构造函数
new.target
元属性,是用new
调用函数时对该函数的引用,所以可以通过检查new.target
的值来确定函数是否是通过new
来调用的。在这段代码中,不用
new
调用Numbers()
会抛出一个错误。如果目标是防止用new
调用函数,则这样编写代码比使用代理简单得多。但有时不能控制要修改行为的函数,在这种情况下,使用代理才有意义。但是假设
Numbers()
函数定义在无法修改的代码中(如看不到的源码),知道代码依赖new.target
,希望函数避免检查却仍想调用函数。在这种情况下,用new
调用时的行为已被设定,所以只能使用apply
陷阱了。apply
陷阱用传入的参数调用Reflect.construct()
,就可以让Numbersproxy()
函数无须使用new
就能实现用new
调用Numbers()
的行为。Numbers()
内部的new.target
等于Numbers()
,所以不会有错误抛出。尽管这个修改new.target
的示例非常简单,但这样做显得更加直接。覆写抽象基类构造函数
进一步修改
new.target
,可以将第三个参数指定为Reflect.construct()
作为赋值给new.target
的特定值。这项技术在函数根据已知值检查new.target
时很有用,例如创建抽象基类构造函数。在一个抽象基类构造函数中,new.target
理应不同于类的构造函数,就像在这个示例中:AbstractNumbersProxy
使用construct
陷阱来拦截对new AbstractNumbersProxy()
方法的调用。然后传入陷阱的参数来调用Reflect.construct()
方法,并添加一个空函数作为第三个参数。这个空函数被用作构造函数内部new.target
的值。由于new.target
不等于AbstractNumbers
,因此不会抛出错误,构造函数可以完全执行。可调用的类构造函数
必须用
new
来调用类构造函数,因为类构造函数的内部方法[[Call]]
被指定来抛出一个错误。但是代理可以拦截对[[Call]]
方法的调用,这意味着可以通过使用代理来有效地创建可调用类构造函数。例如,如果希望类构造函数不用new
就可以运行,那么可以使用apply
陷阱来创建一个新实例。可撤销代理
通常,在创建代理后,代理不能脱离其目标。但是可能存在希望撤销代理的情况,然后代理便失去效力。无论是出于安全目的通过
API
提供一个对象,还是在任意时间点切断访问,撤销代理都非常有用可以使用
proxy.revocable()
方法创建可撤销的代理,该方法采用与Proxy
构造函数相同的参数:目标对象和代理处理程序,返回值是具有以下属性的对象proxy
可被撤销的代理对象revoke
撤销代理要调用的函数此示例创建一个可撤销代理,它使用解构功能将
proxy
和revoke
变量赋值给Proxy.revocable()
方法返回的对象上的同名属性。之后,proxy
对象可以像不可撤销代理对象一样使用。因此proxy.name返回'target'
,因为它直接透传了target.name
的值。然而,一旦revoke()
函数被调用,代理不再是函数,尝试访问proxy.name
会抛出一个错误,正如任何会触发代理上陷阱的其他操作一样。在业务中,可以这样炫技了
这样我们悄无声息地在
Promise.then
的参数函数中,校验了用户登录态。