用几个例子可以很容易地说明正在发生的事情:
通过原型链访问personal_details
var person = { name :"dummy", personal_details: { age : 22, country : "USA" } }
var bob = Object.create(person)
bob.name = "bob"
bob.personal_details.age = 23
输出如下:
console.log( bob );
/// { name :"bob", personal_details: { age : 23, country : "USA" } }
console.log( person )
/// { name :"dummy", personal_details: { age : 23, country : "USA" } }
现在在 person 对象上设置的 Age 23 是通过 bob 的原型链because bob.personal_details
直接引用的。person.personal_details
向下导航对象结构后,您将直接使用该person.personal_details
对象。
用本地属性覆盖原型属性
但是,如果您personal_details
用另一个对象覆盖 bob 的属性,则该原型链接将被更本地的属性覆盖。
bob.personal_details = { a: 123 }
现在输出是:
console.log( bob );
/// { name :"bob", personal_details: { a : 123 } }
console.log( person )
/// { name :"dummy", personal_details: { age : 23, country : "USA" } }
因此,通过访问bob.personal_details
——从现在开始——你引用的是{ a: 123 }
对象而不是 person 的原始{ age : 23, country : "USA" }
对象。bob
所做的所有更改都将发生在该对象上,并且基本上与该对象无关person
。
原型链
为了让事情变得有趣,你认为当你这样做时会发生什么,毕竟以上:
delete bob.personal_details
您最终恢复了原始原型链接person.personal_details
(因为您已删除添加的本地属性),因此控制台日志将显示:
console.log( bob );
/// { name :"bob", personal_details: { age : 23, country : "USA" } }
基本上,JavaScript 引擎将沿着原型链返回,直到找到您在每个原型对象上请求的属性或方法。一个项目设置得越靠前,意味着它将在以后覆盖其他项目。
现在另一个问题,如果您再次触发以下命令会发生什么?
delete bob.personal_details
什么都没有,不再有一个实际的属性分配给 bob 调用personal_details
,delete
只会在当前对象上工作,而不是沿着原型链。
一种不同的看待它的方式
查看原型链如何工作的另一种方法基本上是想象一堆对象。当 JavaScript 扫描特定的属性或方法时,它会向下读取以下结构:
bob : { }
person : { name: 'dummy', personal_details: { age: 22 } }
Object : { toString: function(){ return '[Object object]'; } }
例如,假设我想访问bob.toString
. toString
是一种存在于 JavaScript 基础对象上的方法,Object
它是几乎所有东西的基础原型。当解释器收到对对象上特定方法或属性的读取请求时,它将遵循以下事件链:
- 有
bob
一个属性叫? 不。toString
bob.__proto__
ie是否person
有一个名为 的属性toString
?不。
bob.__proto__.__proto__
ie是否Object
有一个名为 的属性toString
?是的
- 返回参考
function(){ return '[Object object]'; }
一旦到达第 4 点,解释器将返回对toString
在 Object 上找到的方法的引用。如果未在未定义属性Object
的错误上找到该属性,则很可能已被触发(因为它是链中的最后一个)。
现在如果我们以之前的例子为例,这次toString
在 - 上定义一个方法bob
:
bob : { toString: function(){ return '[Bob]'; } }
person : { name: 'dummy', personal_details: { age: 22 } }
Object : { toString: function(){ return '[Object object]'; } }
如果我们再次尝试读取toString
bob 上的方法,这次我们得到:
- 有
bob
一个属性叫? 是的。toString
该过程在第一个障碍处停止并toString
从 bob 返回方法。这意味着bob.toString()
将返回[Bob]
而不是[Object object]
.
正如 Phant0m 所说,对对象的写入请求遵循不同的路径,并且永远不会沿着原型链向下传播。理解这一点是为了弄清楚什么是读请求和什么是写请求之间的区别。
bob.toString --- is a read request
bob.toString = function(){} --- is a write request
bob.personal_details --- is a read request
bob.personal_details = {} --- is a write request
bob.personal_details.age = 123 --- is a read request, then a write request.
最后一项是引起混乱的一项。该过程将遵循以下路线:
- 有
bob
一个属性叫? 不。personal_details
- 有
person
一个属性叫? 是的。personal_details
- 返回
{ age: 22 }
存储在内存中某处的引用。
现在开始一个新的过程,因为对象导航或赋值的每个部分都是对属性或方法的新请求。所以,现在我们有了我们的personal_details
对象,我们切换到一个写请求,因为等号左边的属性或变量=
总是一个赋值。
- 将值写入对象
123
的属性age
{ age: 22 }
所以原始请求可以被看作是这样的:
(bob.personal_details) --- read
(personal_details.age = 123) --- write
如果bob
拥有它自己personal_details
的进程属性将是相同的,但被写入的目标对象将是不同的。
最后...
按照你的问题的思路:
很难理解原型对象上的属性被视为 READ_ONLY,但如果属性是一个对象,那么人们可以掌握它并可以自由修改其属性!我的理解对吗?
原型属性似乎是只读的,但只有在作为继承它们的对象的属性直接访问时——因为这些属性实际上根本不存在于继承对象上。如果您向下导航到原型对象本身,则可以将其视为任何普通对象(具有读取和写入功能),因为这正是它的本质——普通对象。一开始可能会令人困惑,但这是本质或原型继承,它与您如何访问正在使用的属性有关。