From eb03391929a286f1d4687a9e69451a5460062cfa Mon Sep 17 00:00:00 2001 From: wizardforcel <562826179@qq.com> Date: Tue, 1 May 2018 20:51:19 +0800 Subject: [PATCH] 4. --- 4.md | 239 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 125 insertions(+), 114 deletions(-) diff --git a/4.md b/4.md index ec60a02..de70c66 100644 --- a/4.md +++ b/4.md @@ -277,7 +277,7 @@ addEntry(["weekend", "cycling", "break", "peanuts", ### 计算关联性 -我们可以用包含4个元素的数组([76,9,4,1])来表示一张2乘2的表格。我们也可以使用其他表示方式,比如包含两个数组的数组,每个子数组又包含两个元素([[76,9],[4,1]])。也可以使用一个对象,在其中包含一些属性,并将其取名为“11”和“01”。但是,一维数组更为简单,也容易进行操作。我们可以将数组索引看成包含两个二进制位的数字,左边一位(高位)数字表示变量“是否变成松鼠”,右边一位(低位)数字表示事件变量。例如,若二进制数字为10,表示雅克变成了松鼠,但事件并未发生(比如说吃比萨)。这种情况发生了4次。由于二进制数字10的十进制表示法是2,因此我们将其存储到数组中索引为2的位置上。 +我们可以用包含 4 个元素的数组(`[76,9,4,1]`)来表示一张 2 乘 2 的表格。我们也可以使用其他表示方式,比如包含两个数组的数组,每个子数组又包含两个元素(`[[76,9],[4,1]]`)。也可以使用一个对象,它包含一些属性,名为`"11"`和`"01"`。但是,一维数组更为简单,也容易进行操作。我们可以将数组索引看成包含两个二进制位的数字,左边的(高位)数字表示变量“是否变成松鼠”,右边的(低位)数字表示事件变量。例如,若二进制数字为 10,表示雅克变成了松鼠,但事件并未发生(比如说吃比萨)。这种情况发生了 4 次。由于二进制数字 10 的十进制是 2,因此我们将其存储到数组中索引为 2 的位置上。 下面这个函数用于计算数组的系数`ϕ`: @@ -294,9 +294,9 @@ console.log(phi([76, 9, 4, 1])); // → 0.068599434 ``` -该函数只是把`ϕ`计算公式转换成了JavaScript语言编写的代码。Math.sqrt函数用于求平方根,该函数属于标准JavaScript环境中Math对象中的属性。由于我们没有将行列之和保存在我们的数据结构中,因此需要通过计算表格中的两个字段来获取像n<sub class="calibre4">1·这样的值。</sub> +这将`φ`公式直接翻译成 JavaScript。 `Math.sqrt`是平方根函数,由标准 JavaScript 环境中的`Math`对象提供。 我们必须在表格中添加两个字段来获取字段,例如`n1`因为行和或者列和不直接存储在我们的数据结构中。 -雅克花了三个月的时间记录日志。在本章的代码沙箱([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/))的下载文件中用JOURNAL变量存储了该结果数据集合。 +雅克花了三个月的时间记录日志。在本章的代码沙箱([http://eloquentjavascript.net/code/](http://eloquentjavascript.net/code/))的下载文件中,用`JOURNAL`变量存储了该结果数据集合。 若要从这篇记录中提取出某个特定事件的2乘2表格,我们首先需要循环遍历整个记录,并计算出与变身成松鼠相关事件发生的次数。 @@ -306,10 +306,10 @@ function hasEvent(event, entry) { } 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; + let table = [0, 0, 0, 0]; + for (let i = 0; i < journal.length; i++) { + let entry = journal[i], index = 0; + if (entry.events.includes(event)) index += 1; if (entry.squirrel) index += 2; table[index] += 1; } @@ -320,74 +320,64 @@ console.log(tableFor("pizza", JOURNAL)); // → [76, 9, 4, 1] ``` -函数hasEvent用于测试某个记录中是否包含某个特定的事件。数组有indexOf方法,可以用于查找指定的值(在本例当中是事件名),如果找到了特定的值则返回当前索引,否则返回–1。所以说,如果indexOf返回的值是–1,则表示该事件在记录中不存在。 +数组拥有`includes`方法,检查给定值是否存在于数组中。 该函数使用它来确定,对于某一天,感兴趣的事件名称是否在事件列表中。 -函数tableFor中的循环体会检查记录中是否包含某个特定事件,并检查发生的事件是否伴随变身成松鼠一起发生,然后计算出表格中相应格子中的数字。每当发生某个特定事件时,就把表格当中对应格子中的数字加1。 +`tableFor`中的循环体通过检查列表是否包含它感兴趣的特定事件,以及该事件是否与松鼠事件一起发生,来计算每个日记条目在表格中的哪个盒子。 然后循环对表中的正确盒子加一。 -现在我们手上就有了计算单个关联性的工具函数。我们接下来只需要找出记录中每类事件的相关系数,并查看它们之间的关联程度。我们该如何把计算出来的结果保存起来呢? +我们现在有了我们计算个体相关性的所需工具。 剩下的唯一一步,就是为记录的每种类型的事件找到关联,看看是否有什么明显之处。 -### 对象映射 +### 数组循环 -其中一种方法是把所有计算出来的相关系数保存在一个数组当中,每条记录使用一个对象来进行保存,其中包含name和value两个属性。但这么做有一个问题,就是查询某个特定事件的相关系数的操作十分烦琐:你必须循环遍历整个数组,才可以找到你期望的那个包含正确name的对象。虽然我们可以将这个查询过程封装成一个函数,但我们还是要去编写更多的代码,而且对于计算机来说这些操作有些多余。 - -一种更好的解决方法是用事件类型作为对象的属性名称。这样我们就可以使用方括号来创建或读取对应的属性,也可以使用in运算符来检测其中是否包含我们期望的属性。 +在`tableFor`函数中,有一个这样的循环: ``` -var map = {}; -function storePhi(event, phi) { - map[event] = phi; +for (let i = 0; i < JOURNAL.length; i++) { + let entry = JOURNAL[i]; + // Do something with entry } - -storePhi("pizza", 0.069); -storePhi("touched tree", -0.081); -console.log("pizza" in map); -// → true -console.log(map["touched tree"]); -// → -0.081 ``` -映射表(map)可以通过一个值(在本例中是事件名)来获取对应的另一个值(在本例中是`ϕ`系数)。 +这种循环在经典的 JavaScript 中很常见 - 遍历数组,一次一个元素会很常见,为此,您需要在数组长度上维护一个计数器,并依次选取每个元素。 -在这种情况下使用对象会有一些潜在的隐患,我们会在第6章对此进行详细阐述。但就目前来说,我们可以暂且不去关心这些问题。 - -如果想从映射表当中获取所有存储的系数该怎么办呢?与数组的操作方式不同,我们无法预先了解所有的属性名称,因此也就没有办法使用一般的for循环体来遍历整个映射表。JavaScript提供了另一种遍历对象属性的循环语句。它与一般的for循环看起来很像,只是我们使用的关键字不是for而是in。 +在现代 JavaScript 中有一个更简单的方法来编写这样的循环。 ``` -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 +for (let entry of JOURNAL) { + console.log(`${entry.events.length} events.`); +} ``` +当`for`循环看起来像这样,在变量定义之后用`of`这个词时,它会遍历`of`之后的给定值的元素。 这不仅适用于数组,而且适用于字符串和其他数据结构。 我们将在第 6 章中讨论它的工作原理。 + ### 分析结果 -为了找出数据集中存在的所有事件类型,我们只需依次处理每条记录,然后遍历记录中的所有事件即可。目前,我们把所有遍历的事件相关系数存储在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)); +function journalEvents(journal) { + let events = []; + for (let entry of journal) { + for (let event of entry.events) { + if (!events.includes(event)) { + events.push(event); + } } } - return phis; + return events; } -var correlations = gatherCorrelations(JOURNAL); -console.log(correlations.pizza); -// → 0.068599434 +console.log(journalEvents(JOURNAL)); +// → ["carrot", "exercise", "weekend", "bread", …] ``` -让我们来看看计算结果。 +通过遍历所有事件,并将那些不在里面的事件添加到`events`数组中,该函数收集每种事件。 + +使用它,我们可以看到所有的相关性。 ``` -for (var event in correlations) - console.log(event + ": " + correlations[event]); +for (let event of journalEvents(JOURNAL)) { + console.log(event + ":", phi(tableFor(event, JOURNAL))); +} // → carrot: 0.0140970969 // → exercise: 0.0685994341 // → weekend: 0.1371988681 @@ -396,13 +386,14 @@ for (var event in correlations) // and so on... ``` -绝大多数相关系数都趋近于0。显然,摄入胡萝卜、面包或布丁并不会引发变身成松鼠。但是似乎在周末变身成松鼠的概率更高。让我们来过滤一下结果,看看相关系数大于0.1和小于–0.1的事件。 +绝大多数相关系数都趋近于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); +for (let event of journalEvents(JOURNAL)) { + let correlation = phi(tableFor(event, JOURNAL)); + if (correlation > 0.1 || correlation < -0.1) { + console.log(event + ":", correlation); + } } // → weekend: 0.1371988681 // → brushed teeth: -0.3805211953 @@ -413,49 +404,49 @@ for (var event in correlations) { // → 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"); +for (let entry of JOURNAL) { + if (entry.events.includes("peanuts") && + !entry.events.includes("brushed teeth")) { + entry.events.push("peanut teeth"); + } } console.log(phi(tableFor("peanut teeth", JOURNAL))); // → 1 ``` -嗯,错不了!当雅克摄入了一些花生而且不刷牙的话,就会变身成松鼠。要不是因为他这么不注意口腔卫生,也就不至于让他为此烦恼如此之久了。 +这是一个强有力的结果。 这种现象正好发生在雅克吃花生并且没有刷牙时。 如果他只是不注意口腔卫生,他从来没有注意到他的病痛。 -在了解情况之后,雅克再也不吃花生了,因此再也没有变身成松鼠。 +知道这些之后,雅克完全停止吃花生,发现他的变形消失了。 -在此后的一段时间里,雅克的生活一切顺利。但过了几年后他失业了,最后被迫去马戏团谋生。在马戏团,他表演的节目是“不可思议的松鼠人”,每次表演之前他都会在嘴巴上涂满花生酱。终于有一天,受够了这种屈辱的生存方式的雅克再也没能变回人形,他从马戏团帐篷的缝隙中逃了出去,消失在了森林之中。从此,再也没人看到过他。 +几年来,雅克过得越来越好。 但是在某个时候他失去了工作。 因为他生活在一个糟糕的国家,没有工作就意味着没有医疗服务,所以他被迫在一个马戏团就业,在那里他扮演的是不可思议的松鼠人,在每场演出前都用花生酱塞满了它的嘴。 -### 详解数组 +### 数组详解 -在本章结束之前,我想要介绍一些与对象相关的概念。我们先来介绍一些实用的数组方法。 +在完成本章之前,我想向您介绍几个对象相关的概念。 我将首先介绍一些通常实用的数组方法。 我们在本章的前面已经了解了push和pop方法,分别用于在数组末尾添加或删除元素。相应地,在数组的开头添加或删除元素的方法分别是unshift和shift。 ``` -var todoList = []; -function rememberTo(task) { +let todoList = []; +function remember(task) { todoList.push(task); } -function whatIsNext() { +function getTask() { return todoList.shift(); } -function urgentlyRememberTo(task) { +function rememberUrgently(task) { todoList.unshift(task); } ``` -上面的程序用于管理任务列表。你可以调用rememberTo("eat")向列表末尾添加任务。当你准备完成一件任务时,调用whatIsNext()来获取(并删除)列表中的第一项任务。函数urgentlyRemeberTo也可以用来添加任务,只不过是将任务添加到列表的开头而已。 +这个程序管理任务队列。 您通过调用`remember("groceries")`,将任务添加到队列的末尾,并且当您准备好执行某些操作时,可以调用`getTask()`从队列中获取(并删除)第一个项目。 `rememberUrgently`函数也添加任务,但将其添加到队列的前面而不是队列的后面。 -有一个与indexOf方法类似的方法叫lastIndexOf,只不过indexOf从数组第一个元素向后搜索,而lastIndexOf从最后一个元素向前搜索。 +有一个与`indexOf`方法类似的方法,叫`lastIndexOf`,只不过`indexOf`从数组第一个元素向后搜索,而`lastIndexOf`从最后一个元素向前搜索。 ``` console.log([1, 2, 3, 2, 1].indexOf(2)); @@ -475,9 +466,11 @@ console.log([0, 1, 2, 3, 4].slice(2)); // → [2, 3, 4] ``` -如果没有指定结束索引,slice会返回从起始位置之后的所有元素。对于字符串来说,它也有一个具有相同功能的slice方法供开发人员使用。 +如果没有指定结束索引,slice会返回从起始位置之后的所有元素。您也可以省略起始索引来复制整个数组。 -concat方法用于拼接两个数组,其作用类似于字符串的+运算符。下面的示例展示了如何使用concat和slice方法。该函数接受一个数组与一个索引,并返回原数组移除指定索引处的那个元素后的一个副本。 +`concat`方法可用于将数组粘在一起,来创建一个新数组,类似于`+`运算符对字符串所做的操作。 + +以下示例展示了`concat`和`slice`的作用。 它接受一个数组和一个索引,然后它返回一个新数组,该数组是原数组的副本,并且删除了给定索引处的元素: ``` function remove(array, index) { @@ -488,20 +481,22 @@ console.log(remove(["a", "b", "c", "d", "e"], 2)); // → ["a", "b", "d", "e"] ``` +如果你将`concat`传递给一个不是数组的参数,该值将被添加到新数组中,就像它是单个元素的数组一样。 + ### 字符串及其属性 我们可以调用字符串的length或toUpperCase这样的属性,但不能向字符串中添加任何新的属性。 ``` -var myString = "Fido"; -myString.myProperty = "value"; -console.log(myString.myProperty); +let kim = "Kim"; +kim.age = 88; +console.log(kim.age); // → undefined ``` -字符串、数字和布尔类型的值并不是对象,因此当你向这些值中添加属性时JavaScript并不会报错,但实际上你并没有将这些属性添加进去。这些值都是不可变的,而且无法向其中添加任何属性。 +字符串、数字和布尔类型的值并不是对象,因此当你向这些值中添加属性时JavaScript并不会报错,但实际上你并没有将这些属性添加进去。前面说过,这些值是不变的,不能改变。 -但这些类型的值包含一些内置属性。每个字符串中包含了若干方法供我们使用,最有用的方法可能就是slice和indexOf了,它们的功能与数组中的同名方法类似。 +但这些类型包含一些内置属性。每个字符串中包含了若干方法供我们使用,最有用的方法可能就是slice和indexOf了,它们的功能与数组中的同名方法类似。 ``` console.log("coconuts".slice(4, 7)); @@ -510,7 +505,7 @@ console.log("coconut".indexOf("u")); // → 5 ``` -唯一的区别在于,字符串的indexOf方法可以使用多个字符作为搜索条件,而数组中的indexOf方法则只能搜索单个元素。 +一个区别是,字符串的`indexOf`可以搜索包含多个字符的字符串,而相应的数组方法仅查找单个元素。 ``` console.log("one two three".indexOf("ee")); @@ -524,63 +519,79 @@ console.log(" okay \n ".trim()); // → okay ``` -我们已经了解了字符串类型length属性的用法。可以使用charAt方法来获取字符串当中某个特定的字符,当然也可以像数组中那样使用方括号和数字来获取字符串中的字符。 +上一章中的`zeroPad`函数也作为方法存在。 它被称为`padStart`,接受所需的长度和填充字符作为参数。 ``` -var string = "abc"; +console.log(String(6).padStart(3, "0")); +// → 006 +``` + +你可以使用`split`,在另一个字符串的每个出现位置分割一个字符串,然后再用`join`把它连接在一起。 + +``` +let sentence = "Secretarybirds specialize in stomping"; +let words = sentence.split(" "); +console.log(words); +// → ["Secretarybirds", "specialize", "in", "stomping"] +console.log(words.join(". ")); +// → Secretarybirds. specialize. in. stomping +``` + +可以用`repeat`方法重复一个字符串,该方法创建一个新字符串,包含原始字符串的多个副本,并将其粘在一起。 + +``` +console.log("LA".repeat(3)); +// → LALALA +``` + +我们已经看到了字符串类型的`length`属性。 访问字符串中的单个字符,看起来像访问数组元素(有一个警告,我们将在第 5 章中讨论)。 + +``` +let string = "abc"; console.log(string.length); // → 3 -console.log(string.charAt(0)); -// → a console.log(string[1]); // → b ``` -### arguments对象 +### 剩余参数 -每当函数被调用时,就会在函数体的运行环境当中添加一个特殊的变量arguments。该变量指向一个包含了所有入参的对象。在JavaScript中,我们可以传递多于(或少于)函数参数列表定义个数的参数。 +一个函数可以接受任意数量的参数。 例如,`Math.max`计算提供给它的参数的最大值。 + +为了编写这样一个函数,你需要在函数的最后一个参数之前放三个点,如下所示: ``` -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."); +function max(...numbers) { + let result = -Infinity; + for (let number of numbers) { + if (number > result) result = number; + } + return result; } -argumentCounter("Straw man", "Tautology", "Ad hominem"); -// → You gave me 3 arguments. +console.log(max(4, 1, 9, -2)); +// → 9 ``` -有些函数可以接受任意数量的参数,比如console.log。这类函数一般都会遍历arguments对象。这样可以创建非常易用的接口。比如之前雅克创建的日志记录。 +当这样的函数被调用时,剩余参数绑定一个数组,包含所有其它参数。 如果之前有其他参数,它们的值不是该数组的一部分。 当它是唯一的参数时,如`max`中那样,它将保存所有参数。 + +您可以使用类似的三点表示法,来使用参数数组调用函数。 ``` -addEntry(["work", "touched tree", "pizza", "running", - "television"], false); +let numbers = [5, 1, 7]; +console.log(max(...numbers)); +// → 7 ``` -由于该函数会被反复多次调用,因此我们可以创建一个更简单的接口来替代现有的接口。 +这在函数调用中“展开”数组,并将其元素传递为单独的参数。 像`max(9, ...numbers, 2)'那样,可以包含像这样的数组以及其他参数。 + +方括号的数组表示法,同样允许三点运算符将另一个数组展开到新数组中: ``` -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"); +let words = ["never", "fully"]; +console.log(["will", ...words, "understand"]); +// → ["will", "never", "fully", "understand"] ``` -这个版本的函数与一般的函数一样读取第一个参数(即squirrel),然后从arguments对象中读取其余的参数(循环索引从1开始,跳过第一个参数),并将这些参数添加到数组中。 - ### Math对象 正如我们所看到的那样,Math对象中包含了许多与数字相关的工具函数,比如Math.max(求最大值)、Math.min(求最小值)和Math.sqrt(求平方根)。