From fb3d8cc84ca36b1cba428b26e426d486f0f44adb Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Tue, 1 May 2018 10:25:24 +0800 Subject: [PATCH] 4. --- 4.md | 729 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 4.md diff --git a/4.md b/4.md new file mode 100644 index 0000000..2cfa5c4 --- /dev/null +++ b/4.md @@ -0,0 +1,729 @@ +## 四、数据结构:对象和数组 + +数字、布尔值和字符串构成了基本的数据结构。少了其中任何一样,你可能都很难构造出完整的结构。我们可以使用对象来把值和其他对象组织起来,通过这种手段来构造更为复杂的结构。 + +到目前为止,我们编写的程序当中只对一些简单数据类型进行了操作。在本章中,我们会在程序当中使用一些基本的数据结构。在阅读完本章的内容后,你就可以编写一些实用的程序了。 + +本章会编写一个实际的程序,并在编写的过程中针对一些概念进行讲解。在示例代码中会使用到一些我们之前提到的函数和变量。 + +本书提供了一个在线编码沙箱工具([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/)),读者可以使用该工具在特定章节的环境中运行代码。如果你想在其他环境下运行示例代码,则需要先从网页上下载本章的完整代码。 + +### 松鼠人 + +一般在晚上八点到十点之间,雅克就会变身成为一只毛茸茸的松鼠,尾巴上的毛十分浓密。 + +让雅克感到庆幸的是,他并没有变成狼人,而是变身成为一只松鼠,这可比变身成狼人轻松多了。虽然他不用担心会不小心吃了邻居(这可不太妙),但他需要时刻提防不被邻居家的猫吃掉。有两次他醒来后发现自己躺在橡木树上,赤身裸体而且意识模糊。经过这两次事情之后,他便在晚上锁上自己的房门和窗户,并在地上放些核桃,让自己能在晚上有些事干。 + +![](../Images/00109.jpeg) + +这样,他就不必再去担心被猫吃掉或者在橡树上醒来的尴尬情况了。但是雅克仍要忍受变身成松鼠所带来的不便。由于变身随时都有可能发生,因此他怀疑是否有什么东西促使他发生了变身。有那么一段时间,他觉得只要接触了树就会变身。因此他决定再也不去触碰任何树木,甚至不再接近它们。但问题始终没有得到解决。 + +于是,雅克决定采用一种更为科学的方法来解决变身为松鼠的问题,他开始记录日常做过的每一件事,以及是否变身成松鼠。他希望通过这些数据来缩小触发变身因素的范围。 + +他所做的第一件事便是设计一个数据结构,用来存储这些信息。 + +### 数据集 + +如果要处理这些数字数据,我们需要先找到一种方法在机器的内存中存储这些数据。比如说,我们想表示一组数字:2、3、5、7和11。 + +我们可以用字符串来表示这组数字。毕竟,字符串可以存储成任意长度,因此我们可以用字符串存储很多数据,并使用“235711”来表示这组数字。但问题是,你还得将这些数字从字符串中解析出来,并转换成数字再使用它们。 + +幸运的是,JavaScript提供了一种数据类型,专门用于存储一系列的值。我们将这种数据类型称为数组(array),将一连串的值写在方括号当中,值之间使用逗号(,)分隔。 + +``` +var listOfNumbers = [2, 3, 5, 7, 11]; +console.log(listOfNumbers[2]); +// → 5 +console.log(listOfNumbers[2 - 1]); +// → 3 +``` + +我们同样使用方括号来获取数组当中的值。在表达式后紧跟一对方括号,并在方括号中填写表达式,这将会在左侧表达式里查找方括号中给定的索引所对应的值,并返回结果。 + +数组中第一个元素的索引是0,而非1。因此我们可以使用listOfNumbers[0]来读取数组中的第一个元素。如果之前没有编程经验,那么你可能需要花些时间来适应这种约定。但在计算机技术当中,长久以来都是使用0作为计数的开头,只要大家都遵循这种约定习惯(就像在JavaScript中一样),就不会有任何问题。 + +### 属性 + +我们已经见过一些形迹可疑的表达式,比如前面例子中的用myString.length来获取一个字符串的长度和求最大值的函数Math.max。我们可以通过这些表达式来访问某个值的属性。在第一个例子中,我们访问了myString当中的length属性。在第二个例子中,我们访问Math对象的max属性,其中Math对象是包含数学运算相关值和函数的集合。 + +在JavaScript中,几乎所有的值都有属性。但null和undefined没有。如果你尝试访问null和undefined的属性,会得到一个错误提示。 + +``` +null.length; +// → TypeError: Cannot read property 'length' of null +``` + +在JavaScript中有两种最为常用的访问属性的方法:使用点(.)和方括号[]。value.x和value[x]两种写法都可以访问value的属性,但访问的未必是同一个属性,这取决于JavaScript如何解释x。如果使用点,则点之后的部分必须是一个合法变量名,即直接写属性名称。如果使用方括号,则JavaScript会将方括号中表达式的返回值作为属性名称。value.x获取value中名为x的属性,而values[x]则先计算表达式x的值,并将其计算结果作为属性名称。 + +因此,如果你知道需要获取的属性名为“length”,就可以使用value.length访问属性。如果你想要从变量i中提取属性名称,就要写成value[i]。由于属性名可以是任意字符串,因此如果你访问名为“0”或“John Done”的属性,就必须使用方括号:value[0]或value["John Doe"]。虽然这种情况下属性名称都是已知的,但由于“0”和“John Doe”都不是合法变量名,因此你无法使用点来访问这些属性。 + +数组当中的元素均以属性的方式进行存储。由于这些属性的名称都是数字,我们又常常需要使用变量来获取元素名称,因此我们必须使用方括号来访问这些元素。数组的length属性用于获取数组中的元素的个数,由于该属性名是合法的变量名,而且我也知道这个属性的名称,因此我们可以用array.length来获取数组的长度,因为这样写起来比array["length"]简单。 + +### 方法 + +除了length属性以外,字符串和数组对象还包含了许多其他属性,这些属性是函数值。 + +``` +var doh = "Doh"; +console.log(typeof doh.toUpperCase); +// → function +console.log(doh.toUpperCase()); +// → DOH +``` + +每个字符串都有一个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"] +``` + +我们可以使用push方法向数组的末尾添加值,pop方法则作用相反:删除数组末尾的值并返回给调用者。我们可以使用join方法将字符串数组拼接成单个字符串,join方法的参数是连接数组元素之间的文本内容。 + +### 对象 + +让我们回到松鼠人的问题上来。一系列的日志记录可以用一个数组来表示。但是记录中并不仅仅只包含一个数字或字符串那么简单:每条记录都需要保存一个活动列表和一个布尔值,其中布尔值用来判断雅克是否变身为松鼠。那么理想情况是我们将这些信息组织成一个单一的值,然后将这些组织好的值存入日志的数组中。 + +对象类型的值可以存储任意类型的属性,我们可以随意增删这些属性。一种创建对象的方法是使用大括号。 + +``` +var day1 = { + squirrel: false, + events: ["work", "touched tree", "pizza", "running", + "television"] +}; +console.log(day1.squirrel); +// → false +console.log(day1.wolf); +// → undefined +day1.wolf = false; +console.log(day1.wolf); +// → false +``` + +在大括号中,我们可以添加一系列的属性,并用逗号分隔。每一个属性均以名称开头,紧跟一个冒号,然后是对应属性的表达式。在这里空格和换行符不会产生什么影响。在定义对象时,像上面的示例代码那样分多行定义对象并适当缩进代码,可以提升代码的可读性。如果属性名不是有效的变量名或者数字,则需要使用引号将其括起来。 + +``` +var descriptions = { + work: "Went to work", + "touched tree": "Touched a tree" +}; +``` + +这就意味着在JavaScript中,大括号具有两种含义。如果将大括号放在语句开头,则表示语句块的开头。若放在其他位置,则表示描述对象。不过,把大括号描述的对象放在语句开头没有什么实际用途,而且在一般的程序当中,这两种用法不会出现任何歧义。 + +读取一个不存在的属性就会产生undefined值,比如在上面的示例中,我们第一次尝试读取wolf属性时就返回了undefined。 + +我们可以使用=运算符来给一个属性表达式赋值。如果该属性已经存在,那么这项操作就会替换原有的值。如果该属性不存在,则会在目标对象中新建一个属性。 + +让我们简要回顾一下变量绑定的概念,即触须模型。属性绑定的原理也与其十分类似。属性会引用一些值,而其他变量或属性也可能会引用相同的值。你可以把对象想象成长有任意多触须的章鱼,而每条触须上都刻有一个名称。 + +![](../Images/00116.jpeg) + +delete运算符就像从章鱼身上切除触须。delete是个一元运算符,其操作数是访问属性的表达式,可以从对象中移除指定属性。虽说删除属性的操作很少执行,但我们确实可以执行删除操作。 + +``` +var anObject = {left: 1, right: 2}; +console.log(anObject.left); +// → 1 +delete anObject.left; +console.log(anObject.left); +// → undefined +console.log("left" in anObject); +// → false +console.log("right" in anObject); +// → true +``` + +二元运算符in的第一个操作数是一个表示属性名的字符串,第二个操作数是一个对象,它会返回一个布尔值,表示该对象是否包含该属性。将属性设置为undefined与使用delete删除属性的区别在于:对于第一种情况,对象仍然包含left属性,只不过该属性没有引用任何值;而对于第二种情况,对象中已不存在left属性,因此in运算符会返回false。 + +数组只不过是一种用于存储数据序列的特殊对象,因此typeof[1,2]的执行结果是“object”。可以将数组看成一只长长的、扁平的章鱼,其触手平整地排布在一行上,而每个触须使用数字作为标签。 + +![](../Images/00118.jpeg) + +因此,我们可以用一个数组对象来表示雅克的日志。 + +``` +var 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"], + squirrel: true}, + /* and so on... */ +]; +``` + +### 可变性 + +我们马上就要开始编写真正的程序了。但在此之前,我们还剩最后一些理论知识需要掌握。 + +我们已经知道对象的值是可以进行修改的。而我们在前面的章节中讨论的一些值类型比如:数字、字符串和布尔值都是不可变值,我们无法修改这些类型值的内容。你可以将这些值进行组合,也可以通过这些值来产生新值,但当你创建好一个字符串后,这个值就不能再进行任何修改了。字符串中的文本信息无法修改。如果引用了一个包含“cat”的字符串,你不能修改该字符串当中的任一字符,让字符串改写成“rat”。 + +但对于对象来说,我们可以通过修改其属性来改变对象的内容。 + +当我们拥有两个数字120和120时,我们可以认为这两个数字是相等的,不管怎么说,它们都引用了真实的位序列。但对于对象来说,引用两个相同的对象和两个不同对象中包含相同属性是有区别的。我们来看以下代码: + +``` +var object1 = {value: 10}; +var object2 = object1; +var object3 = {value: 10}; + +console.log(object1 == object2); +// → true +console.log(object1 == object3); +// → false + +object1.value = 15; +console.log(object2.value); +// → 15 +console.log(object3.value); +// → 10 +``` + +变量object1和object2引用了同一个对象,这就是为什么改变object1时,也会改变object2中的值。而object3变量则引用了不同的对象,虽然其中的属性和object1相同,但它们之间却没有任何直接的联系。 + +在JavaScript中,使用==运算符来比较两个对象时,只有两个对象引用了同一个值,结果才会返回true。比较两个不同的对象将会返回false,哪怕对象内容相同。JavaScript中没有内置深度比较运算符(比较对象内容),但你可以自己编写一个(将会作为本章末尾的习题)。 + +### 松鼠人的记录 + +于是,雅克开始了他的JavaScript之旅,并搭建了用于保存每天记录的一套开发环境。 + +``` +var journal = []; + +function addEntry(events, didITurnIntoASquirrel) { + journal.push({ + events: events, + squirrel: didITurnIntoASquirrel + }); +} +``` + +然后,在每天晚上的十点钟或第二天早上,从家中的书柜上爬下来后,他就会记录当天的情况。 + +``` +addEntry(["work", "touched tree", "pizza", "running", + "television"], false); +addEntry(["work", "ice cream", "cauliflower", "lasagna", + "touched tree", "brushed teeth"], false); +addEntry(["weekend", "cycling", "break", "peanuts", + "beer"], true); +``` + +在记录了足够的数据点后,他尝试计算出他的变身与日常事件之间的关联,并想从中获取一些有价值的信息。 + +关联是变量(这里的变量指的是统计学中的变量,而非JavaScript中的变量)之间依赖性的一种度量方式。通常可以使用一个系数(范围从–1~1)来表示。关联为0时,意味着变量之间毫无关系;而关联为1时,表示两个变量之间密切相关。如果你知道了一个变量,就可以知道另一个变量。关联为–1时,则表示两者关系密切,只不过是负相关。若一个是真,另一个就是假。 + +对二元(布尔值)变量而言,phi系数(`ϕ`)提供了一种非常好的关联性度量方式。为了计算`ϕ`,我们需要一张长宽为n的表格,其中包含两个观测变量所有组合情况的出现次数。我们以吃比萨事件为例,并画出如下表格: + +![](../Images/00125.jpeg) + +可以使用以下公式计算`ϕ`,n指的是表格中的数字: + +![](../Images/00126.jpeg) + +符号n<sub class="calibre4">01</sub>表示所有度量值中,第一个度量值(吃比萨)为假(0),而第二个度量值(是否变为松鼠)为真(1)时的数量。在本例中,n<sub class="calibre4">01</sub>的值为4。 + +符号n<sub class="calibre4">1</sub>·则指第一个变量为真时,所有度量值数量之和,在示例表格中值为10。类似地,n·0指第二个变量(是否变成松鼠)为假时,所有度量值的数量之和。 + +因此,我们以比萨表为例,除法线上方的部分(被除数)为1×76–9×4=40,而除法线下面的部分(除数)则是10×80×5×85的平方根,也就是![](../Images/00127.jpeg)。计算结果`ϕ`≈0.069,这个结果很小,因此吃比萨对是否变身成松鼠显然没有太大影响。 + +### 计算关联性 + +我们可以用包含4个元素的数组([76,9,4,1])来表示一张2乘2的表格。我们也可以使用其他表示方式,比如包含两个数组的数组,每个子数组又包含两个元素([[76,9],[4,1]])。也可以使用一个对象,在其中包含一些属性,并将其取名为“11”和“01”。但是,一维数组更为简单,也容易进行操作。我们可以将数组索引看成包含两个二进制位的数字,左边一位(高位)数字表示变量“是否变成松鼠”,右边一位(低位)数字表示事件变量。例如,若二进制数字为10,表示雅克变成了松鼠,但事件并未发生(比如说吃比萨)。这种情况发生了4次。由于二进制数字10的十进制表示法是2,因此我们将其存储到数组中索引为2的位置上。 + +下面这个函数用于计算数组的系数`ϕ`: + +``` +function phi(table) { + return (table[3] * table[0] - table[2] * table[1]) / + Math.sqrt((table[2] + table[3]) * + (table[0] + table[1]) * + (table[1] + table[3]) * + (table[0] + table[2])); +} + +console.log(phi([76, 9, 4, 1])); +// → 0.068599434 +``` + +该函数只是把`ϕ`计算公式转换成了JavaScript语言编写的代码。Math.sqrt函数用于求平方根,该函数属于标准JavaScript环境中Math对象中的属性。由于我们没有将行列之和保存在我们的数据结构中,因此需要通过计算表格中的两个字段来获取像n<sub class="calibre4">1·这样的值。</sub> + +雅克花了三个月的时间记录日志。在本章的代码沙箱([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/))的下载文件中用JOURNAL变量存储了该结果数据集合。 + +若要从这篇记录中提取出某个特定事件的2乘2表格,我们首先需要循环遍历整个记录,并计算出与变身成松鼠相关事件发生的次数。 + +``` +function hasEvent(event, entry) { + return entry.events.indexOf(event) != -1; +} + +function tableFor(event, journal) { + var table = [0, 0, 0, 0]; + for (var i = 0; i < journal.length; i++) { + var entry = journal[i], index = 0; + if (hasEvent(event, entry)) index += 1; + if (entry.squirrel) index += 2; + table[index] += 1; + } + return table; +} + +console.log(tableFor("pizza", JOURNAL)); +// → [76, 9, 4, 1] +``` + +函数hasEvent用于测试某个记录中是否包含某个特定的事件。数组有indexOf方法,可以用于查找指定的值(在本例当中是事件名),如果找到了特定的值则返回当前索引,否则返回–1。所以说,如果indexOf返回的值是–1,则表示该事件在记录中不存在。 + +函数tableFor中的循环体会检查记录中是否包含某个特定事件,并检查发生的事件是否伴随变身成松鼠一起发生,然后计算出表格中相应格子中的数字。每当发生某个特定事件时,就把表格当中对应格子中的数字加1。 + +现在我们手上就有了计算单个关联性的工具函数。我们接下来只需要找出记录中每类事件的相关系数,并查看它们之间的关联程度。我们该如何把计算出来的结果保存起来呢? + +### 对象映射 + +其中一种方法是把所有计算出来的相关系数保存在一个数组当中,每条记录使用一个对象来进行保存,其中包含name和value两个属性。但这么做有一个问题,就是查询某个特定事件的相关系数的操作十分烦琐:你必须循环遍历整个数组,才可以找到你期望的那个包含正确name的对象。虽然我们可以将这个查询过程封装成一个函数,但我们还是要去编写更多的代码,而且对于计算机来说这些操作有些多余。 + +一种更好的解决方法是用事件类型作为对象的属性名称。这样我们就可以使用方括号来创建或读取对应的属性,也可以使用in运算符来检测其中是否包含我们期望的属性。 + +``` +var map = {}; +function storePhi(event, phi) { + map[event] = phi; +} + +storePhi("pizza", 0.069); +storePhi("touched tree", -0.081); +console.log("pizza" in map); +// → true +console.log(map["touched tree"]); +// → -0.081 +``` + +映射表(map)可以通过一个值(在本例中是事件名)来获取对应的另一个值(在本例中是`ϕ`系数)。 + +在这种情况下使用对象会有一些潜在的隐患,我们会在第6章对此进行详细阐述。但就目前来说,我们可以暂且不去关心这些问题。 + +如果想从映射表当中获取所有存储的系数该怎么办呢?与数组的操作方式不同,我们无法预先了解所有的属性名称,因此也就没有办法使用一般的for循环体来遍历整个映射表。JavaScript提供了另一种遍历对象属性的循环语句。它与一般的for循环看起来很像,只是我们使用的关键字不是for而是in。 + +``` +for (var event in map) + console.log("The correlation for '" + event + + "' is " + map[event]); +// → The correlation for 'pizza' is 0.069 +// → The correlation for 'touched tree' is -0.081 +``` + +### 分析结果 + +为了找出数据集中存在的所有事件类型,我们只需依次处理每条记录,然后遍历记录中的所有事件即可。目前,我们把所有遍历的事件相关系数存储在phis对象中。每当phis对象中找不到当前遍历的事件类型时,就计算其相关系数,然后把计算结果添加到对象中。 + +``` +function gatherCorrelations(journal) { + var phis = {}; + for (var entry = 0; entry < journal.length; entry++) { + var events = journal[entry].events; + for (var i = 0; i < events.length; i++) { + var event = events[i]; + if (!(event in phis)) + phis[event] = phi(tableFor(event, journal)); + } + } + return phis; +} + +var correlations = gatherCorrelations(JOURNAL); +console.log(correlations.pizza); +// → 0.068599434 +``` + +让我们来看看计算结果。 + +``` +for (var event in correlations) + console.log(event + ": " + correlations[event]); +// → carrot: 0.0140970969 +// → exercise: 0.0685994341 +// → weekend: 0.1371988681 +// → bread: -0.0757554019 +// → pudding: -0.0648203724 +// and so on... +``` + +绝大多数相关系数都趋近于0。显然,摄入胡萝卜、面包或布丁并不会引发变身成松鼠。但是似乎在周末变身成松鼠的概率更高。让我们来过滤一下结果,看看相关系数大于0.1和小于–0.1的事件。 + +``` +for (var event in correlations) { + var correlation = correlations[event]; + if (correlation > 0.1 || correlation < -0.1) + console.log(event + ": " + correlation); +} +// → weekend: 0.1371988681 +// → brushed teeth: -0.3805211953 +// → candy: 0.1296407447 +// → work: -0.1371988681 +// → spaghetti: 0.2425356250 +// → reading: 0.1106828054 +// → peanuts: 0.5902679812 +``` + +啊哈!这里明显有两项因素的相关系数比其他的高。摄入花生会极大地促进变身成松鼠,而刷牙则正好相反。 + +这太有意思了。让我们再仔细看看这些数据。 + +``` +for (var i = 0; i < JOURNAL.length; i++) { + var entry = JOURNAL[i]; + if (hasEvent("peanuts", entry) && + !hasEvent("brushed teeth", entry)) + entry.events.push("peanut teeth"); +} +console.log(phi(tableFor("peanut teeth", JOURNAL))); +// → 1 +``` + +嗯,错不了!当雅克摄入了一些花生而且不刷牙的话,就会变身成松鼠。要不是因为他这么不注意口腔卫生,也就不至于让他为此烦恼如此之久了。 + +在了解情况之后,雅克再也不吃花生了,因此再也没有变身成松鼠。 + +在此后的一段时间里,雅克的生活一切顺利。但过了几年后他失业了,最后被迫去马戏团谋生。在马戏团,他表演的节目是“不可思议的松鼠人”,每次表演之前他都会在嘴巴上涂满花生酱。终于有一天,受够了这种屈辱的生存方式的雅克再也没能变回人形,他从马戏团帐篷的缝隙中逃了出去,消失在了森林之中。从此,再也没人看到过他。 + +### 详解数组 + +在本章结束之前,我想要介绍一些与对象相关的概念。我们先来介绍一些实用的数组方法。 + +我们在本章的前面已经了解了push和pop方法,分别用于在数组末尾添加或删除元素。相应地,在数组的开头添加或删除元素的方法分别是unshift和shift。 + +``` +var todoList = []; +function rememberTo(task) { + todoList.push(task); +} +function whatIsNext() { + return todoList.shift(); +} +function urgentlyRememberTo(task) { + todoList.unshift(task); +} +``` + +上面的程序用于管理任务列表。你可以调用rememberTo("eat")向列表末尾添加任务。当你准备完成一件任务时,调用whatIsNext()来获取(并删除)列表中的第一项任务。函数urgentlyRemeberTo也可以用来添加任务,只不过是将任务添加到列表的开头而已。 + +有一个与indexOf方法类似的方法叫lastIndexOf,只不过indexOf从数组第一个元素向后搜索,而lastIndexOf从最后一个元素向前搜索。 + +``` +console.log([1, 2, 3, 2, 1].indexOf(2)); +// → 1 +console.log([1, 2, 3, 2, 1].lastIndexOf(2)); +// → 3 +``` + +indexOf和lastIndexOf方法都有一个可选参数,可以用来指定搜索的起始位置。 + +另一个基本方法是slice,该方法接受一个起始索引和一个结束索引,然后返回数组中两个索引范围内的元素。起始索引元素包含在返回结果中,但结束索引元素不会包含在返回结果中。 + +``` +console.log([0, 1, 2, 3, 4].slice(2, 4)); +// → [2, 3] +console.log([0, 1, 2, 3, 4].slice(2)); +// → [2, 3, 4] +``` + +如果没有指定结束索引,slice会返回从起始位置之后的所有元素。对于字符串来说,它也有一个具有相同功能的slice方法供开发人员使用。 + +concat方法用于拼接两个数组,其作用类似于字符串的+运算符。下面的示例展示了如何使用concat和slice方法。该函数接受一个数组与一个索引,并返回原数组移除指定索引处的那个元素后的一个副本。 + +``` +function remove(array, index) { + return array.slice(0, index) + .concat(array.slice(index + 1)); +} +console.log(remove(["a", "b", "c", "d", "e"], 2)); +// → ["a", "b", "d", "e"] +``` + +### 字符串及其属性 + +我们可以调用字符串的length或toUpperCase这样的属性,但不能向字符串中添加任何新的属性。 + +``` +var myString = "Fido"; +myString.myProperty = "value"; +console.log(myString.myProperty); +// → undefined +``` + +字符串、数字和布尔类型的值并不是对象,因此当你向这些值中添加属性时JavaScript并不会报错,但实际上你并没有将这些属性添加进去。这些值都是不可变的,而且无法向其中添加任何属性。 + +但这些类型的值包含一些内置属性。每个字符串中包含了若干方法供我们使用,最有用的方法可能就是slice和indexOf了,它们的功能与数组中的同名方法类似。 + +``` +console.log("coconuts".slice(4, 7)); +// → nut +console.log("coconut".indexOf("u")); +// → 5 +``` + +唯一的区别在于,字符串的indexOf方法可以使用多个字符作为搜索条件,而数组中的indexOf方法则只能搜索单个元素。 + +``` +console.log("one two three".indexOf("ee")); +// → 11 +``` + +trim方法用于删除字符串中开头和结尾的空白符号(空格、换行符和制表符等符号)。 + +``` +console.log(" okay \n ".trim()); +// → okay +``` + +我们已经了解了字符串类型length属性的用法。可以使用charAt方法来获取字符串当中某个特定的字符,当然也可以像数组中那样使用方括号和数字来获取字符串中的字符。 + +``` +var string = "abc"; +console.log(string.length); +// → 3 +console.log(string.charAt(0)); +// → a +console.log(string[1]); +// → b +``` + +### arguments对象 + +每当函数被调用时,就会在函数体的运行环境当中添加一个特殊的变量arguments。该变量指向一个包含了所有入参的对象。在JavaScript中,我们可以传递多于(或少于)函数参数列表定义个数的参数。 + +``` +function noArguments() {} +noArguments(1, 2, 3); // This is okay +function threeArguments(a, b, c) {} +threeArguments(); // And so is this +``` + +arguments对象有一个length属性,表示实际传递给函数的参数个数。每个参数对应一个属性,被命名为0、1、2,依此类推。 + +这样看起来arguments对象很像一个数组。但该对象不包含任何数组方法(比如slice或indexOf),因此在使用arguments对象时会比数组稍微复杂一些。 + +``` +function argumentCounter() { + console.log("You gave me", arguments.length, "arguments."); +} +argumentCounter("Straw man", "Tautology", "Ad hominem"); +// → You gave me 3 arguments. +``` + +有些函数可以接受任意数量的参数,比如console.log。这类函数一般都会遍历arguments对象。这样可以创建非常易用的接口。比如之前雅克创建的日志记录。 + +``` +addEntry(["work", "touched tree", "pizza", "running", + "television"], false); +``` + +由于该函数会被反复多次调用,因此我们可以创建一个更简单的接口来替代现有的接口。 + +``` +function addEntry(squirrel) { + var entry = {events: [], squirrel: squirrel}; + for (var i = 1; i < arguments.length; i++) + entry.events.push(arguments[i]); + journal.push(entry); +} +addEntry(true, "work", "touched tree", "pizza", + "running", "television"); +``` + +这个版本的函数与一般的函数一样读取第一个参数(即squirrel),然后从arguments对象中读取其余的参数(循环索引从1开始,跳过第一个参数),并将这些参数添加到数组中。 + +### Math对象 + +正如我们所看到的那样,Math对象中包含了许多与数字相关的工具函数,比如Math.max(求最大值)、Math.min(求最小值)和Math.sqrt(求平方根)。 + +Math对象简单地把一组相关的功能打包成一个对象供用户使用。全局只有一个Math对象,其对象本身没有什么实际用途。Math对象其实提供了一个“命名空间”,封装了所有的数学运算函数和值,确保这些元素不会变成全局变量。 + +过多的全局变量会对命名空间造成“污染”。全局变量越多,就越有可能一不小心把某些变量的值覆盖掉。比如,我们可能想在程序中使用名为max的变量,由于JavaScript将内置的max函数安全地放置在Math对象中,因此不必担心max的值会被覆盖。 + +当你去定义一个已经被使用的变量名的时候,对于很多编程语言来说,都会阻止你这么做,至少会对这种行为发出警告。但是JavaScript不会,因此要小心这些陷阱。 + +让我们来继续了解Math对象。如果需要做三角运算,Math对象可以帮助到你,它包含cos(余弦)、sin(正弦)、tan(正切)和各自的反函数(acos、asin和atan)。Math.PI则表示数字π(pi),或至少是JavaScript中的数字近似值(在传统的程序设计当中,常量均以大写来标注)。 + +``` +function randomPointOnCircle(radius) { + var angle = Math.random() * 2 * Math.PI; + return {x: radius * Math.cos(angle), + y: radius * Math.sin(angle)}; +} +console.log(randomPointOnCircle(2)); +// → {x: 0.3667, y: 1.966} +``` + +如果你对正弦或余弦不大熟悉,不必担心。我们会在第13章用到它们时,再做进一步解释。 + +在上面的示例代码中使用了Math.random。每次调用该函数时,会返回一个伪随机数,范围在0(包括)~1(不包括)之间。 + +``` +console.log(Math.random()); +// → 0.36993729369714856 +console.log(Math.random()); +// → 0.727367032552138 +console.log(Math.random()); +// → 0.40180766698904335 +``` + +虽然计算机的一切行为都是预先设定好的,只要提供相同的输入,就会得到相同的输出结果。但我们仍然可以通过计算机来产生伪随机数。要产生一个随机数,计算机会在其内部状态中维护一个数字(或一组数字)。接着,每当我们要产生一个随机数时,计算机会根据其内部维护的状态,执行一系列复杂的预定义计算过程,然后返回计算的部分结果作为随机数。在返回计算结果的同时,计算机也会根据结果来改变其内部维护的状态,以便下次产生伪随机数的时候产生不同的结果。 + +如果我们想获取一个随机的整数而非小数,可以使用Math.floor(向下取整到与当前数字最接近的整数)来处理Math.random的结果。 + +``` +console.log(Math.floor(Math.random() * 10)); +// → 2 +``` + +将随机数乘以10可以得到一个在0~10之间的数字。由于Math.floor是向下取整,因此该函数会等概率地取到0~9中的任何一个数字。 + +还有两个函数,分别是Math.ceil(向上取整)和Math.round(四舍五入)。 + +### 全局对象 + +JavaScript全局作用域中有许多全局变量,都可以通过全局对象进行访问。每一个全局变量作为一个属性存储在全局对象当中。在浏览器中,全局对象存储在window变量当中。 + +``` +var myVar = 10; +console.log("myVar" in window); +// → true +console.log(window.myVar); +// → 10 +``` + +### 本章小结 + +对象和数组(一种特殊对象)可以将几个值组合起来形成一个新的值。理论上说,我们可以将一组相关的元素打包成一个对象,并通过这个对象来访问这些元素,以避免管理那些支离破碎的元素。 + +在JavaScript中,除了null和undefined以外,绝大多数的值都含有属性。我们可以用value.propName或value["propName"]的方式来访问属性。对象使用名称来定义和存储一定数量的属性。另外,数组中通常会包含不同数量的值,并使用数字(从0开始)作为这些值的属性。 + +在数组中有一些具名属性,比如length和一些方法。方法是作为属性存在的函数,常常作用于其所属的值。 + +对象可以用来当作映射表,将名称与值关联起来。我们可以使用in运算符确定对象中是否包含特定名称的属性。我们同样可以在for循环中(for(var name in object))使用关键字in来遍历对象中包含的属性。 + +### 习题 + +#### 特定范围数字求和 + +在本书的前言中,提到过一种很好的计算固定范围内数字之和的方法: + +``` +console.log(sum(range(1, 10))); +``` + +编写一个range函数,接受两个参数:start和end,然后返回包含start到end(包括end)之间的所有数字。 + +接着,编写一个sum函数,接受一个数字数组,并返回所有数字之和。运行上面的程序,检查一下结果是不是55。 + +附加题是修改range函数,接受第3个可选参数,指定构建数组时的步数(step)。如果没有指定步数,构建数组时,每步按1增长,和旧函数行为一致。调用函数range(1,10,2),应该返回[1,3,5,7,9]。另外确保步数值为负数时也可以正常工作,因此range(5,2,-1)应该产生[5,4,3,2]。 + +``` +// Your code here. + +console.log(range(1, 10)); +// → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +console.log(range(5, 2, -1)); +// → [5, 4, 3, 2] +console.log(sum(range(1, 10))); +// → 55 +``` + +#### 逆转数组 + +数组有一个reverse方法,它可以逆转数组中元素的次序。在本题中,编写两个函数,reverseArray和reverseArrayInPlace。第一个函数reverseArray接受一个数组作为参数,返回一个新数组,并逆转新数组中的元素次序。第二个函数reverseArrayInPlace与第一个函数的功能相同,但是直接将数组作为参数进行修改来,逆转数组中的元素次序。两者都不能使用标准的reverse方法。 + +回想一下,在上一章中关于副作用和纯函数的讨论,哪个函数的写法的应用场景更广?哪个的执行效率会更高? + +``` +// Your code here. + +console.log(reverseArray(["A", "B", "C"])); +// → ["C", "B", "A"]; +var arrayValue = [1, 2, 3, 4, 5]; +reverseArrayInPlace(arrayValue); +console.log(arrayValue); +// → [5, 4, 3, 2, 1] +``` + +#### 实现列表 + +对象作为一个值的容器,它可以用来构建各种各样的数据结构。有一种通用的数据结构叫作列表(list)(不要与数组混淆)。列表是一种嵌套对象集合,第一个对象拥有第二个对象的引用,而第二个对象有第三个对象的引用,依此类推。 + +``` +var list = { + value: 1, + rest: { + value: 2, + rest: { + value: 3, + rest: null + } + } +}; +``` + +最后产生的对象形成了一条链,如下图所示: + +![](../Images/00158.jpeg) + +使用列表的一个好处是,它们之间可以共享相同的子列表。举个例子,如果我们新建了两个值:{value:0,result:list}和{value:-1,result:list}(list引用了我们前面定义的变量)。这是两个独立的列表,但它们之间却共享了同一个数据结构,该数据结构包含列表末尾的三个元素。而且我们前面定义的list仍然是包含三个元素的列表。 + +编写一个函数arrayToList,当给定参数[1,2,3]时,建立一个和前面示例相似的数据结构。然后编写一个listToArray函数,将列表转换成数组。再编写一个工具函数prepend,接受一个元素和一个列表,然后创建一个新的列表,将元素添加到输入列表的开头。最后编写一个函数nth,接受一个列表和一个数,并返回列表中指定位置的元素,如果该元素不存在则返回undefined。 + +如果你觉得这都不是什么难题,那么编写一个递归版本的nth函数。 + +``` +// Your code here. + +console.log(arrayToList([10, 20])); +// → {value: 10, rest: {value: 20, rest: null}} +console.log(listToArray(arrayToList([10, 20, 30]))); +// → [10, 20, 30] +console.log(prepend(10, prepend(20, null))); +// → {value: 10, rest: {value: 20, rest: null}} +console.log(nth(arrayToList([10, 20, 30]), 1)); +// → 20 +``` + +#### 深度比较 + +==运算符可以判断对象是否相等。但有些时候,你希望比较的是对象中实际属性的值。 + +编写一个函数deepEqual,接受两个参数,若两个对象是同一个值或两个对象中有相同属性,且使用deepEqual比较属性值均返回true时,返回true。 + +为了通过类型(使用===运算符)或其属性比较出两个值是否完全相同,可以使用typeof运算符。如果对两个值使用typeof均返回“object”,则说明你应该进行深度比较。但需要考虑一个例外的情况:由于历史原因,typeof null也会返回“object”。 + +``` +// Your code here. + +var obj = {here: {is: "an"}, object: 2}; +console.log(deepEqual(obj, obj)); +// → true +console.log(deepEqual(obj, {here: 1, object: 2})); +// → false +console.log(deepEqual(obj, {here: {is: "an"}, object: 2})); +// → true +```