在之前的关于属性的章节里,我们介绍了夏洛克-福尔摩斯移动到马里布的谜题。但是我们还没有解释它。
打开你的涂鸦板或者 app,现在,我们将一起逐步画一张图出来 检查你的思维模式。
尽管你之前可能已经尝试过了,但额外的练习不会有什么害处的!在这一章的最后,我们将会讨论在这个例子之后的一个大命题。
我们这样声明变量:
let sherlock = {
surname: 'Holmes',
address: { city: 'London' }
};
画出这一步的图
画出答案之前不要往下翻了
...
...
...
...
...
你的图应该是如下这样:
sherlock
变量指向了一个对象。这个对象有两个属性。surname
属性指向了一个 "Holmes"
的字符串值。address
属性指向了另一个对象。那个对象只有一个叫做 city
的属性。那个属性指向了 "London"
这个字符串值。
再仔细观察一下我画这幅图的流程:
你对这个流程熟悉吗?
注意我们在这讨论的不是一个,而是两个完全分开的对象。两对大括号意味着两个对象。
对象可能在代码里看起来是“嵌套”的,但是我们的宇宙里,每个对象都是完全地分开的。一个对象不能在另外一个对象的“里面”
如果你仍然认为对象是嵌套的,现在请尝试丢开这个想法。
在这一步,我们定义另外一个变量:
let john = {
surname: 'Watson',
address: sherlock.address
};
别往下翻了,除非你做出来了
...
...
...
接下来的图应该这样:
我们同样声明了 john
变量。它指向了一个有两个属性的对象。它的 address
属性指向的值和 sherlock.address
已经指向的值一样。它的 surname
属性指向的是 "Watson"
这个字符串值。
来看看细节:
你做的一样吗?
当你看到 address: sherlock.address
的时候,很容易想到 John
的 address
属性指向了 Sherlock
的 address
属性。
这是错误的。
记住:一个属性永远指向一个值!它不能指向其他的属性或者变量。一般的,我们的宇宙里,所有的线都指向的是值
当你看见 address: sherlock.address
的时候,我们必须先找出 sherlock.address
的值,然后将 address
这个属性的线指到这个值上面去。只有值本身是重要的,而不是我们如何寻找它(sherlock.address)。
结果,两个不同对象各自的 address
属性都指向了同一个对象,你能在图中找出它们吗?
现在,让我们回忆我们例子中的最后一步。
John 受够了这里,他对伦敦的毛毛雨感到厌倦。他决定换一个名字,并搬到马里布。我们通过改变一些属性的值来做到这个:
john.surname = 'Lennon';
john.address.city = 'Malibu';
我们怎样用图表示呢?
画出图之前不要往下滚动
...
...
...
你的图应该是这样:
john
变量指向的对象的 surname
属性现在指向了 "Lennon"
字符串值。更有趣的是,sherlock
和 john
的 address
的 city
属性现在都有了一个新的字符串值,都指向了 "Malibu"
字符串值。
这个奇怪的位置错乱,将夏洛克和约翰都带到了马里布。顺着这些图中的线,我们可以轻易确认下面这个正确答案:
console.log(sherlock.surname); // "Holmes"
console.log(sherlock.address.city); // "Malibu"
console.log(john.surname); // "Lennon"
console.log(john.address.city); // "Malibu"
这是我再这最后一步做的变动:
我们找出了这些线,这些值,然后将线指向了新的值。
可变性是“改变”的另一种说法。
举个例子,我们可以说我们改变了一个对象的属性,或者我们会说我们使这个对象发生了改变(以及它的属性)。这是一回事
人们爱说“可变性”是因为这个词有个阴险的隐喻。它提醒你需要额外的注意。这不意味着可变性是“坏的” —— 这只是程序的一部分!—— 但是你还是需要非常关注它。
让我们回顾一下最初的任务。我们希望给约翰一个新的名字,并且将他移动到马里布。现在看看我们做出的两个变化:
john.surname = 'Lennon';
john.address.city = 'Malibu';
我们到底改变了哪些对象?
第一行改变的是 john
指向的对象 —— 没错, 它的 surname
属性。这是合理的:事实上,我们就是要改变约翰的名字。这个对象表示了约翰的数据。所以我们改变它的 surname
属性。
然而,第二行做了一些不同的事。它改变的不是 john
所指的对象。它改变的完全是另外一个对象 —— 我们可以通过 john.address
找到的对象。如果我们看看图,我们知道这个对象通过 sherlock.address
也能达到!
在程序中到处都去改变对象的话,我们会得到一坨很混乱的东西
一个修复的方法是避免去改变被共享的对象:
// Replace Step 3 with this code:
john.surname = 'Lennon';
john.address = { city: 'Malibu' };
第二行的差异可能不那么明显,但是它非常重要。
当我们使用 john.address.city = "Malibu"
的时候,线的左边是 john.address.city
。我们是改变了通过 john.address
到达的对象中的 city
属性。但是通过 sherlock.address
同样能到达这个对象。结果就是,我们不小心改变了共享数据。
通过 john.address = { city: 'Malibu' }
的话,左边的线是 john.address
。我们改变的是 john
指向的对象中的 address
属性。换句话说,我们只改变了代表约翰的数据的对象。这也是为什么 sherlock.address.city
保持不变的原因。
如你所见,看起来差不多的代码可能造成非常不一样的结果。请特别注意在赋值操作中左边的部分的线指向的是哪里!
还有另外一种方法是可以使得 john.address.city
返回 "Malibu"
,并且 sherlock.address.city
仍然返回 "London"
。
// Replace Step 3 with this code:
john = {
surname: 'Lennon',
address: { city: 'Malibu' }
};
在这,我们不再去改变约翰的对象。相反,我们给 john
变量指向了一个“新版本”的数据。现在开始, john
指向了另一个对象,它的 address
指向了一个全新的对象。
你可能注意到现在图中有一个“废弃”了的老版本的约翰的对象。我们不用担心它。JavaScript 最终会自动地将它从内存中移除,如果没有任何线指向它的话。
注意着两个方法都可以满足我们的需求:
console.log(sherlock.surname); // "Sherlock"
console.log(sherlock.address.city); // "London"
console.log(john.surname); // "Lennon"
console.log(john.address.city); // "Malibu"
比较一下这两幅图。你会对那种方法有所偏爱?或者说,你觉得它们分别有什么好处或不好的地方?
夏洛克-福尔摩斯说过:“当你消除了不可能的事情,那么剩下的事情无论多不可能,都将是真相。”
当你的思维模式越来越完善,你会发现将会更容易去 debug 问题,因为你会知道问题的原因可能出在哪里,并去找到它。
举个例子,如果你知道 sherlock.address.city
在运行默写代码之后发生了改变,我们的图里会提供三种解释:
- 可能
sherlock
变量被重新赋值了 - 可能通过
sherlock
到达的对象呗改变了,并且它的address
属性被设置成了其他的东西。 - 可能通过
sherlock.address
到达的对象发生了改变,它的city
这个属性被设置成了新的值。
你的思维模式给你一个调查 bug 的起点。 反过来说依然成立 有时候,你可以告诉别人一段代码不是问题的根源 —— 因为思维模式证明了它!
所以,如果我们将 john
变量指向另外一个对象,我们可以非常确信 sherlock.address.city
不会被改变。我们的图展示出当改变 john
的线的时候不会影响到 sherlock
:
在你不是夏洛克——福尔摩斯之前,仍然需要保持注意力,这样你就可以对一些事情更有信心。它会和你的思维模式一样好。思维模式会帮助我们认识这些理论,但是你需要进行一些实验,你可以通过 console.log
或者 debugger
来去人你的想法。
你需要知道你可以使用 const
关键词来替换 let
:
const shrek = { species: 'ogre' };
const
关键词让你创建一个只读变量 —— 也就是常量。当我们声明一个常量,我们不能将它指向另外的值:
shrek = fiona; // TypeError
但还是有些差别。 我们仍然可以改变 const 声明的对象:
shrek.species = 'human';
console.log(shrek.species); // 'human'
在这个例子里,shrek
变量自己的线是只读的(const)。它指向一个对象 —— 而这个对象的属性是可变的!
const
的作用是一个备受争议的话题。有写人喜欢禁止 let
,只使用 const
。另外一些会说程序员应该相信自己对变量的重新赋值。无论你偏爱哪种,记住 const
智能防止变量被重新赋值 —— 而不是对象的可变性。
我想让你确认你没有走到认为可变性是“坏的”这条路上去。那可能会是一种懒惰的过分简化,从而掩盖了真正地理解。如果数据通过实践改变,可变性将会发生在某些地方。问题在于什么,哪里,什么时间发生了改变。这同样是一个有争议的话题。
可变性是“远处的诡异动作”。改变 john.address.city
让 console.log(sherlock.address.city)
打印出了另外的东西。
在你改变对象的时候,变量和属性可能已经指向了一些东西。你的改变让代码“遵循”最后的线。
这既不是好的也不是不好的。可变性使得一些数据可以很轻易的改变并且可以立即在代码中“看”到这个改变。但是,滥用可变性将会导致程序发生难以预测的事情。
有一些学校认为可变性最好值被包含在你的应用中一些很小的范围里。这造成的负面效果就是你会需要些更多的模板代码来“让数据流转”。但是按照哲学层面的好处,你的程序行为将会更好地被预测。
值得注意的是,对于刚创建的对象进行改变是没问题的,因为这时候还没有其他的线指过来。另外,我建议你应该非常注意你改变了什么,以及什么时候。你对可变性的依赖取决于你的应用的架构。
- 对象在我们的宇宙里不会被“嵌套”
- 更加注意赋值的时候左边的线是什么。
- 改变对象也叫做对对象作出改变
- 如果你改变了一个对象,你的代码将会让所有通过线连到这个对象的变量“看到”这个改变。有时候,这可能是你想要得。然而,可变性在共享数据间的发生往往会导致 bug。
- 改变一个刚创建的对象是安全的。广义地说,你对可变性的依赖取决于你应用的架构。如果你不想使用过多,你应该花点时间去了解它如何工作的。
- 你可以通过
const
来代替let
声明一个变量。这让你可以保证这个变量永远指向同一个值。但是记住const
不会防止对象的可变性!