内部方法[[Set]]
当执行obj.prop = value时,其实会调用内部方法[[Set]],主要有修改属性的值和定义新属性两种行为。我们接下来逐一分析:
定义属性
我先说一个看似正确的结论:当我们使用obj.prop = value时,如果obj本身没有key为'prop'的属性,就会定义一个属性。
请注意,这段话不完全正确,我们从一个奇怪的“BUG”开始。
1. 奇怪的“BUG”
先看一段代码:
let value;
const proto = {
get prop() {
return value;
},
set prop(v) {
value = v;
}
};
const obj = Object.create(proto);
obj.prop = 'Hello';
check(obj, 'prop');
check(proto, 'prop');
function check(object, key) {
console.log(`has own: ${Object.hasOwn(object, key)}, key: ${key}, value: ${object[key]}`);
}
运行结果:
has own: false, key: prop, value: Hello
has own: true, key: prop, value: Hello
可以看到,我们想要给obj定义一个key为prop的属性,但是很奇怪,上面代码运行完之后,obj上并没有添加一个新属性,反而是执行了原型链上的一个访问器属性的setter。
要想探究这个原因,就得去看ES6的规范了。obj.prop = 'Hello';在底层其实是调用了obj对象的内部方法[[Set]],所以,我们需要看一下[[Set]]的具体流程。
这里我们直接引用ES6规范的原文:
我们结合上面的例子,一起走一遍[[Set]]方法的流程。
整个流程和我们一开始想的“定义一个属性”好像完全不沾边,[[Set]]方法首先会沿着原型链找到同名的属性,在上面的例子中,找的了一个访问器属性,然后直接调用了这个访问器属性的setter,即:
set prop(v) {
value = v;
}
然后就返回了,并没有给obj添加新的属性。这就是我们上面那个奇怪的“BUG”的原因。
2. 原型链中找到一个同名的数据属性
我们再简单总结一下上面产生问题的原因,那就是,沿着原型链找key为prop的属性,第一个找到的是访问器属性。
但是,如果第一个找到的是数据属性呢?改一下上面的例子:
const proto = {
prop: 'world!'
};
Object.setPrototypeOf(proto, null);
const obj = Object.create(proto);
obj.prop = 'Hello';
check(obj, 'prop');
check(proto, 'prop');
function check(object, key) {
console.log(`has own: ${Object.hasOwn(object, key)}, key: ${key}, value: ${object[key]}`);
}
has own: true, key: prop, value: Hello
has own: true, key: prop, value: world!
运行的结果也符合预期,就是obj上创建了一个新属性。我们看一下[[Set]]的执行流程。
一开始的流程还是一样,因为obj本身没有prop这个属性,就需要沿着原型链找,然后在proto上找到了这个属性:
当在proto上找到这个属性后,还会检查它的属性描述符中的writable是否为false,如果为false就直接返回了。
我们可以做个假设,对于我们上面的例子,如果proto的prop属性不可写,我们就没办法在obj上定义prop属性。我们写个代码验证一下:
const proto = Object.create(null);
Object.defineProperty(proto, 'prop', {
value: 'world!',
writable: false,
})
const obj = Object.create(proto);
obj.prop = 'Hello';
check(obj, 'prop');
check(proto, 'prop');
function check(object, key) {
console.log(`has own: ${Object.hasOwn(object, key)}, key: ${key}, value: ${object[key]}`);
}
has own: false, key: prop, value: world!
has own: true, key: prop, value: world!
可以看到,obj创建属性失败了。
3. 整个原型链都没有找到这个属性
我们已经介绍了,在定义属性之前,会现在原型链中找同名属性;上面已经了解了几种情况:
- 找到访问器属性:不会创建新属性,直接调用
setter - 找到数据属性:
- 该数据属性可写:创建新属性
- 该数据属性不可写:不创建新属性
但是,如果没有找到同名的属性呢,这也是平时写代码最常见的一种情况:
没有找到这个属性的话,就会创建一个新的属性描述符,重要的是指定[[writable]]为true,这样,之后的流程就和“找到数据属性”一致了,最终会定义一个新属性。
const proto = Object.create(null);
const obj = Object.create(proto);
obj.prop = 'Hello';
check(obj, 'prop');
check(proto, 'prop');
function check(object, key) {
console.log(`has own: ${Object.hasOwn(object, key)}, key: ${key}, value: ${object[key]}`);
}
has own: true, key: prop, value: Hello
has own: false, key: prop, value: undefined
修改属性的值
修改属性就没什么可说的了,平时开发经常用,这里也走一遍流程吧。
1. 修改数据属性
修改一个数据属性其实就是将新的值作为属性描述符的
[[Value]]属性,然后调用另一个[[DefineOwnProperty]]内部方法。
2. 修改访问器属性
修改一个访问器属性会调用setter。
总结
[[Set]]内部方法主要有两个用途:
- 定义一个新属性
- 修改属性的值
在obj.prop = value时,如果obj本身没有prop这个属性,就会沿着原型链找同名的prop属性:
- 如果找到一个访问器属性,则直接调用
setter,且不会给obj定义新属性。 - 如果找到一个数据属性,判断这个属性是否可写:
- 如果可写,则给
obj定义一个新属性 - 如果不可写,则不会给
obj定义新属性
- 如果可写,则给
- 如果没有找到同名属性,则给
obj定义一个新属性。
在obj.prop = value时,如果obj本身有prop属性,则为修改属性:
- 如果
prop为数据属性,判断这个属性是否可写:- 如果可写,通过
[[DefineOwnProperty]]内部方法修改属性的值 - 如果不可写,则不会修改属性的值
- 如果可写,通过
- 如果
prop为访问器属性,则调用setter