为什么给普通对象设置 __proro__ 属性会有问题?

330 阅读2分钟

设置对象属性时,有一条规则:当给对象设置一个不存的属性时,会把这个属性添加到对象上

var obj = {}
obj.foo = 'bar'

obj // { foo: "bar" }
obj.foo // "bar"

这很好理解。当然,对象的继承属性也可覆盖(Shadow):

// 此处调用的是 Object.prototype 对象上定义的 toString 方法,即
// toString 是 obj 的继承属性
obj.toString() // "[object Object]"

// 我们给 obj 设置一个 toString 方法属性,就能覆盖原型对象上的
obj.toString = function () {
  return JSON.stringify(this)
}

// 设置成功!
obj // {foo: "bar", toString: ƒ}

// 现在再次调用 toString 方法
obj.toString() // "{"foo":"bar"}"

但不是所有的继承属性都能覆盖的,有一个例外—— __proto__ 属性。 __proto__ 是 Object.prototype 对象上的一个访问器属性。

image.png
在 ES5 的 Object.setPrototypeOf() 和 Object.getPrototypeOf() 方法出现之前,__proto__ 是在浏览器中获取和设置原型对象的“事实标准”。不过,因为 __proto__ 是个访问器属性,“获取和设置原型对象”只是表象,实际是靠 setter、setter 内部对真实原型对象的代理。 我们重写 Object.prototype 对象的 __proto__ 属性,来说明内部的代理机制:

var obj = {}
obj.foo = 'bar'

// 重写 Object.prototype 对象的 __proto__ 属性
// 把对原型对象的代理,改为对对象属性 foo 的代理
Object.defineProperty(Object.prototype, '__proto__', {
    get() {
        return this.foo
    },
    set(val) {
       return (this.foo = val)
    }
})

obj // {foo: "bar"}
obj.foo // "bar"
obj.__proto__ // "bar"

// 现在设置 __proto__ 属性
obj.__proto__ = 'Oops'

obj // {foo: "Oops"}
obj.foo // "Oops"
obj.__proto__ // "Oops"

看到没?obj.__proto__ = 'Oops' 并没有给 obj 添加一个 __proto__ 属性,而是修改了对象上的 foo 属性。整个 __proto__ 属性变成了对 foo 属性的代理。

由此,我们可以模拟 __proto__ 属性默认的实现原理:

Object.defineProperty(Object.prototype, '__proto__', {
    get() {
        return Object.getPrototypeOf(this)
    },
    set(val) {
       return Object.setPrototypeOf(this, val)
    }
})

这里只是模拟实现原理,浏览器里使用的是 native code,不可能是 JS 代码。

Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').set.toString()
// "function set __proto__() { [native code] }"
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').get.toString()
// "function get __proto__() { [native code] }"

还需要注意的是,原型对象只可能是 null 或者一个对象,用任何其它值设置原型,都会静默失败。

var obj = {}
obj.__proto__ === Object.prototype

// '' 既不是 null 也不是对象,因此设置(静默)失败
obj.__proto__ = ''

// obj 的原型对象还是 Object.prototype
obj.__proto__ === Object.prototype

现在,我们就可以来回答“为什么给普通对象设置 __proro__ 属性没有创建新属性?” 答案是:设置的 __proro__ 属性时,实际操作的是 Object.prototype 上定义的访问器属性 __proro__,访问器属性的 setter、getter 本质是函数,所以获取和设置都是函数调用。__proro__ 的 setter、getter 内部是对原型对象的操作代理,因此设置 __proro__ 属性的结果是修改了当前对象的原型对象,而没有在当前对象上创建一个新的属性。

(正文完)


广告时间(长期有效)

我有一位好朋友开了一间猫舍,在此帮她宣传一下。现在猫舍里养的都是布偶猫。如果你也是个爱猫人士并且有需要的话,不妨扫一扫她的【闲鱼】二维码。不买也不要紧,看看也行。

(完)