JS中的渣对象

544 阅读4分钟

本篇文章主要讲解了JS中对象属性赋值时,会出现的种种情况,在开始之前,为了加深大家的印象,我们来假设一个场景。

让我们来先编个故事

这是一个凄惨/圆满的爱情故事,让我们慢慢来说。

很久以前,有一个很漂亮的女孩,她叫做小菊

var 小菊 = {
  face: 'beauty'
}

然后呢有一个小屁孩叫做小明

var 小明 = {}

小明很喜欢小菊,但是却一直没敢告诉她,他每天等她下课,接送她回家,他已经觉得很幸福了。 有一天,放学时分,小明还是照旧等在校门口,四处张望着,看到小菊走过来,原本倚在墙边的他立马站直了起来,手里似乎攥着什么。 小菊走过他身边,转过头对他说了一声“走吧”。 小明身体紧张得身体前倾了一下,支支吾吾的说道,“等...等一下”。 小菊停住了脚步,回头一个黑人问号。 “我...我不太会说话,给你看这个”,说完小明将手递过去,是一张纸条。 小菊接过纸条,一脸疑惑的打开。 上面写道:

var 小菊 = {}

var 小明 = {
  heart: 小菊,
  wait () {
    console.log('I love you')
  }
}

while(小菊.heart !== 小明) {
  小明.wait()
}

交往(小菊, 小明)

function 交往(p1, p2) {
  console.log(`${p1.name}${p2.name}在一起啦`)
}

小明接着说道,“我的程序死掉了,你能帮一下我吗?” 小菊抬起头,一脸懵逼得看着小明,“我没学过编程哦,不好意思。” 只见小菊从背包里面翻出一只笔来,在纸上加了一行

// ...
while(小菊.heart !== 小明) {
  小明.wait()
+ 小菊.heart = 小明
}

然后抬起头,对小明说:“现在我的心里面有你了,你的程序不会死循环了。” 小明激动地抱住了小菊。 ...

对象属性赋值

咳咳,我们回到正题啊。 让我们来看看这句小菊.heart = 小明,看似很正常,却暗藏杀机的语句。 如果小菊之前没谈过恋爱,那么结果正如我们预期。 但是,如果小菊之前有过感情经历

var 小菊 = Object.create({ heart: "小强" });

小菊.heart === '小强' // true

重点来了(敲黑板),

小菊.heart = 小明

小菊.heart === 小明 // true

看上去很美好哦,不过我们来看看

小菊.__proto__.heart === '小强' // true

???也就是说,当我们执行以下代码

Reflect.deleteProperty(小菊, "heart");

这个时候

小菊.heart === '小强' // true

OK,我们要怎么来理解这个现象呢?

首先我们知道,当在对象上找不到某个属性时,JS引擎会在在其原型链上继续查找,直到找到或者到达原型链末端为止。 所以,从这个过程中我们可以看出,小菊.heart它并不是对某个确定的属性的一个引用,而是查找的一个返回结果。 它会取到访问链最前端的那个,所以也会造成属性覆盖的现象。

但是为什么,小菊.heart = 小明没有将原型链上的同名属性覆盖掉呢?

Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when there is an inherited property with a getter or a setter.

::: hljs-right -- MDN :::

这里说 如果没有继承来的setter或者getter,当给对象设置属性时,只会添加到它自己身上

所以JS引擎并没有选择重写掉原型链上的属性。

这也就带来了一些问题,比如:

var obj1 = {
  a: 1
};

var obj2 = Object.create(obj1);
var obj3 = Object.create(obj1);

obj2.a++;

console.log(obj2.a); // 2
console.log(obj3.a); // 1

当我们想要共享原型上的属性时,却都自己创建了一个副本。

给属性加上setter/getter

如果访问者的属性是被继承的,它的 get 和set 方法会在子对象的属性被访问或者修改时被调用

-- MDN

我们增加一个getter来试试效果

var obj1 = {
  get a() {
    return 1;
  }
};

var obj2 = Object.create(obj1);

obj2.a++;

console.log(obj2); // {}

这个时候,赋值操作没有用了,当设置了setter时也一样。

var obj1 = {
  _a: 1,
  set a(val) {
    obj1._a = val;
  },
  get a() {
    return obj1._a;
  }
};

var obj2 = Object.create(obj1);

obj2.a++;

现在,对于原型链上属性的修改就能直接重写原来的值,而不是自己创建一个属性。

修改属性的property

当涉及到读写属性时,我们就会想到属性描述符

var obj1 = {};

Object.defineProperty(obj1, "a", {
  writable: false,
  enumerable: true,
  configurable: true,
  value: 1
});

var obj2 = Object.create(obj1);

obj2.a++;

console.log(obj2); // {}

当我们将对象原型链上属性的writable设置为false时,对象也不会自己创建一个属性。

那我非要给自己建个属性呢?

当有继承setter/getter,或writable设置为false的时候,如若想要创建自有属性。可以使用Object.defineProperty进行设置。

结语

所以这里的对象有三种情况:

  • 对象的原型属性啥都没设置 => 直接遮盖,但不会影响原型属性
  • 对象的原型属性上有设置setter/getter => 按照继承的setter/getter来重写/获取
  • 对象的原型属性设置为不可写 => 不能对该属性进行重写

OK,这样我们就清楚了,到底谁是渣对象!所以各位,清楚原理很重要,别被表象冲昏了头脑。