# Reflect.set(), 翻看ECMA2020
当下理解有限, 难免有误
Reflect.set()和Reflect.defineProperty
本文为了解释如下现象,
在Reflect.set
配合Proxy
使用的时候, 会有这样一个现象.
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
// receiver 指向obj
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
// 内部调用[[DefineOwnProperty]]
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
默认Reflect.set
的receiver是指向target的, 但是当指向Proxy对象的时候, 会触发Proxy对象的defineProperty.
既然Reflect.set
内部调用了对象的内部方法[[Set]]
, 那么可以猜测, 上面的情况中[[Set]]
调用了receiver的内部方法[[DefineOwnProperty]]
, 从而被Proxy截获.
# Reflect.set()
Reflect.set()
用于对象的属性赋值, 其会调用对象的内部方法[[Set]]
, 需要注意要将其和Accessor Property
的[[Set]]
区分.
# [[Put]]
, [[Set]]
规范中关于对象属性赋值的内容随着版本变化也有所不同, 在ES5中, 对象的属性赋值是调用对象的内部方法[[Put]]
, 但是在我当下最新的ES2020中, 已经使用内部方法[[Set]]
, 主要是文字描述上的区别.
ES5:
PutValue
-> [[Put]]
ES2020:
PutValue
-> [[Set]]
下面看下具体ES2020中的内容,
let p = {
a: 'a'
};
// 步骤6.b
p.a = 'A'
2
3
4
5
对于上述例子, base为p
, 最终调用p
对象的内部[[Set]]
方法, p.[[Set]]('a', 'A', p)
, 可见默认[[Set]]
函数的receiver是this, 即这里的p
.
上述例子如果使用Reflect.set
, 代码如下
let p = {
a: 'a'
};
Reflect.set(p, 'a', 'A')
//或者
//Reflect.set(p, 'a', 'A', p)
2
3
4
5
6
那么再看一个例子
let p = {
a: 'a'
};
let obj = {}
Reflect.set(p, 'a', 'A', obj)
console.log(p.a) // 'a'
console.log(obj.a) // 'A'
2
3
4
5
6
7
8
可以发现, 赋值操作在receiver上生效了, 并没有改变target. 查看ECMA2020中[[Set]]
的相关流程可以知道, [[Set]]
最后是作用在receiver上的, 而target只是用于获取属性描述符. 只不过通常target和receiver是同一个对象, 而Reflect.set
可以指定receiver.
https://stackoverflow.com/questions/47992070/reflect-set-not-working-as-intended
# [[Set]]
具体流程
[[Set]]
-> OrdinarySet
-> OrdinarySetWithOwnDescriptor
let p = {
a: 'a'
};
let obj = {}
Reflect.set(p, 'a', 'A', obj)
console.log(p.a) // 'a'
console.log(obj.a) // 'A'
2
3
4
5
6
7
8
我们的例子中, 首先利用内部方法[[GetOwnProperty]]
通过p
, 获取'a'
属性的属性描述符ownDesc.
Object.getOwnPropertyDescriptor
就是调用内部方法[[GetOwnProperty]]
Object.getOwnPropertyDescriptor(p, 'a')
// {value: "a", writable: true, enumerable: true, configurable: true}
2
在获取到属性描述符之后, 调用OrdinarySetWithOwnDescriptor
由于p.a
定义的是数据描述符, 而obj
本身没有定义'a'
, 所以最后执行CreateDataProperty(Receiver, P, V)
, 这也就解释了, 上述例子的现象.
TIP
数据描述符对应Data Property, 访问/存取描述符对应Accessor Property
根据上述OrdinarySetWithOwnDescriptor
的执行步骤, 不难看出, 下面的代码, 会使得Reflect.set
返回false
let p = {}
Object.defineProperty(p, 'a', {writable:false, value: 'a'})
let obj = {}
Reflect.set(p, 'a', 'A', obj)
2
3
4
let p = {
a: 'a'
}
let obj = {}
Object.defineProperty(obj, 'a', {writable:false, value: 'a'})
Reflect.set(p, 'a', 'A', obj)
2
3
4
5
6
了解了receiver在[[Set]]
中的作用后, 就可以解释, 文章开始所描述的场景.
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
// receiver 指向obj
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
// 内部调用[[DefineOwnProperty]]
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
和之前提到的例子类似的, Reflect.set
会通过CreateDataProperty(Receiver, P, V)
, 从而执行Receiver.[[DefineOwnProperty]](P, newDesc)
, 被Proxy对象obj
的defineProperty
trap拦截到.
仔细回味一下, 我认为这是预期行为, 既然通过Proxy去赋值'a'属性, 而赋值操作内部调用[[DefineOwnProperty]]
, 也就该触发Proxy对应的trap, 毕竟我们是通过Proxy去赋值的.
# [[Set]]
和原型链
通过观察OrdinarySetWithOwnDescriptor
的执行步骤, 可以发现有查找原型链的步骤. 这让我想到<<你不知道的Javascript 上卷>>中关于属性屏蔽的描述.
上面的3种情况其实就是OrdinarySetWithOwnDescriptor
中的各种场景判断
# 参考
https://tc39.es/ecma262/
https://www.ecma-international.org/ecma-262/5.1/
https://stackoverflow.com/questions/47992070/reflect-set-not-working-as-intended
https://github.com/getify/You-Dont-Know-JS
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
https://es6.ruanyifeng.com/#docs/reflect
https://www.zhihu.com/question/267483784