diff --git a/4.md b/4.md index 2cfa5c4..21eaa91 100644 --- a/4.md +++ b/4.md @@ -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[1,2]的执行结果是“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”。