数字、布尔值和字符串是构建数据结构的原子。不过,许多类型的信息需要不止一个原子。对象允许我们对值(包括其他对象)进行分组,从而构建更复杂的结构。到目前为止,我们所构建的程序都受到限制,因为它们只能在简单的数据类型上运行。在本章中学习了数据结构的基础知识后,你就可以开始编写有用的程序了。
本章将通过一个或多或少现实的编程示例,介绍适用于当前问题的概念。示例代码通常以本书前面介绍的函数和绑定为基础。
松鼠
每隔一段时间,通常是晚上 8 点到 10 点之间,雅克就会发现自己变成了一只毛茸茸的小啮齿动物,尾巴上还长满了毛。一方面,雅克很庆幸自己没有典型的变狼症。与变成狼相比,变成松鼠带来的问题确实要少一些。他不用担心不小心吃掉邻居(那就尴尬了),而是担心被邻居家的猫吃掉。有两次,雅克在橡树树冠上一根岌岌可危的细树枝上醒来,浑身赤裸,神志不清,后来他就习惯了晚上锁上房间的门窗,在地板上放几个核桃,让自己忙起来。
但雅克更希望完全摆脱这种状况。变身的不规则发生让他怀疑可能是由什么东西引发的。有一段时间,他认为只有在靠近橡树的日子里才会发生这种情况。然而,避开橡树并不能解决问题。雅克转而采用一种更科学的方法,他开始每天记录自己在某一天所做的每一件事,以及自己是否改变了形态。他希望通过这些数据缩小引发变形的条件范围。
他首先需要一个数据结构来存储这些信息。
数据集
要处理一大块数字数据,我们首先要找到一种在机器内存中表示它的方法。比如说,我们想表示一组数字 2、3、5、7 和 11。我们可以创造性地使用字符串--毕竟字符串可以有任意长度,因此我们可以在字符串中放入大量数据--然后使用 “2 3 5 7 11 ”来表示。但这样做很笨拙。我们必须以某种方式提取数字并将其转换为数字才能访问它们。
幸运的是,JavaScript 提供了一种专门用于存储数值序列的数据类型。这种数据类型被称为数组,其写法是在方括号之间用逗号分隔的值列表。
查找数组内部元素的符号也使用方括号。在一个表达式后面紧跟一对方括号,方括号内还有另一个表达式,这样就可以查找左侧表达式中与括号内表达式给出的索引相对应的元素。
数组的第一个索引是 0,而不是 1,因此第一个元素用 listOfNumbers[0] 来检索。基于零的计数法在技术领域有着悠久的传统,在某些方面非常合理,但需要一些时间来适应。将索引视为要跳过的项数,从数组的起点开始计数。
属性
我们在过去的章节中见过一些表达式,如 myString.length(获取字符串长度)和 Math.max(最大值函数)。这些表达式访问某个值的属性。在第一种情况下,我们访问 myString 中值的length属性。在第二种情况中,我们访问的是 Math 对象(它是数学常量和函数的集合)中名为 max 的属性。
几乎所有 JavaScript 值都有属性。null 和undefined是例外。如果尝试访问这些非值的属性,就会出现错误:
JavaScript 中访问属性的两种主要方式是使用点和方括号。value.x 和 value[x] 都可以访问 value 上的一个属性,但不一定是同一个属性。两者的区别在于如何解释 x。
- 使用点时,点后面的单词是属性的字面名称。
- 使用方括号时,括号之间的表达式将被求值,以获得属性名称。
value.x 获取名为 “x ”的值的属性,而 value[x] 则获取名为 x 的变量的值,并将其转换为字符串作为属性名。
如果你知道你感兴趣的属性叫做 color,你就会说 value.color。如果要提取由绑定 i 中的值命名的属性,则需要输入 value[i]。属性名是字符串。它们可以是任何字符串,但点符号只适用于看起来像有效绑定名称的名称--以字母或下划线开头,且只包含字母、数字和下划线。如果要访问名为 2 或 John Doe 的属性,必须使用方括号:value[2] 或 value[“John Doe”]。
数组中的元素存储为数组的属性,使用数字作为属性名。由于数字不能使用圆点符号,而且通常要使用保存索引的绑定,因此必须使用括号符号来获取它们。
就像字符串一样,数组也有一个length属性,告诉我们数组有多少个元素。
方法
除了length属性外,字符串和数组值都包含一些存放函数值的属性。
每个字符串都有一个 toUpperCase 属性。调用时,它会返回一个字符串副本,其中所有字母都已转换为大写。还有一个 toLowerCase 属性,与之相反。有趣的是,虽然调用 toUpperCase 时没有传递任何参数,但函数却可以访问字符串 “Doh”,也就是我们调用的属性值。我们将在第 6 章中了解其工作原理。
包含函数的属性通常被称为其所属值的方法,如 “toUpperCase 是字符串的一个方法”。
本例演示了两种可以用来操作数组的方法。
push 方法将值添加到数组的末尾。pop 方法则相反,删除数组中的最后一个值并将其返回。
这些有点愚蠢的名称是对堆栈进行操作的传统术语。在编程中,堆栈是一种数据结构(先进后出),它允许你以相反的顺序将数值推入堆栈并弹出,这样最后加入的数值就会被先移除。堆栈在编程中很常见,你可能还记得上一章中的函数调用堆栈,它也是相同思想的一个实例。
对象
回到松鼠。一组每日日志条目可以表示为一个数组,但这些条目并不仅仅由数字或字符串组成--每个条目都需要存储一个活动列表和一个布尔值,该布尔值表示 Jacques 是否变成了松鼠。理想情况下,我们希望将这些值组合成一个单一值,然后将这些组合值放入日志条目的数组中。
对象类型的值是属性的任意集合。创建对象的一种方法是使用大括号作为表达式。
在大括号内,你可以写出一个用逗号分隔的属性列表。每个属性都有一个名称,后面跟一个冒号和一个值。当一个对象写了多行时,按本示例所示缩进有助于提高可读性。名称不是有效绑定名称或有效数字的属性必须加引号:
这意味着大括号在 JavaScript 中具有两种含义。在语句的开头,它们表示一个语句块的开始。而在其他任何位置,它们则描述一个对象。幸运的是,用大括号中的对象来开始语句的情况很少,因此这两者之间的歧义并不是什么大问题。唯一会出现这种情况的是,当你想从一个速记箭头函数中返回一个对象时--你不能写 n => {prop: n},因为大括号会被解释为函数体。相反,你必须在对象周围加上一组括号,以明确它是一个表达式。
读取一个不存在的属性会得到undefined的值。可以使用 = 操作符为属性表达式赋值。如果该属性已经存在,则会替换该属性的值,如果不存在,则会在对象上创建一个新属性。
简单地回到我们的绑定触手模型--属性绑定与此类似。它们掌握值,但其他绑定和属性也可能掌握这些相同的值。你可以把对象想象成拥有任意数量触手的章鱼,每个触手上都写着一个名字。
删除操作符切断了章鱼的触角。它是一个一元操作符,当应用于对象属性时,将从对象中删除指定的属性。这种操作并不常见,但却是可能的。
二进制 in 运算符在应用于字符串和对象时,会告诉你该对象是否具有该名称的属性。将属性设置为 undefined 与实际删除属性之间的区别在于,在第一种情况下,对象仍然拥有该属性(只是没有非常有趣的值),而在第二种情况下,属性不再存在,in 将返回 false。
要想知道一个对象有哪些属性,可以使用 Object.keys 函数。给该函数一个对象,它将返回一个字符串数组--对象的属性名称:
有一个 Object.assign 函数可以将一个对象的所有属性复制到另一个对象中:
因此,数组只是一种专门用于存储事物序列的对象。如果对 typeof [] 进行运算,就会产生 “对象”。你可以把数组想象成又长又扁的章鱼,所有的触角都整齐地排成一行,并标上数字。
雅克将用对象数组来表示雅克保存的日记:
可变性
我们很快就会开始实际编程,但首先还要了解一个理论。
我们看到,对象的值是可以修改的。前面几章讨论过的数值类型,如数字、字符串和布尔值,都是不可改变的--不可能改变这些类型的值。你可以将它们组合起来并从中导出新的值,但是当你获取一个特定的字符串值时,该值将始终保持不变。其中的文本无法更改。如果您有一个包含 “cat ”的字符串,其他代码不可能改变字符串中的某个字符,使其拼写成 “rat”。
对象的工作原理则不同。您可以更改对象的属性,从而使单个对象的值在不同时间具有不同的内容。
当我们有 120 和 120 这两个数字时,无论它们是否指向相同的物理位,我们都可以认为它们是相同的数字。对于对象,有两个对同一对象的引用与有两个包含相同属性的不同对象是不同的。请看下面的代码:
object1 和 object2 绑定掌握的是同一个对象,这就是为什么改变 object1 也会改变 object2 的值。可以说它们具有相同的身份。绑定 object3 指向一个不同的对象,它最初包含与 object1 相同的属性,但过着独立的生活。
绑定(即变量的定义方式)可以是可变的(使用 let
)或恒定的(使用 const
),但这与绑定的值是否可以改变是不同的。使用 let
定义的变量可以改变其值,而 const
定义的对象虽然不能重新绑定,但对象的内部属性可以改变。简单来说,const
保证了变量不会指向其他对象,但对象的内容可以变化。
使用 JavaScript 的 == 运算符比较对象时,它是通过同一性进行比较的:只有当两个对象的值完全相同时,它才会返回 true。比较不同的对象将返回 false,即使它们具有相同的属性。JavaScript 中并没有内置按内容比较对象的 “深层 ”比较操作,但可以自己编写(这是本章结尾的练习之一)。
狼人日志
雅克启动了 JavaScript 解释器,并设置了保存日志所需的环境:
请注意,添加到日志中的对象看起来有点奇怪。它没有声明事件:events 这样的属性,而只是给出了一个属性名:events。这是一种速记,意思是一样的--如果在括号符号中的属性名称后面没有值,那么它的值就取自同名的绑定。(译者:events和squirrel会变成对象的属性)
每天晚上 10 点--有时是第二天早上,雅克从书柜顶层爬下来后,记录一天的工作:
一旦有了足够的数据点,他就打算用统计学的方法找出这些事件中哪些可能与松鼠相关。
相关性是衡量统计变量之间依赖关系的一种方法。统计变量与编程变量不太一样。在统计学中,通常会有一组测量值,每个变量在每次测量时都会被测量。变量之间的相关性通常用-1 到 1 之间的数值表示。零相关性表示变量之间没有关系。相关性为 1 表示两者完全相关--如果你知道其中一个,你也就知道了另一个。负 1 也表示变量之间完全相关,但却是对立的--当其中一个为真时,另一个就是假的。
要计算两个布尔变量之间的相关性,我们可以使用 phi 系数 (ϕ )。这是一个公式,其输入是一个频率表,其中包含观察到变量不同组合的次数。公式的输出是一个介于 -1 和 1 之间的数字,用来描述相关性。
我们可以把 “吃披萨 ”这一事件放在这样一个频率表中,每个数字表示在我们的测量中该组合出现的次数。
如果我们将该表称为 n 表,就可以用下面的公式计算出 ϕ:
(如果此时你正放下书本,专注于回味 10 年级数学课的可怕情景--请稍等一下!我并不想用无穷无尽的神秘符号来折磨你--现在只有一个公式。即使是这个公式,我们所要做的也只是将它转化为 JavaScript)。
符号 n01 表示第一个变量(squirrelness)为假(0),第二个变量(pizza)为真(1)的测量次数。在披萨表中,n01 是 9。
n1- 指的是第一个变量为真时的所有测量值的总和,在示例表中为 5。同样,n-0 指的是第二个变量为假的所有测量值的总和。
因此,在披萨表中,除法线以上的部分(红利)是 1×76-4×9 = 40,除法线以下的部分(除数)是 5×85×10×80 的平方根,即 √340,000。得出的结果是 ϕ ≈ 0.069,这是一个很小的数字。吃披萨似乎对变换没有影响。
计算相关性
在 JavaScript 中,我们可以用一个四元素数组([76, 9, 4, 1])来表示一个二乘二的表格。我们还可以使用其他表示方法,例如包含两个二元素数组的数组([[76, 9], [4, 1]]),或者包含属性名称(如 “11 ”和 “01”)的对象,但平面数组非常简单,而且访问表格的表达式也非常简短。我们将数组的索引解释为两位二进制数,其中最左边(最显著)的数字指的是松鼠变量,最右边(最不显著)的数字指的是事件变量。例如,二进制数 10 表示雅克确实变成了松鼠,但事件(比如 “披萨”)没有发生。这种情况发生了四次。由于二进制数 10 在十进制中表示为 2,我们将把这个数字存储在数组的索引 2 中。
这个函数就是从这样的数组中计算出 ϕ 系数:
这是 ϕ 公式在 JavaScript 中的直接转换。Math.sqrt 是平方根函数,由标准 JavaScript 环境中的 Math 对象提供。我们必须从表格中添加两个字段才能得到 n1- 这样的字段,因为行或列的总和并没有直接存储在我们的数据结构中。
雅克写了三个月的日记。由此产生的数据集可在本章的编码沙盒(存储在 JOURNAL 绑定中)和可下载文件中找到。
要从日志中提取特定事件的二乘二表,我们必须循环查看所有条目,并统计该事件与松鼠变换相关的出现次数:
数组有一个包含方法,用于检查数组中是否存在给定值。函数使用该方法来确定其感兴趣的事件名称是否是给定日期的事件列表的一部分。
tableFor 循环的主体通过检查条目是否包含感兴趣的特定事件,以及该事件是否与松鼠事件同时发生,来确定每个日志条目属于表中的哪个方框。然后,循环会在表中正确的方框中添加一条。
现在我们已经掌握了计算单个相关性所需的工具。剩下的唯一步骤就是为记录的每一类事件找到相关性,看看是否有什么特别之处。
循环数组
在 tableFor 函数中,有一个这样的循环:
这种循环在经典 JavaScript 中很常见,每次一个元素地遍历数组是经常出现的情况,要做到这一点,你需要在数组的长度上运行一个计数器,然后依次挑出每个元素。
在现代 JavaScript 中,有一种更简单的方法来编写这种循环:
当 for 循环在变量定义后使用 of 时,它将循环遍历 of 后面给定值的元素。这不仅适用于数组,也适用于字符串和其他一些数据结构。我们将在第 6 章讨论其工作原理。
最终分析
我们需要计算数据集中每一类事件的相关性。为此,我们首先需要找到每种类型的事件。
通过在事件数组中添加任何未包含在其中的事件名称,该函数可以收集所有类型的事件。
使用该函数,我们可以看到所有相关性:
大多数相关性似乎接近于零。吃胡萝卜、面包或布丁显然不会引发松鼠厌食症。这种转变似乎更多发生在周末。让我们过滤一下结果,只显示大于 0.1 或小于-0.1 的相关性:
啊哈!有两个因素的相关性明显强于其他因素。吃花生对变成松鼠的几率有很强的正向影响,而刷牙则有明显的负向影响。
有意思 我们来试试看。
这是一个强有力的结果。这种现象恰恰发生在雅克吃花生又不刷牙的时候。如果他不是一个不注意牙齿卫生的人,他甚至永远都不会注意到自己的痛苦。
了解到这一点后,雅克完全停止了吃花生,并发现自己的变身停止了。但仅仅过了几个月,他就发现这种完全像人类的生活方式少了点什么。没有了野外探险,雅克几乎感觉不到自己还活着。他决定还是做一只全职的野生动物。他在森林里建起了一座漂亮的小树屋,并为它配备了花生酱分配器和长达十年的花生酱供应,然后他最后一次改变了自己的形态,过上了松鼠短暂而充满活力的生活。
更多数组方法
在结束本章之前,我想再向大家介绍一些与对象相关的概念。首先,我将介绍一些常用的数组方法。
在本章前面,我们介绍了在数组末尾添加和删除元素的 push 和 pop 方法。在数组的起始位置添加和删除元素的相应方法称为 unshift 和 shift。
该程序管理一个任务队列。您可以通过调用 remember(“groceries”) 将任务添加到队列的末尾,当您准备好做某事时,您可以调用 getTask() 从队列中获取(并移除)前面的项目。rememberUrgently 函数也会添加任务,但会将其添加到队列的前面而不是后面。
要搜索特定值,数组提供了 indexOf 方法。该方法从开始到结束搜索数组,并返回找到所请求值的索引,如果没有找到,则返回-1。如果想从末尾而不是起点开始搜索,还有一个类似的方法,叫做 lastIndexOf:
indexOf 和 lastIndexOf 都需要一个可选的第二个参数,用于指示从哪里开始搜索。
另一个基本的数组方法是 slice,它接受开始和结束索引,并返回一个只包含这两个索引之间元素的数组。开始索引是包容性的,结束索引是排他性的(左开右闭)。
如果没有给出结束索引,slice 将拷贝起始索引后的所有元素。也可以省略起始索引,复制整个数组。
concat 方法可用于将数组追加到一起以创建一个新数组,类似于 + 操作符对字符串的作用。
下面的示例展示了 concat 和 slice 的操作。它接收一个数组和一个索引,并返回一个新数组,该数组是原始数组的副本,但删除了给定索引处的元素:
如果您传递的 concat 参数不是数组,那么该值将被添加到新数组中,就像它是一个单元素数组一样。
字符串及其属性
我们可以从字符串值中读取 length 和 toUpperCase 等属性。但如果我们尝试添加一个新属性,它却不会粘连。
字符串、数字和布尔类型的值不是对象,虽然语言不会抱怨您试图为它们设置新属性,但实际上并不存储这些属性。如前所述,这些值是不可变的,不能更改。
但这些类型确实有内置属性。每个字符串值都有许多方法。其中一些非常有用的方法是 slice 和 indexOf,它们类似于同名的数组方法:
其中一个区别是,字符串的 indexOf 可以搜索包含多个字符的字符串,而相应的数组方法只能搜索单个元素:
trim方法会删除字符串开头和结尾的空白(空格、换行符、制表符和类似字符):
上一章中的 zeroPad 函数也是一种方法。该函数名为 padStart,参数为所需长度和填充字符:
您可以用 split 将一个字符串拆分到另一个字符串的每一次出现上,然后再用 join 将其连接起来:
使用 repeat 方法可以重复字符串,该方法会创建一个新字符串,其中包含多个原始字符串的副本,并将其粘合在一起:
我们已经了解了字符串类型的length属性。访问字符串中的单个字符就像访问数组元素一样(我们将在第 5 章讨论一个复杂的问题)。
可变参数
函数可以接受任意数量的参数。例如,Math.max 计算所有参数的最大值。要编写这样的函数,可以在函数的最后一个参数前加上三个点,就像这样:
调用此类函数时,其余参数会绑定到一个包含所有其他参数的数组。如果在 rest 之前还有其他参数,它们的值就不在数组中。在 max 中,当它是唯一的参数时,它将包含所有参数。
您可以使用类似的三点符号来调用一个包含参数数组的函数。
这样就将数组 “分散 ”到函数调用中,将其元素作为单独的参数传递。在 max(9, ...numbers, 2) 中,可以将数组与其他参数一起包含在内。
方括号数组符号同样允许三点运算符将另一个数组扩散到新数组中:
即使在大括号对象中也是如此,它会添加另一个对象的所有属性。如果一个属性被多次添加,则以最后添加的值为准:
数学对象
正如我们所见,Math 是一个与数字相关的实用函数包,例如 Math.max(最大值)、Math.min(最小值)和 Math.sqrt(平方根)。
Math 对象被用作一个容器,用于组合一系列相关功能。只有一个 Math 对象,而且它几乎从不作为值使用。相反,它提供了一个命名空间,这样所有这些函数和数值就不必进行全局绑定。
过多的全局绑定会 “污染 ”命名空间。占用的名称越多,就越有可能意外覆盖某些现有绑定的值。例如,你可能想在某个程序中将某个函数命名为 max。由于 JavaScript 内置的 max 函数被安全地隐藏在数学对象中,因此您不必担心会覆盖它。
许多语言都会在你定义绑定时使用一个已被使用的名称来阻止你,或至少警告你。JavaScript 会对使用 let 或 const 声明的绑定进行阻止,但相反,它不会对标准绑定或使用 var 或 function 声明的绑定进行阻止。
回到数学对象。如果你需要计算三角函数,Math 可以帮上忙。它包含 cos(余弦)、sin(正弦)和 tan(正切),以及它们的反函数 acos、asin 和 atan。数字 π(圆周率)--或至少是适合 JavaScript 数字的最接近的近似值--可作为 Math.PI 使用。有一个古老的编程传统,就是用大写字母书写常量值的名称。
如果您不熟悉正弦和余弦,不用担心。我将在第 14 章中解释如何使用它们。
上一个示例使用了 Math.random。这是一个每次调用都会返回一个介于 0(包含)和 1(不包含)之间的新伪随机数的函数:
虽然计算机是确定性机器--如果输入相同的内容,它们总是会做出相同的反应--但让它们产生看似随机的数字还是有可能的。为此,计算机会保留一些隐藏值,每当你要求一个新的随机数时,它就会对这个隐藏值进行复杂的计算,从而产生一个新值。它存储一个新值,并返回从该值派生出来的某个数字。这样,它就能以一种看似随机的方式不断产生新的、难以预测的数字。
如果我们想要一个整数而不是小数的随机数,可以在 Math.random 的结果上使用 Math.floor(四舍五入到最接近的整数):
将随机数乘以 10,得到一个大于或等于 0 且小于 10 的数字。由于 Math.floor 会向下舍入,因此这个表达式会以相同的概率产生 0 到 9 之间的任何数字。
还有函数 Math.ceil(表示 “上限”,向上舍入到整数)、Math.round(舍入到最接近的整数)和 Math.abs,后者取一个数的绝对值,即否定负值而保留正值。
解构
让我们回到 phi 函数上来。
这个函数读起来很别扭的一个原因是,我们有一个指向数组的绑定,但我们更希望有数组元素的绑定--也就是说,让 n00 = table[0] 等等。幸运的是,JavaScript 中有一种简洁的方法可以做到这一点:
这也适用于使用 let、var 或 const 创建的绑定。如果知道要绑定的值是一个数组,可以使用方括号 “查看 ”该值的内部,绑定其内容。
类似的技巧也适用于对象,使用大括号代替方括号。
请注意,如果尝试对 null 或 undefined 进行重组,就会出现错误,就像直接尝试访问这些值的属性一样。
可选属性访问
如果不能确定给定值是否产生了一个对象,但仍想读取它的一个属性,可以使用点符号的变体:object?.property.
当 a 不是 null 或 undefined 时,表达式 a?.b 的含义与 a.b 相同。当 a 为 null 或 undefined 时,它的值为 undefined。在示例中,如果您不确定某个属性是否存在,或者某个变量可能持有 undefined 的值,使用这种表达式会很方便。
在方括号或括号前面加上?
JSON
由于属性是对其值的把握,而不是将其包含在内,因此对象和数组在计算机内存中是以比特序列的形式存储的,这些比特序列包含了其内容的地址--在内存中的位置。一个数组的内部包含另一个数组,其中(至少)一个内存区域用于存储内部数组,另一个内存区域用于存储外部数组,其中(除其他外)包含一个代表内部数组地址的数字。
如果你想将数据保存在文件中备用,或通过网络发送到另一台计算机,就必须以某种方式将这些杂乱无章的内存地址转换成可以存储或发送的描述。我想,你可以将整个计算机内存连同你感兴趣的值的地址一起发送过去,但这似乎不是最好的办法。
我们可以做的是将数据序列化。这意味着将数据转换成平面描述。一种流行的序列化格式叫做 JSON(发音为 “Jason”),是 JavaScript Object Notation 的缩写。它作为一种数据存储和通信格式在网络上被广泛使用,即使是 JavaScript 之外的语言也不例外。
JSON 与 JavaScript 写数组和对象的方式类似,但有一些限制。所有属性名都必须用双引号括起来,而且只允许使用简单的数据表达式,不允许函数调用、绑定或任何涉及实际计算的内容。JSON 中不允许有注释。
日志条目用 JSON 数据表示时可能是这样的:
JavaScript 提供了 JSON.stringify 和 JSON.parse 函数,用于将数据转换为这种格式。第一个函数接收一个 JavaScript 值并返回一个 JSON 编码字符串。第二个函数接收这样一个字符串,并将其转换为它所编码的值:
总结
对象和数组提供了将多个值组合成一个值的方法。这样,我们就可以把一堆相关的东西放在一个袋子里,然后拿着这个袋子到处跑,而不用把所有的东西都包起来,试图分别抓住它们。
JavaScript 中的大多数值都有属性,但 null 和 undefined 值除外。使用 value.prop 或 value[“prop”] 访问属性。对象倾向于使用名称来表示属性,并或多或少存储一组固定的属性。而数组通常包含不同数量的概念上相同的值,并使用数字(从 0 开始)作为属性名称。
数组中有一些已命名的属性,如length和一些方法。方法是存在于属性中的函数,(通常)作用于作为属性的值。
练习
范围之和
本书的导言中提到了下面这种计算一系列数字之和的好方法:
- 编写一个 range 函数,接收两个参数 start 和 end,并返回一个数组,其中包含从 start 到 end(包括 end)的所有数字。
代码:
function range(start, end) {
if (end < start) {
return [-1];
}
let result = [];
for (let i = start; i <= end; i++) {
result.push(i);
}
return result;
}
- 接着,编写一个求和函数,接收一个数字数组并返回这些数字的和。运行示例程序,看看是否确实返回 55。
代码:
function sum(arr) {
let result = 0;
for (let i = 0; i < arr.length; i++) {
result += arr[i];
}
return result;
}
console.log(sum(range(1, 5)));
- 作为附加作业,修改 range 函数,使其包含一个可选的第三个参数,该参数表示构建数组时使用的 “步长 ”值。如果没有给定步长,元素将以 1 为增量向上移动,这与以前的行为一致。函数调用 range(1, 10, 2) 应返回 [1, 3, 5, 7, 9]。确保这也适用于负步长值,这样 range(5, 2, -1) 将产生 [5, 4, 3, 2]。
代码:
function range(start, end, step = 1) {
let result = [];
if (end < start) {
for (let i = start; i >= end; i = i + step) {
result.push(i);
}
} else {
for (let i = start; i <= end; i = i + step) {
result.push(i);
}
}
return result;
}
反转数组
数组有一个反转方法,可以通过颠倒元素出现的顺序来改变数组。在本练习中,请编写两个函数 reverseArray 和 reverseArrayInPlace。
第一个函数 reverseArray 将数组作为参数,并产生一个新数组,该数组中的元素以相反的顺序排列。
第二种是 reverseArrayInPlace,它的作用与 reverse 方法相同:通过反转数组中的元素来修改作为其参数的数组。两者都不能使用标准的 reverse 方法。
回想上一章中关于副作用和纯函数的说明,你认为哪种变体在更多情况下有用?哪个运行得更快?
代码:循环交换
function reverseArray(arr) {
let tempArr = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
tempArr[i] = arr[arr.length - 1 - i];
}
return tempArr;
}
指针控制,数组解构
function reverse2(arr) {
let left = 0;
let right = arr.length - 1;
while (left < right) {
[arr[left], arr[right]] = [arr[right], arr[left]];
left++;
right--;
}
return arr;
}
console.log(reverse2([1, 2, 3]));
列表
作为通用的值块,对象可用于构建各种数据结构。常见的数据结构是列表(不要与数组混淆)。列表是一组嵌套的对象,第一个对象持有第二个对象的引用,第二个对象持有第三个对象的引用,以此类推:
由此产生的对象形成一个链,如下图所示:
列表的一个好处是,它们可以共享部分结构。例如,如果我创建两个新值 {value: 0, rest: list} 和 {value: -1, rest: list}(其中 list 指的是前面定义的绑定),它们都是独立的列表,但它们共享构成最后三个元素的结构。原始列表仍然是一个有效的三元素列表。
- 请编写一个 arrayToList 函数,当给定 [1, 2, 3] 作为参数时,它可以建立如图所示的列表结构。
代码:
function arrayToList(arr) {
if (arr.length == 0) {
return null;
}
return { value: arr.shift(), rest: arrayToList(arr) };
}
- 同时编写一个 listToArray 函数,从 list 生成一个数组。
代码:
- 添加辅助函数 prepend 和 nth,前者接收一个元素和一个列表,并创建一个新的列表,将该元素添加到输入列表的前面;后者接收一个列表和一个数字,并返回列表中给定位置的元素(0 表示第一个元素),如果没有这样的元素,则返回未定义的元素。
如果还没有,也可以编写 nth 的递归版本。