1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
wizardforcel b2f72cf56b 4.
2018-05-01 11:12:05 +08:00

37 KiB
Raw Blame History

四、数据结构:对象和数组

数字,布尔和字符串是构建数据结构的原子。 不过,许多类型的信息都需要多个原子。 对象允许我们将值(包括其他对象)放到一起,来构建更复杂的结构。

我们迄今为止构建的程序,受到一个事实的限制,它们仅在简单数据类型上运行。 本章将介绍基本的数据结构。 到最后,你会知道足够多的东西,开始编写有用的程序。

本章将着手于一个或多或少的实际编程示例,当概念适用于手头问题时引入它们。 示例代码通常基于本文前面介绍的函数和绑定。

松鼠人

一般在晚上八点到十点之间,雅克就会变身成为一只毛茸茸的松鼠,尾巴上的毛十分浓密。

一方面,雅克非常高兴他没有变成经典的狼人。 与变成狼相比,变成松鼠的确会产生更少的问题。 他不必担心偶然吃掉邻居(那会很尴尬),而是担心被邻居的猫吃掉。 他在橡木树冠上的一个薄薄的树枝上醒来,赤身裸体并迷失方向。在这两次偶然之后,他在晚上锁上了房间的门窗,并在地板上放了几个核桃,来使自己忙起来。

这就解决了猫和树的问题。 但雅克宁愿完全摆脱他的状况。 不规律发生的变形使他怀疑,它们可能会由某种东西触发。 有一段时间,他相信只有在他靠近橡树的日子里才会发生。 但是避开橡树不能阻止这个问题。

雅克切换到了更科学的方法,开始每天记录他在某一天所做的每件事,以及他是否变形。 有了这些数据,他希望能够缩小触发变形的条件。

他需要的第一个东西,是存储这些信息的数据结构。

数据集

为了处理大量的数字数据,我们首先必须找到一种方法,将其在我们的机器内存中表示。 举例来说,我们想要表示一组数字 2, 3, 5, 7 和 11。

我们可以用字符串来创建 - 毕竟,字符串可以有任意长度,所以我们可以把大量数据放入它们中,并使用"2 3 5 7 11"作为我们的表示。 但这很笨拙。 你必须以某种方式提取数字,并将它们转换回数字才能访问它们。

幸运的是JavaScript提供了一种数据类型专门用于存储一系列的值。我们将这种数据类型称为数组array将一连串的值写在方括号当中值之间使用逗号分隔。

let listOfNumbers = [2, 3, 5, 7, 11];
console.log(listOfNumbers[2]);
// → 5
console.log(listOfNumbers[0]);
// → 2
console.log(listOfNumbers[2 - 1]);
// → 3

我们同样使用方括号来获取数组当中的值。在表达式后紧跟一对方括号,并在方括号中填写表达式,这将会在左侧表达式里查找方括号中给定的索引所对应的值,并返回结果。

数组的第一个索引是零,而不是一。 所以第一个元素用listOfNumbers[0]获取。 基于零的计数在技术上有着悠久的传统,并且在某些方面意义很大,但需要一些时间来习惯。 将索引看作要跳过的项目数量,从数组的开头计数。

属性

在之前的章节中,我们已经看到了一些可疑的表达式,例如myString.length(获取字符串的长度)和Math.max(最大值函数)。 这些表达式可以访问某个值的属性。 在第一个中,我们访问myString中的length属性。 第二个中,我们访问Math对象(它是数学相关常量和函数的集合)中的名为max的属性。

在JavaScript中几乎所有的值都有属性。但null和undefined没有。如果你尝试访问null和undefined的属性会得到一个错误提示。

null.length;
// → TypeError: null has no properties

在JavaScript中访问属性的两种主要方式是点.)和方括号([])。 value.xvalue [x]都可以访问value属性,但不一定是同一个属性。 区别在于如何解释x。 使用点时,点后面的单词是该属性的字面名称。 使用方括号时,会求解括号内的表达式来获取属性名称。 鉴于value.x获取value的名为x的属性,value [x]尝试求解表达式x,并将结果转换为字符串作为属性名称。

所以如果你知道你感兴趣的属性叫做color,那么你会写value.color。 如果你想提取属性由绑定i中保存的值命名,你可以写value [i]。 属性名称是字符串。 它们可以是任何字符串,但点符号仅适用于看起来像有效绑定名的名称。 所以如果你想访问名为2John Doe的属性,你必须使用方括号:value[2]value["John Doe"]

数组中的元素以数组属性的形式存储,使用数字作为属性名称。 因为你不能用点号来表示数字,并且通常想要使用一个保存索引的绑定,所以你必须使用括号来表达它们。

数组的length属性告诉我们它有多少个元素。 这个属性名是一个有效的绑定名,我们事先知道它的名字,所以为了得到一个数组的长度,通常写array.length,因为它比array["length"]更容易编写。

方法

length属性之外,字符串和数组对象都包含一些持有函数值的属性。

let doh = "Doh";
console.log(typeof doh.toUpperCase);
// → function
console.log(doh.toUpperCase());
// → DOH

