1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-24 04:22:20 +00:00
This commit is contained in:
wizardforcel 2018-05-01 11:12:05 +08:00
parent fb3d8cc84c
commit b2f72cf56b

129
4.md
View File

@ -1,111 +1,110 @@
## 四、数据结构:对象和数组
数字、布尔值和字符串构成了基本的数据结构。少了其中任何一样,你可能都很难构造出完整的结构。我们可以使用对象来把值和其他对象组织起来,通过这种手段来构造更为复杂的结构。
数字,布尔和字符串是构建数据结构的原子。 不过,许多类型的信息都需要多个原子。 对象允许我们将值(包括其他对象)放到一起,来构建更复杂的结构。
到目前为止,我们编写的程序当中只对一些简单数据类型进行了操作。在本章中,我们会在程序当中使用一些基本的数据结构。在阅读完本章的内容后,你就可以编写一些实用的程序了
我们迄今为止构建的程序,受到一个事实的限制,它们仅在简单数据类型上运行。 本章将介绍基本的数据结构。 到最后,你会知道足够多的东西,开始编写有用的程序
本章会编写一个实际的程序,并在编写的过程中针对一些概念进行讲解。在示例代码中会使用到一些我们之前提到的函数和变量。
本书提供了一个在线编码沙箱工具([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/)),读者可以使用该工具在特定章节的环境中运行代码。如果你想在其他环境下运行示例代码,则需要先从网页上下载本章的完整代码。
本章将着手于一个或多或少的实际编程示例,当概念适用于手头问题时引入它们。 示例代码通常基于本文前面介绍的函数和绑定。
### 松鼠人
一般在晚上八点到十点之间,雅克就会变身成为一只毛茸茸的松鼠,尾巴上的毛十分浓密。
让雅克感到庆幸的是,他并没有变成狼人,而是变身成为一只松鼠,这可比变身成狼人轻松多了。虽然他不用担心会不小心吃了邻居(这可不太妙),但他需要时刻提防不被邻居家的猫吃掉。有两次他醒来后发现自己躺在橡木树上,赤身裸体而且意识模糊。经过这两次事情之后,他便在晚上锁上自己的房门和窗户,并在地上放些核桃,让自己能在晚上有些事干
一方面,雅克非常高兴他没有变成经典的狼人。 与变成狼相比,变成松鼠的确会产生更少的问题。 他不必担心偶然吃掉邻居(那会很尴尬),而是担心被邻居的猫吃掉。 他在橡木树冠上的一个薄薄的树枝上醒来,赤身裸体并迷失方向。在这两次偶然之后,他在晚上锁上了房间的门窗,并在地板上放了几个核桃,来使自己忙起来
![](../Images/00109.jpeg)
这就解决了猫和树的问题。 但雅克宁愿完全摆脱他的状况。 不规律发生的变形使他怀疑,它们可能会由某种东西触发。 有一段时间,他相信只有在他靠近橡树的日子里才会发生。 但是避开橡树不能阻止这个问题。
这样,他就不必再去担心被猫吃掉或者在橡树上醒来的尴尬情况了。但是雅克仍要忍受变身成松鼠所带来的不便。由于变身随时都有可能发生,因此他怀疑是否有什么东西促使他发生了变身。有那么一段时间,他觉得只要接触了树就会变身。因此他决定再也不去触碰任何树木,甚至不再接近它们。但问题始终没有得到解决
雅克切换到了更科学的方法,开始每天记录他在某一天所做的每件事,以及他是否变形。 有了这些数据,他希望能够缩小触发变形的条件
于是,雅克决定采用一种更为科学的方法来解决变身为松鼠的问题,他开始记录日常做过的每一件事,以及是否变身成松鼠。他希望通过这些数据来缩小触发变身因素的范围。
他所做的第一件事便是设计一个数据结构,用来存储这些信息。
他需要的第一个东西,是存储这些信息的数据结构。
### 数据集
如果要处理这些数字数据我们需要先找到一种方法在机器的内存中存储这些数据。比如说我们想表示一组数字2、3、5、7和11。
为了处理大量的数字数据,我们首先必须找到一种方法,将其在我们的机器内存中表示。 举例来说,我们想要表示一组数字 2, 3, 5, 7 和 11。
我们可以用字符串来表示这组数字。毕竟字符串可以存储成任意长度因此我们可以用字符串存储很多数据并使用“235711”来表示这组数字。但问题是你还得将这些数字从字符串中解析出来并转换成数字再使用它们。
我们可以用字符串来创建 - 毕竟,字符串可以有任意长度,所以我们可以把大量数据放入它们中,并使用`"2 3 5 7 11"`作为我们的表示。 但这很笨拙。 你必须以某种方式提取数字,并将它们转换回数字才能访问它们。
幸运的是JavaScript提供了一种数据类型专门用于存储一系列的值。我们将这种数据类型称为数组array将一连串的值写在方括号当中值之间使用逗号分隔。
```
var listOfNumbers = [2, 3, 5, 7, 11];
let listOfNumbers = [2, 3, 5, 7, 11];
console.log(listOfNumbers[2]);
// → 5
console.log(listOfNumbers[0]);
// → 2
console.log(listOfNumbers[2 - 1]);
// → 3
```
我们同样使用方括号来获取数组当中的值。在表达式后紧跟一对方括号,并在方括号中填写表达式,这将会在左侧表达式里查找方括号中给定的索引所对应的值,并返回结果。
数组中第一个元素的索引是0而非1。因此我们可以使用listOfNumbers[0]来读取数组中的第一个元素。如果之前没有编程经验那么你可能需要花些时间来适应这种约定。但在计算机技术当中长久以来都是使用0作为计数的开头只要大家都遵循这种约定习惯就像在JavaScript中一样就不会有任何问题
数组的第一个索引是零,而不是一。 所以第一个元素用`listOfNumbers[0]`获取。 基于零的计数在技术上有着悠久的传统,并且在某些方面意义很大,但需要一些时间来习惯。 将索引看作要跳过的项目数量,从数组的开头计数
### 属性
我们已经见过一些形迹可疑的表达式比如前面例子中的用myString.length来获取一个字符串的长度和求最大值的函数Math.max。我们可以通过这些表达式来访问某个值的属性。在第一个例子中我们访问了myString当中的length属性。在第二个例子中我们访问Math对象的max属性其中Math对象是包含数学运算相关值和函数的集合
在之前的章节中,我们已经看到了一些可疑的表达式,例如`myString.length`(获取字符串的长度)和`Math.max`(最大值函数)。 这些表达式可以访问某个值的属性。 在第一个中,我们访问`myString`中的`length`属性。 第二个中,我们访问`Math`对象(它是数学相关常量和函数的集合)中的名为`max`的属性
在JavaScript中几乎所有的值都有属性。但null和undefined没有。如果你尝试访问null和undefined的属性会得到一个错误提示。
```
null.length;
// → TypeError: Cannot read property 'length' of null
// → TypeError: null has no properties
```
在JavaScript中有两种最为常用的访问属性的方法:使用点(.)和方括号[]。value.x和value[x]两种写法都可以访问value的属性但访问的未必是同一个属性这取决于JavaScript如何解释x。如果使用点则点之后的部分必须是一个合法变量名即直接写属性名称。如果使用方括号则JavaScript会将方括号中表达式的返回值作为属性名称。value.x获取value中名为x的属性而values[x]则先计算表达式x的值并将其计算结果作为属性名称。
在JavaScript中访问属性的两种主要方式是点(`.`)和方括号(`[]`)。 `value.x``value [x]`都可以访问`value`属性,但不一定是同一个属性。 区别在于如何解释`x`。 使用点时,点后面的单词是该属性的字面名称。 使用方括号时,会求解括号内的表达式来获取属性名称。 鉴于`value.x`获取`value`的名为`x`的属性,`value [x]`尝试求解表达式`x`,并将结果转换为字符串作为属性名称。
因此如果你知道需要获取的属性名为“length”就可以使用value.length访问属性。如果你想要从变量i中提取属性名称就要写成value[i]。由于属性名可以是任意字符串因此如果你访问名为“0”或“John Done”的属性就必须使用方括号value[0]或value["John Doe"]。虽然这种情况下属性名称都是已知的但由于“0”和“John Doe”都不是合法变量名因此你无法使用点来访问这些属性
所以如果你知道你感兴趣的属性叫做`color`,那么你会写`value.color`。 如果你想提取属性由绑定`i`中保存的值命名,你可以写`value [i]`。 属性名称是字符串。 它们可以是任何字符串,但点符号仅适用于看起来像有效绑定名的名称。 所以如果你想访问名为`2``John Doe`的属性,你必须使用方括号:`value[2]``value["John Doe"]`
数组当中的元素均以属性的方式进行存储。由于这些属性的名称都是数字我们又常常需要使用变量来获取元素名称因此我们必须使用方括号来访问这些元素。数组的length属性用于获取数组中的元素的个数由于该属性名是合法的变量名而且我也知道这个属性的名称因此我们可以用array.length来获取数组的长度因为这样写起来比array["length"]简单。
数组中的元素以数组属性的形式存储,使用数字作为属性名称。 因为你不能用点号来表示数字,并且通常想要使用一个保存索引的绑定,所以你必须使用括号来表达它们。
数组的`length`属性告诉我们它有多少个元素。 这个属性名是一个有效的绑定名,我们事先知道它的名字,所以为了得到一个数组的长度,通常写`array.length`,因为它比`array["length"]`更容易编写。
### 方法
除了length属性以外字符串和数组对象还包含了许多其他属性这些属性是函数值
`length`属性之外,字符串和数组对象都包含一些持有函数值的属性
```
var doh = "Doh";
let doh = "Doh";
console.log(typeof doh.toUpperCase);
// → function
console.log(doh.toUpperCase());
// → DOH
```
每个字符串都有一个toUpperCase属性。调用该属性会返回当前字符串的一个副本并将副本当中的所有字母都转换成大写字母。字符串也有一个toLowerCase属性你应该知道这个属性具体是做什么的
每个字符串都有`toUpperCase`属性。 调用时,它将返回所有字母转换为大写字符串的副本。 另外还有`toLowerCase`
有趣的是虽然我们没有在调用toUpperCase时传递任何参数但该函数访问了字符串“Doh”即被调用的属性所属的值。我们会在第6章中阐述这其中的原理。
我们通常将包含函数的属性称为某个值的方法method。比如说“toUpperCase是字符串的一个方法”。
以下示例代码展示了数组对象的一些方法
此示例演示了两种方法,可用于操作数组
```
var mack = [];
mack.push("Mack");
mack.push("the", "Knife");
console.log(mack);
// → ["Mack", "the", "Knife"]
console.log(mack.join(" "));
// → Mack the Knife
console.log(mack.pop());
// → Knife
console.log(mack);
// → ["Mack", "the"]
let sequence = [1, 2, 3];
sequence.push(4);
sequence.push(5);
console.log(sequence);
// → [1, 2, 3, 4, 5]
console.log(sequence.pop());
// → 5
console.log(sequence);
// → [1, 2, 3, 4]
```
我们可以使用push方法向数组的末尾添加值pop方法则作用相反删除数组末尾的值并返回给调用者。我们可以使用join方法将字符串数组拼接成单个字符串join方法的参数是连接数组元素之间的文本内容。
`push`方法将值添加到数组的末尾,而`pop`方法则相反,删除数组中的最后一个值并将其返回。
这些有点愚蠢的名字是栈的传统术语。 编程中的栈是一种数据结构,它允许您将值推入并按相反顺序再次弹出,最后添加的内容首先被移除。 这些在编程中很常见 - 您可能还记得前一章中的函数调用栈,它是同一个想法的实例。
### 对象
让我们回到松鼠人的问题上来。一系列的日志记录可以用一个数组来表示。但是记录中并不仅仅只包含一个数字或字符串那么简单:每条记录都需要保存一个活动列表和一个布尔值,其中布尔值用来判断雅克是否变身为松鼠。那么理想情况是我们将这些信息组织成一个单一的值,然后将这些组织好的值存入日志的数组中。
回到松鼠人的示例。 一组每日的日志条目可以表示为一个数组。 但是这些条目并不仅仅由一个数字或一个字符串组成 - 每个条目需要存储一系列活动和一个布尔值,表明雅克是否变成了松鼠。 理想情况下,我们希望将它们组合成一个值,然后将这些分组的值放入日志条目的数组中。
对象类型的值可以存储任意类型的属性,我们可以随意增删这些属性。一种创建对象的方法是使用大括号
对象类型的值是任意的属性集合。 创建对象的一种方法是使用大括号作为表达式
```
var day1 = {
let day1 = {
squirrel: false,
events: ["work", "touched tree", "pizza", "running",
"television"]
events: ["work", "touched tree", "pizza", "running"]
};
console.log(day1.squirrel);
// → false
@ -116,29 +115,29 @@ console.log(day1.wolf);
// → false
```
在大括号中,我们可以添加一系列的属性,并用逗号分隔。每一个属性均以名称开头,紧跟一个冒号,然后是对应属性的表达式。在这里空格和换行符不会产生什么影响。在定义对象时,像上面的示例代码那样分多行定义对象并适当缩进代码,可以提升代码的可读性。如果属性名不是有效的变量名或者数字,则需要使用引号将其括起来
大括号内有一列用逗号分隔的属性。 每个属性都有一个名字,后跟一个冒号和一个值。 当一个对象写为多行时,像这个例子那样,对它进行缩进有助于提高可读性。 名称不是有效绑定名称或有效数字的属性必须加引号
```
var descriptions = {
let descriptions = {
work: "Went to work",
"touched tree": "Touched a tree"
};
```
就意味着在JavaScript中大括号具有两种含义。如果将大括号放在语句开头则表示语句块的开头。若放在其他位置则表示描述对象。不过把大括号描述的对象放在语句开头没有什么实际用途而且在一般的程序当中这两种用法不会出现任何歧义
意味着大括号在 JavaScript 中有两个含义。 在语句的开头,他们起始了一个语句块。 在任何其他位置,他们描述一个对象。 幸运的是,语句很少以花括号对象开始,因此这两者之间的不明确性并不是什么大问题
读取一个不存在的属性就会产生undefined值比如在上面的示例中我们第一次尝试读取wolf属性时就返回了undefined
读取一个不存在的属性就会产生`undefined`
我们可以使用=运算符来给一个属性表达式赋值。如果该属性已经存在,那么这项操作就会替换原有的值。如果该属性不存在,则会在目标对象中新建一个属性。
让我们简要回顾一下变量绑定的概念,即触须模型。属性绑定的原理也与其十分类似。属性会引用一些值,而其他变量或属性也可能会引用相同的值。你可以把对象想象成长有任意多触须的章鱼,而每条触须上都刻有一个名称
简要回顾我们的绑定的触手模型 - 属性绑定也类似。 他们捕获值,但其他绑定和属性可能会持有这些相同的值。 你可以将对象想象成有任意数量触手的章鱼,每个触手上都有一个名字的纹身
![](../Images/00116.jpeg)
delete运算符就像从章鱼身上切除触须。delete是个一元运算符其操作数是访问属性的表达式可以从对象中移除指定属性。虽说删除属性的操作很少执行但我们确实可以执行删除操作
`delete`运算符切断章鱼的触手。 这是一个一元运算符,当应用于对象属性时,将从对象中删除指定的属性。 这不是一件常见的事情,但它是可能的
```
var anObject = {left: 1, right: 2};
let anObject = {left: 1, right: 2};
console.log(anObject.left);
// → 1
delete anObject.left;
@ -150,24 +149,38 @@ console.log("right" in anObject);
// → true
```
二元运算符in的第一个操作数是一个表示属性名的字符串第二个操作数是一个对象它会返回一个布尔值表示该对象是否包含该属性。将属性设置为undefined与使用delete删除属性的区别在于对于第一种情况对象仍然包含left属性只不过该属性没有引用任何值而对于第二种情况对象中已不存在left属性因此in运算符会返回false
当应用于字符串和对象时,二元`in`运算符会告诉您该对象是否具有名称为它的属性。 将属性设置为`undefined`,和实际删除它的区别在于,在第一种情况下,对象仍然具有属性(它只是没有有意义的值),而在第二种情况下属性不再存在,`in`会返回`false`
数组只不过是一种用于存储数据序列的特殊对象因此typeof[12]的执行结果是“object”。可以将数组看成一只长长的、扁平的章鱼其触手平整地排布在一行上而每个触须使用数字作为标签。
![](../Images/00118.jpeg)
因此,我们可以用一个数组对象来表示雅克的日志。
为了找出对象具有的属性,可以使用`Object.keys`函数。 你给它一个对象,它返回一个字符串数组 - 对象的属性名称。
```
var journal = [
console.log(Object.keys({x: 0, y: 0, z: 2}));
// → ["x", "y", "z"]
```
`Object.assign`函数可以将一个对象的所有属性复制到另一个对象中。
```
let objectA = {a: 1, b: 2};
bject.assign(objectA, {b: 3, c: 4});
console.log(objectA);
// → {a: 1, b: 3, c: 4}
```
然后,数组只是一种对象,专门用于存储对象序列。 如果你求解`typeof []`,它会产生`object`。 你可以看到它们是长而平坦的章鱼,它们的触手整齐排列,并以数字标记。
我们将雅克的日记表示为对象数组。
```
let journal = [
{events: ["work", "touched tree", "pizza",
"running", "television"],
squirrel: false},
{events: ["work", "ice cream", "cauliflower",
"lasagna", "touched tree", "brushed teeth"],
squirrel: false},
{events: ["weekend", "cycling", "break",
"peanuts", "beer"],
{events: ["weekend", "cycling", "break", "peanuts",
"beer"],
squirrel: true},
/* and so on... */
];
@ -175,7 +188,7 @@ var journal = [
### 可变性
我们马上就要开始编写真正的程序了。但在此之前,我们还剩最后一些理论知识需要掌握
我们现在即将开始真正的编程。 首先还有一个理论要理解
我们已经知道对象的值是可以进行修改的。而我们在前面的章节中讨论的一些值类型比如数字、字符串和布尔值都是不可变值我们无法修改这些类型值的内容。你可以将这些值进行组合也可以通过这些值来产生新值但当你创建好一个字符串后这个值就不能再进行任何修改了。字符串中的文本信息无法修改。如果引用了一个包含“cat”的字符串你不能修改该字符串当中的任一字符让字符串改写成“rat”。