像 me.name = 'forceddd'
的赋值语句在JavaScript中是随处可见的。但是,我们真的了解这句代码做了什么事情吗?是创建了一个新属性吗?是修改了原有属性的值吗?这次操作是成功还是失败了呢?这只是一行简单的赋值语句,但如果我们认真思考的话,就会发现这种细节其实也没有那么简单。
万物皆可分类讨论。首先分为两类情况:对象 me
上已有 name
属性和没有 name
属性。
当然,我们都知道,JavaScript中存在着原型链机制,所以当对象 me
中不存在 name
属性时,也可以分为两种情况: me
的原型链上存在 name
属性与不存在 name
属性。也就是可以分为三类来讨论。
me中已存在name属性
const me = { name: 'me', }; me.name = 'forceddd'; console.log(me.name); //forceddd
在这种情况下显然我们的目的是重新设置
name
属性的值,结果似乎也是显而易见的,me.name
被修改成了'forceddd'
。但是不要忘记了,一个对象的属性也是拥有它自己的属性的,其中就包括是否是只读的(
writable
),(通常用于描述一个对象的某个属性的各种特性的对象被称为属性描述符或者是属性描述对象),所以当name属性的可写性(writable
)为false
时,结果会是什么样的呢?不仅如此,name
属性定义了getter
或者setter
函数,成为一个访问描述符的时候,结果又会是怎么样的呢?name
属性的writable
为false
时const me = { name: 'me', }; Object.defineProperty(me, 'name', { writable: false, value: me.name, }); me.name = 'forceddd'; // name的属性值仍是 'me' console.log(me.name); //me
因为
name
属性是只读的的,所以赋值操作me.name = 'forceddd'
失败了,这恒河狸。但是要注意的是,这种操作失败是静默的,如果我们在操作之后没有校验name
值的话,是很难发现的。使用严格模式可以把这种静默失败变成显式的TypeError。'use strict';//使用严格模式 const me = { name: 'me', }; Object.defineProperty(me, 'name', { writable: false, value: me.name, }); me.name = 'forceddd'; //TypeError: Cannot assign to read only property 'name' of object '#<Object>'
当
name
属性是访问描述符(定义了getter
或者setter
)时const me = { _name: 'me', get name() { return this._name; }, set name(v) { console.log('调用setter,设置name'); this._name = v; }, }; me.name = 'forceddd';//调用setter,设置name console.log(me.name);//forceddd
此时,
name
属性存在setter
函数,在进行赋值操作时,就会调用setter
函数。一般来说,我们在定义访问描述符时,getter
和setter
都是成对出现的,但是只定义其中一个也是没有任何问题的,只是这样的话可能会出现一些我们不期望的情况,比如,当name
只定义了一个getter
函数时,不存在setter
函数,赋值操作便没有意义了。非严格模式下,因为没有
setter
函数,所以赋值静默失败了。const me = { _name: 'me', get name() { return this._name; }, }; me.name = 'forceddd'; console.log(me.name); //me
严格模式下,对没有
setter
函数的访问描述符进行赋值操作,会出现一个TypeError,也是非常河狸的。'use strict'; const me = { _name: 'me', get name() { return this._name; }, }; me.name = 'forceddd'; //TypeError: Cannot set property name of #<Object> which has only a getter
总结一下,当
me
中存在name
属性时,进行赋值操作时,存在三种情况:- 当属性是一个访问描述符时,如果存在
setter
,则调用setter
;如果不存在setter
,在非严格模式下会静默失败,在严格模式下会产生一个TypeError。 - 当属性的属性描述符中可写性(
writable
)为false
时,在非严格模式下会静默失败,在严格模式下会产生一个TypeError - 当属性不属于上面两种情况时,将值设为该属性的值,赋值成功。
me及其原型链上均不存在name属性
此时,会在对象
me
上新建一个属性name
,值为'forceddd'
。这是也是我们经常使用的一种方式。const human = {}; const me = {}; Object.setPrototypeOf(me, human); me.name = 'forceddd'; console.log({ me, human }); //{ me: { name: 'forceddd' }, human: {} }
我们使用这种方式创建的属性当然是可以通过赋值的方式修改的,这也就是代表着此时这个属性的属性描述符中的
writable
为true
。事实上,此时属性的可配置性、可枚举性也都为true
,这和通过defineProperty
添加属性的默认值是不同的,defineProperty
方法默认添加的属性描述符中writable
、configurable
、enumerable
默认值都为false
。这也是值得注意的一点。console.log(Object.getOwnPropertyDescriptor(me, 'name')); // { // value: 'forceddd', // writable: true, // enumerable: true, // configurable: true // }
通过
defineProperty
方法添加属性Object.defineProperty(me, 'prop', { value: 'forceddd' }); console.log(Object.getOwnPropertyDescriptor(me, 'prop')); // { // value: 'forceddd', // writable: false, // enumerable: false, // configurable: false // }
me中不存在name属性,而其原型链上存在name属性
此时,
me
的原型对象human
上存在name
属性,很多时候,我们使用me.name = 'forceddd'
之类的赋值语句,只是为了修改对象me
,而不想牵涉到其他对象。但是在JS中,访问对象属性时,如果该属性在对象上不存在,便会去原型链上查找,所以对象的原型链是很难绕开的。const human = {}; const me = {}; Object.setPrototypeOf(me, human);
如前文所述,
name
属性在human
对象上也是有三种情况:name
属性的属性描述符中writable
为false
//设置human对象的name属性 Object.defineProperty(human, 'name', { writable: false, value: 'human', }); me.name = 'forceddd'; console.log(me);//{}
此时,当我们进行了赋值操作后,检查
me
对象,会发现它并没有name
属性。WTF?很难理解是吧,因为human
中的name
属性是只读的,所以将human
对象为原型的对象就不能通过=
赋值操作新增name
属性了。其实这是为了模仿类的继承行为,如果父类中的
name
属性是只读的,那么继承它的子类中的name
属性自然应该也是只读的。又因为在JS中时通过原型链来模拟的类的继承行为,所以就导致了这一看起来很奇怪的现象。同样地,如果是在严格模式下,不会是静默失败了,而是会产生一个TypeError。
name
属性是一个访问描述符Object.defineProperty(human, 'name', { set() { console.log('调用了human的setter'); } }); me.name = 'forceddd';//'调用了human的setter' console.log(me);//{}
此时,同样不会在
me
对象上创建name
属性,而是会调用human
上name
属性的setter
函数。类似地,当只存在getter
,不存在setter
时,严格模式下会产生TypeError,非严格模式下会静默失败。name
属性不是之前两种情况这种情况最简单,也是最符合我们预想的,会在
me
对象上创建一个name
属性。
总结一下:当
me
中不存在name
属性,而其原型链上存在name
属性时,有三种情况:- 如果该属性在原型链上是一个访问描述符并且存在
setter
,则会调用这个setter
;如果只存在getter
,则非严格模式下会静默失败,严格模式下出现TypeError。 - 如果该属性在原型链是是一个
writable
为false
的只读属性,在非严格模式下会静默失败,严格模式下出现TypeError。 - 如果该属性在原型链上不是前两种情况,只是一个
writable
为true
的普通属性,则会在me
对象上创建该属性。
如果你一定、必须要修改或者设置 me
的 name
属性的话,可以使用 defineProperty
方法来绕过 =
赋值的这些限制。
Object.defineProperty(human, 'name', {
set() {
console.log('调用了human的setter');
// me.name = 'forceddd';
},
});
Object.defineProperty(me, 'name', {
value: 'forceddd',
enumerable: true,
writable: true,
configurable: true,
});
console.log(me); //{ name: 'forceddd' }
文字上的描述总是没有那么直观,最后总结为一张图,便于理解。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。