这篇文章发布于 2021年07月1日,星期四,23:15,归类于 JS API。 阅读 27411 次, 今日 4 次 12 条评论
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=9984
本文欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可以联系授权。
一、Reflect有什么用?
一句话,Reflect没什么用,除了装装逼,让人看起来高大上以外,并不具有什么牛逼之处。
准确讲应该是这样的,Reflect更像是一种语法变体,其挂在的所有方法都能找到对应的原始语法,也就是Reflect的替代性非常强。
其实从Reflect这个单词本身字面意思就能体会出Reflect的神韵,Reflect的中文意思是“反射”,阳光照在镜子上反射,其实光子还是那些光子,只是变化了方向。
举例说明:
Reflect对象挂载了很多静态方法,所谓静态方法,就是和Math.round()
这样,不需要new就可以直接使用的方法。
比较常用的两个方法就是get()
和set()
方法:
Reflect.get(target, propertyKey[, receiver]) Reflect.set(target, propertyKey, value[, receiver])
就作用而言,等同于:
target[propertyKey] target[propertyKey] = value;
比方说页面上有个输入框,其DOM对象变量是input
,平时我们对整个输入框赋值使用的语句多半是:
input.value = 'zhangxinxu';
就可以直接使用Reflect.set()
方法代替:
Reflect.set(input, 'value', 'zhangxinxu')
效果是一模一样的。
又例如,我们希望对input
的value
属性重新定义,使该输入框value
属性发生变化的时候可以同时触发'change'
事件,下面是使用大家普遍比较熟悉的Object.defineProperty()
方法实现的示意:
const props = Object.getOwnPropertyDescriptor(input, 'value');
Object.defineProperty(input, 'value', {
...props,
set (v) {
let oldv = this.value;
props.set.call(this, v);
// 手动触发change事件
if (oldv !== v) {
this.dispatchEvent(new CustomEvent('change'));
}
}
});
相关介绍可以参见这篇文章:“输入框value属性赋值触发js change事件的实现”
上述代码我们完全可以使用Reflect对象实现,具体的JavaScript代码如下所示。
const props = Reflect.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); Reflect.defineProperty(input, 'value', { ...props, set (v) { let oldv = this.value; props.set.call(this, v); // 手动触发change事件 if (oldv !== v) { this.dispatchEvent(new CustomEvent('change')); } } });
我们可以测试下,假设页面HTML如下:
<input id="input">
测试代码为:
input.addEventListener('change', () => { document.body.append('变化啦~'); }); input.value = 'zhangxinxu';
此时,就可以看到页面上出现了“变化啦~”文字,截图示意如下:
可以在这个JSBIN地址体验:https://output.jsbin.com/vugucajepa
//zxx: 如果你看到这段文字,说明你现在访问是体验糟糕的垃圾盗版网站,你可以访问原文获得很好的体验:https://www.zhangxinxu.com/wordpress/?p=9984(作者张鑫旭)
二、细微差异-返回值
事物存在必有道理,如果Reflect仅仅是换了种语法,存在的意义并不大,很显然,Reflect对象的出现必然有其他的考量。
我认为其中有意义的一点就是返回值。
对于某个对象,赋值并不总是成功的。
例如,我们把 input
的type
属性设置为只读,如下:
Object.defineProperty(input, 'type', { get () { return this.getAttribute('type') || 'text'; } });
传统的使用等于号进行的属性赋值并不能知道最后是否执行成功,需要开发者自己进行进一步的检测。
例如:
console.log(input.type = 'number');
// 输出 false
console.log(Reflect.set(input, 'type', 'number'));
上面一行赋值返回值是'number'
,至于改变输入框的type
属性值是否成功,不得而知。
但是下面一行语句使用的Reflect.set()
方法,就可以知道是否设置成功,因为Reflect.set()
的返回值是true
或者false
(只要参数类型准确)。
除了知道执行结果外,Reflect方法还有个好处,不会因为报错而中断正常的代码逻辑执行。
例如下面的代码:
(function () { 'use strict'; var frozen = { 1: 81 }; Object.freeze(frozen); frozen[1] = 'zhangxinxu'; console.log('no log'); })();
会出现下面的TypeError错误:
Uncaught TypeError: Cannot assign to read only property ‘1’ of object ‘#<Object>’
后面的语句console.log('no log')
就没有被执行。
但是如果使用Reflect方法,则console语句是可以执行的,例如:
(function () { 'use strict'; var frozen = { 1: 81 }; Object.freeze(frozen); Reflect.set(frozen, '1', 'zhangxinxu'); console.log('no log'); })();
控制台运行后的log输出值如下图所示:
三、set、get方法中的receiver参数
就功能而言,Reflect.get()
和Reflect.set()
方法和直接对象赋值没有区别,都是可以互相替代的,例如,下面两段JS效果都是一样的。
还是使用input
这个DOM元素示意。
有人可能会疑问,为什么不用纯对象示意呢?
因为我发现大多数前端都对DOM不怎么感兴趣,那我就反其道行之,故意膈应人 ;另外一个原因就是DOM对象更具象,所见即所得,适合偏感性的同学的学习。
const xyInput = new Proxy(input, { set (target, prop, value) { if (prop == 'value') { target.dispatchEvent(new CustomEvent('change')); } target[prop] = value; return true; }, get (target, prop) { return target[prop]; } }); input.addEventListener('change', () => { document.body.append('变化啦~'); }); xyInput.value = 'zhangxinxu';
和下面的JS代码效果类似的。
const xyInput = new Proxy(input, { set (target, prop, value) { if (prop == 'value') { target.dispatchEvent(new CustomEvent('change')); } return Reflect.set(target, prop, value); }, get (target, prop) { return Reflect.get(target, prop); } }); input.addEventListener('change', () => { document.body.append('变化啦~'); }); xyInput.value = 'zhangxinxu';
均有如下图所示的效果:
但是,当需要使用可选参数receiver参数的时候,直接对象赋值和使用Reflect赋值就会出现差异。
首先,对于DOM元素,应用receiver参数会报错。
例如下面的JS就会报错:
Reflect.set(input, 'value', 'xxx', new Proxy({}, {}));
Uncaught TypeError: Illegal invocation
但是把input换成普通的纯对象,则不会有问题,例如:
// 可以正常执行
Reflect.set({}, 'value', 'xxx', new Proxy({}, {}));
关于receiver参数
说了这么多,receiver
参数到底是干嘛用的呢?
receiver是接受者的意思,表示调用对应属性或方法的主体对象,通常情况下,receiver参数是无需使用的,但是如果发生了继承,为了明确调用主体,receiver参数就需要出马了。
比方说下面这个例子:
let miaoMiao = {
_name: '疫苗',
get name () {
return this._name;
}
}
let miaoXy = new Proxy(miaoMiao, {
get (target, prop, receiver) {
return target[prop];
}
});
let kexingMiao = {
__proto__: miaoXy,
_name: '科兴疫苗'
};
// 结果是疫苗
console.log(kexingMiao.name);
实际上,这里预期显示应该是“科兴疫苗”,而不是“疫苗”。
这个时候,就需要使用receiver
参数了,代码变化部分参见下面标红的那一行:
let miaoMiao = { _name: '疫苗', get name () { return this._name; } } let miaoXy = new Proxy(miaoMiao, { get (target, prop, receiver) { return Reflect.get(target, prop, receiver); // 也可以简写为 Reflect.get(...arguments) } }); let kexingMiao = { __proto__: miaoXy, _name: '科兴疫苗' }; // 结果是科兴疫苗 console.log(kexingMiao.name);
此时,运行结果就是预期的“科兴疫苗”了,如下截图所示:
这就是receiver参数的作用,可以把调用对象当作target参数,而不是原始Proxy构造的对象。
四、其他以及结束语
Reflect对象经常和Proxy代理一起使用,原因有三点:
- Reflect提供的所有静态方法和Proxy第2个handle参数方法是一模一样的。具体见后面的对比描述。
- Proxy get/set()方法需要的返回值正是Reflect的get/set方法的返回值,可以天然配合使用,比直接对象赋值/获取值要更方便和准确。
- receiver参数具有不可替代性。
下表是自己整理的Reflect静态方法和对应的其他函数或功能符。
Reflect方法 | 类似于 |
---|---|
Reflect.apply(target, thisArgument, argumentsList) | Function.prototype.apply() |
Reflect.construct(target, argumentsList[, newTarget]) | new target(…args) |
Reflect.defineProperty(target, prop, attributes) | Object.defineProperty() |
Reflect.deleteProperty(target, prop) | delete target[name] |
Reflect.get(target, prop[, receiver]) | target[name] |
Reflect.getOwnPropertyDescriptor(target, prop) | Object.getOwnPropertyDescriptor() |
Reflect.getPrototypeOf(target) | Object.getPrototypeOf() |
Reflect.has(target, prop) | in 运算符 |
Reflect.isExtensible(target) | Object.isExtensible() |
Reflect.ownKeys(target) | Object.keys() |
Reflect.preventExtensions(target) | Object.preventExtensions() |
Reflect.set(target, prop, value[, receiver]) | target[prop] = value |
Reflect.setPrototypeOf(target, prototype) | Object.setPrototypeOf() |
正是人如其名,Reflect就是其他方法、操作符的“反射”。
好,以上就是本文的内容,带大家了解了下Reflect的七七八八。
希望可以对大家的学习有所帮助。
欢迎转发,欢迎分享,谢谢谢谢!
参考文档
本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
本文地址:https://www.zhangxinxu.com/wordpress/?p=9984
(本篇完)
- 输入框value属性赋值触发js change事件的实现 (0.727)
- 聊聊JS DOM变化的监听检测与应用 (0.273)
- 我对原型对象中this的一个懵懂错误认识 (0.136)
- HTML5 DOM元素类名相关操作API classList简介 (0.136)
- jQuery诞生记-原理与机制 (0.136)
- 简单了解ES6/ES2015 Symbol() 方法 (0.136)
- 深入 JS new Function 语法 (0.091)
- 实用的JS对象分组静态方法Object.groupBy() (0.091)
- Object.is/===、数组at/直接索引、substring/slice的区别 (0.091)
- ES6 JavaScript Promise的感性认知 (0.045)
- 万岁,浏览器原生支持ES6 export和import模块啦! (RANDOM - 0.045)
好
– 为啥加了一句 console 就会变成死循环
“`
let miaoMiao = {
_name: ‘疫苗’,
get name () {
return this._name;
}
}
let miaoXy = new Proxy(miaoMiao, {
get (target, prop, receiver) {
// 加了这一句 #######
console.log(receiver)
return Reflect.get(target, prop, receiver);
// 也可以简写为 Reflect.get(…arguments)
}
});
let kexingMiao = {
__proto__: miaoXy,
_name: ‘科兴疫苗’
};
// 结果是科兴疫苗
console.log(kexingMiao.name);
“`
console在输出日志之前会遍历获取对象上的所有key,在这个过程中会触发get陷阱进入死循环
还有个问题就是,在proxy里set当前prop时,再set其他prop,如果直接target[‘其它prop’] = ‘xxx’ proxy不回拦截,得receiver[‘其它prop’] =’xxx’ 才会拦截
回复juice_157
跟console.log实现有关,有两个原因影响
1. console.log时会调用沿着原型链查找是否有splice方法来判断是不是数组,当查找到proxy时候,被劫持又遇到console.log(receiver),就会陷入死循环。
2. chrome浏览器环境下console.log实现时会调用toString方法,会沿着原型链搜索[Symbol.toStringTag]方法,跟上个原因一样,找到proxy时会进入死循环。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toStringTag
chrome浏览器下验证测试的话可以把代码改成:
let kexingMiao = {
__proto__: miaoXy,
_name: “科兴疫苗”,
splice: undefined,
[Symbol.toStringTag]() {
return ‘Validator’;
}
};
这样在当前对象找到那两个方法后,就不会沿着原型链继续搜索,就不会陷入死循环。
就是把一系列元编程的工具放到一个命名空间下,之前的太零散了,所以很多方法就跟Object下的重复了。
再捞一捞reflect
receiver那块还是不太明白,我在return Reflect.get(target, prop, receiver);之前加一个console.log(receiver),就陷入了一个死循环
能提供一下具体代码吗,我这里尝试是不会死循环的
“`js
const target = {};
const obj = new Proxy(target, {
get(target, key, receiver) {
console.log(‘遍历中…’)
console.log(receiver)// Proxy {}
return Reflect.get(target, key, receiver)
}
})
obj.text
“`
就是绕过 proxy 的方法
+1
学会了好像又没学会…