什么时候在当前对象和原型上创建属性?

When is a property created on the current object VS a prototype?

本文关键字:创建 属性 原型 对象 什么时候      更新时间:2023-09-26

我正在阅读谷歌的JS优化页面,并得到了这个部分:

将实例变量声明/初始化放在值类型为…的实例变量的原型上。这避免了每次调用构造函数时不必要地运行初始化代码。

我想,"等等,那你就不能改变实例中的值了"

但事实证明你可以,这让我很惊讶。

为什么当你改变一个原型提供给你的值时,它会在对象本身上被设置?

x = { one: 1 }
y = Object.create(x)
y.one // 1, of course
y.one++;
y.one // 2
x.one // 1! why isn't this 2?

定义此行为的规则是什么?

是不是左侧查找总是在最近的对象上,而右侧查找将遍历原型链?

如果是这样,这将适用于引用类型和值类型,但也许突变(y.arr.push(1))的诱惑太大了,因此Google建议只对值进行此操作?

这有点复杂。

右侧:查找原型链,找到第一个匹配的(无论是getter还是data属性),如果没有找到则返回undefined

左边:查找原型链,如果找到setter就使用它;否则,在对象本身设置一个数据值。

如果简单的赋值改变了原型,那将是可怕的。(想象一下,例如,如果你切掉你自己的一个千克重量,国际千克原型会自动改变重量,随之而来的是混乱。)

这是否意味着只有显式setter,而不仅仅是任何可编辑的属性?我能麻烦你找一个在原型之上有setter的例子和一个没有setter的例子吗?

var x = {
  data: 1,
  hidden: 1,
  set accessor(val) {
    this.hidden = val;
  },
  get accessor() {
    return this.hidden;
  }
};
var y = Object.create(x);
console.log("orig: x.data:", x.data, "; y.data:", y.data); // 1, 1
y.data = y.data + 1;
console.log("inc: x.data:", x.data, "; y.data:", y.data); // 1, 2
delete y.data; // removes y.data, so y.data is again looked up at x.data
console.log("del data: x.data:", x.data, "; y.data:", y.data); // 1, 1
console.log("----------");
console.log("orig: x.accessor:", x.accessor, "; y.accessor:", y.accessor); // 1, 1
y.accessor = y.accessor + 1;
console.log("inc: x.accessor:", x.accessor, "; y.accessor:", y.accessor); // 1, 2
console.log("     x.hidden:", x.accessor, "; y.hidden:", y.hidden); // 1, 2
delete y.accessor; // silent fail, y.accessor doesn't exist
console.log("del acc: x.accessor:", x.accessor, "; y.accessor:", y.accessor); // 1, 2
console.log("         x.hidden:", x.hidden, "; y.hidden:", y.hidden); // 1, 2
delete y.hidden;
console.log("del hidden: x.accessor:", x.accessor, "; y.accessor:", y.accessor); // 1, 1
console.log("            x.hidden:", x.hidden, "; y.hidden:", y.hidden); // 1, 1

这里你可以看到的区别是,我们可以在y.data = ...之后删除y.data(数据属性),但是我们不能在y.accessor = ...之后删除y.accessor,因为它不存在——x.accessor是用来设置y.hidden的。

这是因为属性赋值([[Set]]内部方法)到达了原型对象x上的属性,但接收器仍然是y。然后,在y上定义属性。x的属性保持不变。

从技术上讲,赋值y.one = 2是这样做的:
  1. y.[[Set]]("one", 2, y)被调用
  2. 由于y没有"one"自己的属性,调用被重定向到父y.[[GetPrototypeOf]](),即x
  3. x.[[Set]]("one", 2, y)被调用。
  4. 由于x"one"自己的属性,这是一个数据属性(即不是getter/setter访问器),并且是可写的,因此在y上定义了一个新属性
  5. y.[[DefineOwnProperty]]("one", PropertyDescriptor{[[Value]]: 2})

如果你不希望这样,你可以使用自定义访问器:

var one = 1;
var x = {
  get one() {
    return one;
  },
  set one(value) {
    one = value;
  }
};
var y = Object.create(x);
console.log(y.one); // 1
y.one++;
console.log(y.one); // 2
console.log(x.one); // 2