每个字符串都有toUpperCase属性。 调用时,它将返回所有字母转换为大写字符串的副本。 另外还有toLowerCase

有趣的是虽然我们没有在调用toUpperCase时传递任何参数但该函数访问了字符串“Doh”即被调用的属性所属的值。我们会在第6章中阐述这其中的原理。

我们通常将包含函数的属性称为某个值的方法method。比如说“toUpperCase是字符串的一个方法”。

此示例演示了两种方法,可用于操作数组:

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方法则相反,删除数组中的最后一个值并将其返回。

这些有点愚蠢的名字是栈的传统术语。 编程中的栈是一种数据结构,它允许您将值推入并按相反顺序再次弹出,最后添加的内容首先被移除。 这些在编程中很常见 - 您可能还记得前一章中的函数调用栈,它是同一个想法的实例。

对象

回到松鼠人的示例。 一组每日的日志条目可以表示为一个数组。 但是这些条目并不仅仅由一个数字或一个字符串组成 - 每个条目需要存储一系列活动和一个布尔值,表明雅克是否变成了松鼠。 理想情况下,我们希望将它们组合成一个值,然后将这些分组的值放入日志条目的数组中。

对象类型的值是任意的属性集合。 创建对象的一种方法是使用大括号作为表达式。

let day1 = {
  squirrel: false,
  events: ["work", "touched tree", "pizza", "running"]
};
console.log(day1.squirrel);
// → false
console.log(day1.wolf);
// → undefined
day1.wolf = false;
console.log(day1.wolf);
// → false

大括号内有一列用逗号分隔的属性。 每个属性都有一个名字,后跟一个冒号和一个值。 当一个对象写为多行时,像这个例子那样,对它进行缩进有助于提高可读性。 名称不是有效绑定名称或有效数字的属性必须加引号。

let descriptions = {
  work: "Went to work",
  "touched tree": "Touched a tree"
};

这意味着大括号在 JavaScript 中有两个含义。 在语句的开头,他们起始了一个语句块。 在任何其他位置,他们描述一个对象。 幸运的是,语句很少以花括号对象开始,因此这两者之间的不明确性并不是什么大问题。

读取一个不存在的属性就会产生undefined

我们可以使用=运算符来给一个属性表达式赋值。如果该属性已经存在,那么这项操作就会替换原有的值。如果该属性不存在,则会在目标对象中新建一个属性。

简要回顾我们的绑定的触手模型 - 属性绑定也类似。 他们捕获值,但其他绑定和属性可能会持有这些相同的值。 你可以将对象想象成有任意数量触手的章鱼,每个触手上都有一个名字的纹身。

delete运算符切断章鱼的触手。 这是一个一元运算符,当应用于对象属性时,将从对象中删除指定的属性。 这不是一件常见的事情,但它是可能的。

let 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,和实际删除它的区别在于,在第一种情况下,对象仍然具有属性(它只是没有有意义的值),而在第二种情况下属性不再存在,in会返回false

为了找出对象具有的属性,可以使用Object.keys函数。 你给它一个对象,它返回一个字符串数组 - 对象的属性名称。

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"],
   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的表格其中包含两个观测变量所有组合情况的出现次数。我们以吃比萨事件为例并画出如下表格

可以使用以下公式计算ϕn指的是表格中的数字

符号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×769×4=40而除法线下面的部分除数则是10×80×5×85的平方根也就是。计算结果ϕ≈0.069,这个结果很小,因此吃比萨对是否变身成松鼠显然没有太大影响。

计算关联性

我们可以用包含4个元素的数组[76941]来表示一张2乘2的表格。我们也可以使用其他表示方式比如包含两个数组的数组每个子数组又包含两个元素769][41。也可以使用一个对象在其中包含一些属性并将其取名为“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/的下载文件中用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可以得到一个在010之间的数字。由于Math.floor是向下取整因此该函数会等概率地取到09中的任何一个数字。

还有两个函数分别是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循环中forvar name in object使用关键字in来遍历对象中包含的属性。

习题

特定范围数字求和

在本书的前言中,提到过一种很好的计算固定范围内数字之和的方法:

console.log(sum(range(1, 10)));

编写一个range函数接受两个参数start和end然后返回包含start到end包括end之间的所有数字。

接着编写一个sum函数接受一个数字数组并返回所有数字之和。运行上面的程序检查一下结果是不是55。

附加题是修改range函数接受第3个可选参数指定构建数组时的步数step。如果没有指定步数构建数组时每步按1增长和旧函数行为一致。调用函数range1102应该返回[13579]。另外确保步数值为负数时也可以正常工作因此range52-1应该产生[5432]。

// 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
    }
  }
};

最后产生的对象形成了一条链,如下图所示:

使用列表的一个好处是,它们之间可以共享相同的子列表。举个例子,如果我们新建了两个值:{value0resultlist}和{value-1resultlist}list引用了我们前面定义的变量。这是两个独立的列表但它们之间却共享了同一个数据结构该数据结构包含列表末尾的三个元素。而且我们前面定义的list仍然是包含三个元素的列表。

编写一个函数arrayToList当给定参数[123]时建立一个和前面示例相似的数据结构。然后编写一个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