Eloquent Javascript Note

Eloquent Javascript娱乐笔记。

Written with StackEdit.

Chapter 0 - Introduction

我们有两种方式来与外部沟通,一是通过我们的感官,二则是通过计算机。

相对于所见即所得的前者,使用计算机显然是要复杂困难许多。我们并不能用日常习惯地感官方式(视觉认知、动手移动)去指引计算机工作,而是需要借用语言,计算机能理解的语言。

人类语言的复杂性使我们发展出纷繁多样的文化,而计算机语言,尽管没那么复杂,却也依然能创造一个光怪陆离的世界。

Javascript就是这样一门计算机语言。

在走入这门语言之前,Marijn已经提醒到:编程真是件很困难的事,尽管它的基本原理通常是简单直白的。编程要求你能把所知所学的碎片内容都联系在一块,形成一个融洽的整体。

所以初学者可能常常会迷失在这片无常的海洋里,感到一切都那么复杂又支离。或许怀疑自我,或许心灰意冷。

无甚他法,唯独不要放弃。歇息一会儿,重新阅读材料,确保自己完全理解之前所学,接着再从头来过。学习是件艰难的事,但你学会的任何东西都是自己的,也都将让你接下来的路变得容易一些。

子曰:不愤不启,不悱不发 ;举一隅不以三隅反,则不复也。

程序可以指代很多东西:一段代码、一块数据、或一种驱使电脑工作的力量。

而计算机则是承载程序的宿主。它并不聪明,却让人觉得无所不能。其实这仅仅是因为它能在非常短的时间内处理大量的简单任务而已。

编程的艺术在于控制程序的复杂度,让它处于自己的掌控之中。但不少程序员却因此走了极端,从而只愿意生活在程序的舒适区中,恒久地采用所谓“最佳实践”的方案与技术,并将那些愿意冒险的人视作坏的程序员。

然而,电子世界不正因为程序与技术的多样性而显得如此熠熠生辉吗?它确实遍布危险,让新手们充满困惑。但这只是意味着你需要在这条道路上保持谨慎与理智。如此,我们将会明白,这个虚幻的空间里总是存在挑战与未知。亦可赛艇,不是吗?

过去的计算机语言可并非现在这样。从机器码到汇编语言,从汇编语言到如今的JavaScript。复杂和细枝末节的东西被逐渐剔除,只留下了我们重视的部分。

一门好的计算机语言,能让程序员们站在一个较高层次来编程,消除不感兴趣的末节,提供一些有用的构建模块(如whileconsole.log),并允许我们可以定义自己的模块(如sumrange函数)。

最后,Marijn谈到他为什么选用JavaScript来教授编程。是的,尽管JavaScript显得变化多端,让人讨厌(比如它的弱类型和异常多的浏览器兼容性问题)。但在另一方面,JavaScript却也因这些问题成就了它自己。

它是一门自由而普适的语言(原文里自由指的是flexibility)。

对初学者来说,它是宽容近迂的;而它高度灵活的语言机制则可以让我们实现许多其他严格语言无法实现的技术。

如今,每一个浏览器都在使用JavaScript,所以你大概可以在任何一个地方运行起你的程序。除此之外,在数据库和后台开发方面的长足发展,更是让它的适用范围愈发增广了。

这些都让如今的JavaScript看起来是那么茁壮富有生气,不是吗?

所以,欢迎来到编程世界。

Chapter 1 - Values, Types, and Operators

实际上,一切数据最基础的单位都是比特。比特由二进制表示,只能取0或1。

Values

想象一下,当一台计算机中储存了数以百亿计比特的数据时,我们应怎样做才能避免自己迷失在这片数据之海呢?

在JavaScript中,数据被分成了一个个片段(chunks)来表示信息,这被称为值(values)。值本质上都是由比特构成,但不同的值可能会扮演不同的角色。每个值都有从属的类型,用以决定它本身的角色。

在JavaScript中,值有六种基础类型:

  1. Numbers
  2. Strings
  3. Booleans
  4. Objects
  5. Functions
  6. Undefined values

想要使用这些值,只需简单地唤起它们的名字就行,既不需要什么施法材料,也不需要贡献祭品。但它们并非凭空而生,被召唤出来的值都会以比特形式储存在某一物理位置中。幸运的是,当你不再需要某些值时,它们便会被回收。由此空闲出来的比特将会为新值提供空间。

Numbers

数值就是数值,没什么需要特别解释的。

JavaScript用64比特的信息来表示一个数值。也就是说在理论上,JavaScript可以表示2^64(18e18)长度的整数(whole numbers/integers)。但事实并非如此,因为JavaScript的数值信息中还需要包含正负号(占用了1比特空间)以及小数点的位置信息,所以实际中它可以表示的整数范围会缩小到9e15,依然大得惊人。

尽管JavaScript能保证9e15范围内整数计算的精确性,却依然无法确保小数(fractional numbers)计算的准确。受到64位比特储存空间的限制,许多小数的计算都将会取近似值(比如π)。这是我们应该注意到的地方。

Arithmetic

Javascript的四则运算规则和通常我们熟知的规则无甚差别,如果感到不确定,添加一个小括号提高优先级即可。四则运算操作符(operators)分别是

  1. Addition: +
  2. Minus: -
  3. Multiplication: *
  4. Division: /

需要注意的是%操作符代表的是取余数,它的优先级和乘除操作符一致。

1
2
3
// Modulo / Remainder
212 % 100
// 12

Special Numbers

JavaScript中有3种特别的数值,分别是

  1. 正无穷:Infinity
  2. 负无穷:-Infinity
  3. 非数值:NaN

正无穷和负无穷很好理解。类似于Infinity - 1的简单四则操作依然会得到Infinity。但基于无穷的运算并没有强健的数学基础,便容易引致NaN的出现。

1
2
3
4
Infinity / Infinity
// NaN
Infinity - Infinity
// NaN

除此之外,其他会造成结果不精确、无意义的运算也会得到NaN

1
2
0 / 0
// NaN

Strings

字符串类型用来表示文本。

1
2
3

"Hello World!"
`Hello Moto!`

成对出现的双引号或单引号都是用来说明这段数据属于字符串类型。

有些信息难以被直接放入字符串数据中,比如换行或者添加引号

这时,我们需要使用转义符号\\后紧跟着的字母会具有特别的含义。如\n表示换行,\"表示这就是一个双引号,\\表示这就是一个反斜杠。

1
2
3
4
5
6
7
"This is the first line\nAnd this is the second"
/*
This is the first line
And this is the second
*/

"A newline character is written like \"\\n\"."
// “A newline character is written like "\n"

此外,字符串不能进行四则运算,但可以使用+操作符来表示前后链接。

1
2
"He" + "llo"
// "Hello"

Unary Operators

并非所有的操作符都是符号,有些也会以单词形式存在,例如typeof。这个操作符被用来展现数据的类型。

1
2
3
4
typeof 4.5
// number
typeof "x"
// string

我们之前见过的操作符都会对两个值进行操作,而typeof不同,它只会使用一个值。因此,它被视为一元操作符(unary operator),而其他的则被称作二元操作符(binary operator)。

减法操作符可以同时被用做一元操作符和二元操作符。

1
2
- (5 - 2
// -3

Boolean Values

布尔类型只包含两个值: truefalse,用来表示对立关系。

比较操作(comparisons)是生成布尔值的一种方法。

1
2
3
4
5 > 1
// true
2 > 3
// false

字符串类型也同样可以进行比较,但需遵守以下两点规则:

  1. 字符串中的字母大小按字母表排序,越靠后的字母越大。
  2. 大写字母恒小于小写字母。
1
2
3
4
5
6
7
8
"a" > "b"
// false
"ac" > "ab"
// true
"a" > "A"
// true
"a" > "Z"
// true

事实上,这种比较是基于Unicode标准。这个标准为每一个字符(包括了其他语言,如希腊语,阿拉伯语等)都指定了一个数字。由此使得计算机储存字符信息成为可能。

类似的比较操作符还有:

  • 大于等于:>=
  • 小于等于:<=
  • 等于:==
  • 不等于:!=

在JavaScript中,只有一个值不等于它本身,那就是NaN

1
2
NaN == NaN
// false

由于NaN表示无意义的计算结果,所以它无法和其他无意义的计算结果相等。

Logical Operators

另一种生成布尔值的方法就是使用逻辑比较符,前三种分别是:

  • 和:&&
  • 并:||
  • 非:!

第四种逻辑操作符比较有意思,叫三元操作符(ternary operator):

[Boolean] ? [value] : [value]

这也被称作条件操作符(conditional operator),它将根据问号前的布尔值来选择冒号左右的值。

1
2
3
4
true ? "hehe" : "meme"
// "hehe"
false ? "hehe" : "meme"
// "meme"

Undefined Values

未定义类型包含了两个特别的值:nullundefined,被用来表示缺失有意义的值。未定义类型不包含任何信息。

在许多操作结束后,由于没有产生有意义的值,所以结果会以undefined来代替。这样做仅仅是因为这些操作必须有一个值而已……

为方便理解,undefinednull在大多数情况下可以被视作同义词。

Automatic Type Conversion

之前曾提到过,由于JavaScript具有高度灵活的语言机制,基本上它可以接受并运行你给出的任何代码,甚至是那种看起来奇奇怪怪的。来看看Marijn给的例子:

1
2
3
4
5
6
7
8
9
10
console.log(8 * null)
// → 0
console.log("5" - 1)
// → 4
console.log("5" + 1)
// → 51
console.log("five" * 2)
// → NaN
console.log(false == 0)
// → true

当操作符应用在“错误”的数据类型上时,JavaScript会默默地进行数据类型的转化。而它的转化规则却通常是我们不喜欢的……这个过程被称之为类型转换(type coercion)。具体的转化规则其实在《JavaScript权威指南》上讲得比较清楚。

所以我们该如何避免这种烦人的类型转换发生呢?

那就是用===代替=====符号不仅会检查值是否相等,也会检查数据类型是否一致

Short-circuiting of Logical Operators

&&||逻辑操作符在处理不同类型的值时用的转换方法比较迷离。

&&会将左边的值转化为布尔型,若为false,那将返回左边的值;若为true,那将返回右边的值。

||同样是将左边的值转化为布尔型,若为true,将返回左边的值;若为false,将返回右边的值。

根据这两位的谜之转换方法,我们就能使用一个被称作短路评估(short-circuit evaluation)的技术。

true || X中,无论X如何毁天灭地,JavaScript都不会在乎,它总是返回true。

同样,在false && X中,无论X如何破碎星辰,JavaScript也都无视,它总是返回false。

这点也同样适用于三元操作符,JavaScript将无视那个被抛弃的可怜孩子……

Chapter 2 - Program Structure

这章我们终于要来写点真正的小程序了。

Expressions and statements

我们将一段能创造值的代码片段称为表达式(expression)。一个直接写出的值(类似于11``,"xixi")即是表达式的一种。

通过操作符产生的值依然是表达式。这样的做法显现了程序语言优雅的一面:我们可以通过组合不同的表达式来表示出任意复杂度的计算。

如果说表达式可以比做人类句子中的成分,那语句(statement)即代表了完整的句子。程序其实可以说就是句子的集合。

最简单的语句可以仅仅是一个以分号做结尾的表达式:

1
110;

但这样简单的语句的确一没什么意义。我们需要语句的根本原因,是在于它能对整个程序产生切实影响(side effects)。譬如,改变程序某处的状态以对后续语句造成影响。而简单的表达式,不过是创造出了一个在诞生之后便泯灭于虚空的值罢了。

在某些情况下,JavaScript允许我们在语句结束后不使用分号做结尾。但此间的规则挺有些复杂(权威指南中有提),所以就目前来说,适宜的方式还是在一段语句结束后手动添加;

Variables

我们通过操作符来从旧值中生成新值,但却无法保留它们。所以,如何“挽留”住一些创生后即可能泯灭的值?

JavaScript使用了变量(variables)来勾连和保存值。

1
var love = "broken heart";

关键词(keyword)var表明我们要在接下来的语句中定义一个变量,紧随其后的则是这个变量的名字。

1
var love

变量命名的规则:

  • 不能以数字开头
  • 不能使用除$_之外的符号

如果想立即给这个变量赋值,那么就接着使用=操作符和一个表达式。

1
var love = "forever"

变量被赋值以后,并不意味它就固定了,我们随时可以用=操作符来改变它连接的值。

1
2
3
4
5
6
var love = "forever"
console.log(love)
// "forever"
var love = "nevermore"
console.log(love)
// "nevermore"

我们应该把变量比作一只章鱼,而非盒子它并不包含值,而是抓住值——两个变量可以指向同一个值。

程序只能使用它保存过的值。所以如果我们想保存一个值,那就“长出一只触手”去抓住它,或是让已有的“某只触手”改变目标。

一个没有被赋值的变量被称为空变量,其默认值为undefined

我们可以一次性定义多个变量,使用,隔开即可。

1
var love = "forever", hurt = "without you"

Keywords and Reserved Words

关键字和保留字都是禁止用来作变量名的,它们在JavaScript中有着特殊含义(关键字)或留作日后版本的JavaScript使用(保留字)。

break case catch class const continue debugger
default delete do else enum export extends false
finally for function if implements import in
instanceof interface let new null package private
protected public return static super switch this
throw true try typeof var void while with yield

这时候不得不说,有了IDE,什么都不怕……

The environment

在某个给定时间内存在的变量与它们值的集合被称为环境(environment)。当创建新程序时,初始环境并非是空的。它总是包含了一些语言标准要求的起始变量以及一些提供和周围系统进行交互渠道的变量。譬如:

在浏览器中,有一些变量会负责监视和影响已经加载好的页面,并读取鼠标和键盘的输入。

Functions

在初始环境中被预置的值大多是函数类型。函数是一段被包裹进值里面的小程序。这些值可以被执行(apply)以激活这段程序。譬如alert变量,它所包裹函数的功能是显示一条对话框:

1
2
alert("love you!");
// 在浏览器中出现一个包含"love you!"文本的对话框

执行一个函数的行为可以被称为Invoking(祈唤),calling(调用)或applying(应用)

我们通过在一个可以通过在某个创建函数类型值的表达式后添加()的方式来执行一个函数。

()中的值会被传递进函数所包裹的程序中,这就是所谓的参数(parameters)alert函数只需要一个任意类型的参数。但其他函数可能需要多个参数,参数的类型也可能不尽相同。

The console.log function

虽然aleart函数作为一个呈现输出结果的端口时比较有用,但这种弹框方式的确让人感到恼火。所以我们通常使用console.log函数,它可以将其参数传递给一些文本输出设备。在浏览器中,这个设备被称为控制台(console)。如何打开浏览器的控制台?……

1
2
3
var love = "forever"
console.log("love you", love)
// love you forever

我们在这里发现了一个有趣的问题:变量名不是不可以包含.吗?所以这里的.别有意涵,console.log表达式的真正含义是:

返还console变量所指代值里的log属性。

第四章里Marijn会详细介绍这部分内容。

Return values

类似于alertconsole.log的函数会对程序造成切实影响(side effect),但并不是所有函数都是如此。

有些函数被执行后将产生一个值,这时它们的作用就类似于一个表达式,譬如取最大值函数Math.max

1
2
console.log(Math.max(2, 4));
// 4

函数运行后产生值的过程被称作返回值(return values)。回顾一下,在JavaScript中任何一个创生值的东西都被称作表达式。这意味着返回值的函数可以被用来与其他表达式进行组合,包裹在一段更长的表达式中。

1
2
console.log(Math.min(2, 3) + 5);
// 7

Prompt and Confirm

除了alert函数,浏览器还提供了其他一些会激活提示框的函数。

  • prompt函数
  • confirm函数

这两种函数都已经比较少用了,大部分原因在于我们无法良好地控制提示框的最终样式。

用来玩玩小程序还是不错的(狗日的整蛊弹框链接……)。

Control Flow

一般来说,程序执行语句的顺序是自上而下线性的。可以用简单的直线箭头表示。示例:

1
2
var the_year = Number(prompt("How long do you have fell in love with her/him?","..."))
alert("I got it. It is " + the_year + ", right?")

Number函数的功能是将参数的类型转化为数值,类似的函数还有Boolean或者String()

Straight line

Conditional Execution

条件执行是除直线流外的另一种程序运行方式。程序将基于条件中表达式的布尔值来选择运行那一条程序。

conditional Execution

JavaScript使用if关键词来创建条件执行,在最简单的情况下,我们可能只想让程序在满足一个也是唯一一个条件时执行语句,看看Marijn的例子:

1
2
3
4
var theNumber = Number(prompt("Pick a number", ""));
//只有输入可以被转化为数值时才进行平方操作
if (!isNaN(theNumber))
alert("Your number is the square root of " + theNumber * theNumber);

我们当然不会时时刻刻只想有一个备胎供程序选择,备胎代代无穷已,不是吗?所以下列的条件结构就很自然了:

1
2
3
4
5
6
7
8
var hehe = Number(prompt("Write your favorite numer", "0"));

if (hehe < 10)
alert("xixi");
else if (hehe < 100)
alert("meme");
else
alert("memeda");

While and Do Loops

试想我们需要输出12以内的所有偶数,我们需要怎么办?显然,一个一个console.log()并不是好办法。我们写程序的目的不就是为了do less么?

这时候,就需要循环结构(loop)登场了。

loop

JavaScript通过关键词while调用循环,它的格式为:

1
2
3
while (condition) {

}

还是Marijn的例子:

1
2
3
4
5
6
7
8
var number = 0;
while (number <= 12) {
console.log(number);
number = number + 2;
}
// 0
// 2
// … etcetera

只要小括号中的表达式值为truewhile循环将会反复执行大括号内的语句。

大括号包裹我们所想要执行的语句。它的作用类似于小括号与表达式的关系(小括号可以将数条表达式包裹起来形成一条语句):一系列语句被包裹在大括号中,形成一个区块(block)

Marijn出于简洁性(brevity)的考虑,在书写单语句的循环和条件结构时都不使用大括号。而通常,我们会出于一致性和省事的想法,对这些结构里的语句都用上大括号。

在示例中,number变量起到跟踪器的作用。每轮循环结束时,它都会增加2,并在新循环开始前与条件进行比较,从而控制程序循环的次数。

Marijn给出了另一个计算2^10的循环程序例子:

1
2
3
4
5
6
7
8
var result = 1;
var counter = 0;
while (counter < 10) {
result = result * 2;
counter = counter + 1;
}
console.log(result);
// → 1024

我们同样可以使用1作为跟踪变量的起始值,但Marijn会在第四章里解释为什们咱们应习惯从0开始。

do循环和while循环的作用是类似的。它们唯一的区别是do循环会至少执行区块中的语句一次,然后在下一轮循环开始前才会和循环条件进行比较。

Marijn的例子:

1
2
3
4
do {
var yourName = prompt("Who are you?");
} while (!yourName);
console.log(yourName);```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

在此例中,`!`操作符会在求反操作前强制将字符串转化成布尔值。而只有非````的字符串会被转换为`true`值。所以这个程序的含义就是:除非你输入了任何字符,它将反复地要求你写点东西。

### Indenting Code

在示例程序中,我们应该已经留意了一些字符缩进现象。在JavaScript中,这其实不是必须的,甚至分句断行都是可选的。如果乐意,我们可以在一行里书写所有代码。但在复杂的程序里,对不同区块的代码进行合理缩进会为我们提供一个良好的可视形象。这和没人喜欢在垃圾堆里工作是一个道理。

### for loops

回顾下`while`循环:首先,创建一个计数变量;然后是while循环,在循环条件中进行比对计数变量和条件的比较;最后,在循环区块最后改变计数变量的值。

由于它的应用实在是太普遍了,所以JavaScript提供了一个优化版的循环结构,那就是`for`循环:

``` javascript
for (track;condition;update) {

}

Marijn的例子:

1
2
3
4
5
for (var number = 0; number <= 12; number = number + 2)
console.log(number);
// → 0
// → 2
// … etcetera

for关键字后的小括号里必须包含三个部分,用分号隔开。第一部分的作用是初始化计数变量,第二部分的作用是创建循环条件,第三部分的作用是设置每次循环结束后更新计数变量值的方式。

Marijn给出了for循环实现计算2^10值的方式:

1
2
3
4
5
6
7
var result = 1;
for (var counter = 0; counter < 10; counter = counter + 1)
result = result * 2;
console.log(result);
// → 1024

// 注意!虽然Marijn没用大括号包裹for循环区块,他至少还是用了缩进的方式来表达`result = result * 2;`从属于`for`循环。

Breaking Out of a Loop

让循环条件表达式产生的值为false可不是唯一让我们退出循环的方式。我们还可以使用break来强制中断循环。

1
2
3
4
5
for (var love = 0; ; love++) {
if (love > 23)
break;
console.log("love is worth " + love);
}

如果木有break,我们的这个for循环将会变成一个无限循环(infinite loop),程序将会执行到本宇宙时间的尽头之后……嗯,其实一般是以浏览器崩溃作为结束。

continue的作用和break有些类似,当它在区块中被执行时,会跳过本次循环之后的语句,进入新一轮循环。

Updating variables succinctly

在循环中,我们经常需要更新一些变量的值。

二笔青年:

1
2
trackPlus = trackPlus + 1
trackMinus = trackMinus - 1

普通青年:

1
2
trackPlus += 1
trackMinus -= 1

文艺青年:

1
2
trackPlus++
trackMinus--

Dispatching on a value with switch

我们会经常遇到Marijn提到的如此代码形状:

1
2
3
4
if (variable == "value1") action1();
else if (variable == "value2") action2();
else if (variable == "value3") action3();
else defaultAction();

对此,我们可以用switch结构来更直观的构建。但JavaScript继承来的switch结构比C/JAVA要傻嗨多了(作者强力吐槽)……

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (prompt("What is the weather like?")) {
case "rainy":
console.log("Remember to bring an umbrella.");
break;
case "sunny":
console.log("Dress lightly.");
case "cloudy":
console.log("Go outside.");
break;
default:
console.log("Unknown weather type!");
break;
}

注意”sunny”那里并没有break。在switch结构中,语句只会在遇到break时才会停止执行。这让我们的条件可以共享一些语句(晴天既适合外出又适合穿轻薄些),但也可能制造出一些我们不想有的意外……

Capitalization

命名变量还是挺有点技术含量的,以下是比较常见的变量命名方式

  1. fuzzylittleturtle
  2. fuzzy_little_turtle
  3. FuzzyLittleTurtle
  4. fuzzyLittleTurtle

Marijn推荐2,而4是常见的驼峰命名法,也有很多人喜欢。其他的就比较蠢蠢了。

有时候我们也会遇到变量首字母大写的情况,比如Number。此时JavaScript会将其视为一个构造函数(contruct),具体会在第六章说。

Comments

程序的注释一般用来抒发潜藏的诗意情感……偶尔用来让自己的代码更清晰明了,或解释某些复杂区域的作用方式。

JavaScript有两种注释方式:

  • 单行注释
1
2
var xxx = `love`
`// I love xxx`
  • 多行注释
1
2
3
4
5
6
7
/* 
I love xxx
but
never more.
*/


console.log(`I love &nbsp;`)

Summary

程序是建立在语句之上的。有时候语句中会包含更多的语句。此外,语句中也经常会有表达式,而一个表达式也可能会由其他表达式组合而成。

一般来说,程序的执行是自上而下线性的。但我们可以用条件结构(if, else, switch)循环结构(while, do, for)来改变程序的执行顺序。

变量可以为数据命名,也有助于我们记录程序状态的变化。环境是已定义变量的集合,JavaScript总是会在初始时创建一些符合标准的有用变量。

函数是一类用来封装(encapsulate)小段程序的值。调用函数被看做是表达式,但只是有可能会产生值。

Excercise

和Marign的一对比,我的好蠢……

Chapter 3 - Fucntions

函数是JavaScript语言的润滑剂。将小段程序包裹进一个值的概念大有用武之地:构建更大型程序、减少重复、为不同子程序命名并将它们分隔开。

其中最有用的能力大概就是创造新的词语吧。成人的日常字典有20000个词语,而大概不会有哪门程序语言会内建有20000条默认命令。所以,借助函数,我们可以手动来定义一些新词语,摆脱重复性的工作。

Defining a function

定义一个函数的方式很简单,将一个类型为函数的值勾连到一个变量上即可。

Marijn的例子:

1
2
3
4
5
6
var square = function(x) {
return x * x;
};

console.log(square(12));
// → 144

使用function关键字创建一个函数。一个函数中包含了参数(parameters)和在调用时被执行的主体(body)。就算只有一条语句,主体也必须被大括号包裹,这点和之前的结构有所不同。

函数的参数数量是任意的,看看marijn的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var makeNoise = function() {
console.log("Pling!");
};

makeNoise();
// → Pling!

var power = function(base, exponent) {
var result = 1;
for (var count = 0; count < exponent; count++)
result *= base;
return result;
};

console.log(power(2, 10));
// → 1024

一些函数可以产生新值。我们使用return语句来决定这个值是什么。当读取到return语句时,函数会直接结束,然后返还return后的值。如果值为空,那就返还undefined

Parameters and scopes

参数的行为和变量很类似。但参数值是由函数的调用者(caller)决定的,而不是由函数自己决定。

作用域(scope)是一个比较重要的概念。函数的参数及其内部使用var定义的变量,被称作为本地的(local)。即意是它们只能在函数内部被读取调用,多个相同函数的本地变量是互不干扰的。

在函数之外定义的变量被称作全局的(global),这指的是它们在整个程序里都是可见的,可以被任意函数访问(只要函数内部没有定义同名变量)。

Marijn给的一个简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = "outside";

var f1 = function() {
var x = "inside f1";
};
f1();
console.log(x);
// → outside

var f2 = function() {
x = "inside f2";
};
f2();
console.log(x);
// → inside f2

本地作用域这种有趣的特性避免了函数间可能产生的意外交互。试想如果所有变量都是全局的,那我们恐怕就很容易一失足成千古恨了。

Nested scope

当然,作用域的内容可不仅仅本地和全局这么简单,因为函数内也还能嵌套函数,由此可以创造多个不同层级的作用域。

看看Marijn的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var landscape = function() {
var result = "";
var flat = function(size) {
for (var count = 0; count < size; count++)
result += "_";
};
var mountain = function(size) {
result += "/";
for (var count = 0; count < size; count++)
result += "`";
result += "\\";
};

flat(3);
mountain(4);
flat(6);
mountain(1);
flat(1);
return result;
};

console.log(landscape());
// → ___/````\______/`\_

flatmountain函数可以读取到landscape函数内的result变量,因为它们位于定义result变量的函数下一层。但它们互相间不能读取count变量,答案很显然,它们的作用域是平行的。此外,landscape外部的环境不能读取到其内部的一切变量。

简言之,每一层本地作用域可以读取到全部在其上层的作用域。变量是否可见取决于其在程序中的位置,这被称作静态作用域(lexical scoping)。

在JavaScript中,唯有函数可以创造一层新作用域,类似于使用括号创造新区块的方式是没用的。

1
2
3
4
5
6
var something = 1;
{
var something = 2;
// Do stuff with variable something...
}
// Outside of the block again...

Marijn用这个示例告诉我们,被大括号包裹的something变量依然指代外部的那个变量。

下个版本的JavaScript将会引入key关键字来解决这个问题。

Functions as values

乍看下,函数变量的作用似乎仅仅是用来命名一段程序。这让我们很容易将函数值和它的名字搞混淆。

但这二者显然是不同的。函数值可以做和其他类型值一样的事:将之用于表达式而非直接调用它;将之储存到其他地方;将之作为参数传入到其他函数中……一个勾连了函数值的变量仍然是个正常的变量,它同样还能被指派去关联其他值。

Marijn的例子:

1
2
3
4
5
var launchMissiles = function(value) {
missileSystem.launch("now");
};
if (safeMode)
launchMissiles = function(value) {/* do nothing */};

第五章会细说如何将函数值传入到其他函数中。

Declaration notation

另一种定义函数的简单表示方法如下:

1
2
3
function meme() {
return `xixi`
}

看起来好像和之前差不多,不过确实有一个非常微妙的区别。将这种函数定义方式应用在条件或循环结构中时,不同的平台会有着不同的处理方式,以至于最近的标准已经明确ban掉了。

Marijn例子:

1
2
3
4
5
6
function example() {
function a() {} // Okay
if (something) {
function b() {} // Danger!
}
}

如果想确保程序能正确运行,那么就只在函数或程序的最外层区块中使用这种方式。

此外,我们再看看这段Marijn的代码:

1
2
3
4
5
console.log("The future says:", future());

function future() {
return "We STILL have no flying cars.";
}

函数定义脱离了正常程序的从上至下执行流程。它们会在概念上移动到所属作用域的最顶部,这样可以让作用域的所有代码能使用它。爽歪歪。。。

The call stack

深入看看函数的控制流,Marijn例子:

1
2
3
4
5
function greet(who) {
console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

这个流程近似于:

1
2
3
4
5
6
7
top
greet
console.log
greet
top
console.log
top

由于函数会在结束时跳转回到调用它的位置,所以计算机必须记住函数是在哪(上下文context)被调用的。第一个console.log函数跳转回了greet函数,第二个console.log函数跳转回了程序底部。

计算机储存上下文的地方被称为调用栈(call stack)。每当一个函数被调用时,调用栈最顶部就会储存上一个上下文,当函数结束返还时,这个上下文就会从栈上被移除,并用之来让程序继续执行。

栈的使用将会占据物理储存空间。所以如果栈变得过大时,将可能出现”out of stack space”、”too much recursion”等提示。

Marijn给了一个玄学例子:

1
2
3
4
5
6
7
8
function chicken() {
return egg();
}
function egg() {
return chicken();
}
console.log(chicken() + " came first.");
// → ??

Optional Arguments

JavaScript的机制允许函数在调用时被输入任意数量的参数。多余的参数会被无视,而如果参数不够的话则空缺的参数值会被undefined替代。

从好的方面说,JavaScript让我们的函数可以拥有可选参数值。

Marijn例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
function power(base, exponent) {
if (exponent == undefined)
exponent = 2;
var result = 1;
for (var count = 0; count < exponent; count++)
result *= base;
return result;
}

console.log(power(4));
// → 16
console.log(power(4, 3));
// → 64

第四章将会讲解如何在函数内获取完整参数列表,这种操作将使得函数接受任意数量的参数成为可能,譬如Marijn所说:

1
2
console.log("R", 2, "D", 2);
// → R 2 D 2

Closure

在JavaScript中,函数每次被调用都将重新创造本地变量。这给我们留出了一个问题:当函数执行结束后,如果我们想保留这个本地变量,需要怎么做?

Marijn给出了一个例子:

1
2
3
4
5
6
7
8
9
10
11
function wrapValue(n) {
var localVariable = n;
return function() { return localVariable; };
}

var wrap1 = wrapValue(1);
var wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

warpValue函数首先定义了一个本地变量localVariable,然后返回一个返还localVariable变量的函数。

wrap1wrap2上看,我们成功地保留住了宝贵的本地变量(同时也证明了一个函数每次都调用都将会创造新的本地变量,不同调用产生的同名本地变量不会相互干扰)。

后来想想,觉得老马的这个例子似乎并不太好,我自己写了个合适的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Without closure
function hehe() {
var p = 5;
return ++p;
}

var xixi = hehe();

console.log(xixi);
console.log(xixi);
console.log(xixi);

/**
→6
→6
→6
**/


// With closure
function meme() {
var p = 5;
return function() {return ++p;};
}

var dada = meme();

console.log(dada());
console.log(dada());
console.log(dada());

/**
→6
→7
→8
**/

这种能够引用函数内部本地变量某个具体值的特性被称作闭包(closure)。一个封装了一些本地变量的函数被称作一个闭包- -除了妈妈不用再担心我没法保存函数内本地变量之外,我们还可以用闭包做很多有意思的事。

比如Marijn的这个无聊例子:

1
2
3
4
5
6
7
8
9
function multiplier(factor) {
return function(number) {
return number * factor;
};
}

var twice = multiplier(2);
console.log(twice(5));
// → 10

这例子中我们可以发现,类似localVariable这个本地变量并非是必须的,函数的参数本身就是一个本地变量

理解这样的程序行为比较困难,不过Marijn给了一个很形象的解释:把function关键字看做是一个冷冻仓,它的作用是暂时冻结其内部的代码,并将之打包(成为函数值)。所以当我们看到return function(...) {...}这样的结构时,就可以将之视为要返还一些留待稍后使用的东西。哎哟不错哦。

如之前这个例子,当我们调用multiplier创建twice变量时,我们就激活了冷冻仓,它将暂时封存return number * factor;这段代码,记忆住传入的factor本地变量。进一步,当我们传入number变量后,它就呼啦呼啦解冻了。

Recursion

函数自己调用自己是完全可行的,只要注意别出现爆栈事故。这种行为被我们称为递归(recursion)。老马的例子:

1
2
3
4
5
6
7
8
9
function power(base, exponent) {
if (exponent == 0)
return 1;
else
return base * power(base, exponent - 1);
}

console.log(power(2, 3));
// → 8

这样的迭代函数更近似于数学上对幂运算的定义。但在JavaScript中,迭代函数的运行效率要比循环形式慢10倍。

这种优雅与效率的竞争时时刻刻都会突显在计算机程序设计中。程序员需要从中做出权衡。

老马的意见是,除非你觉得程序已经明显慢下来了,之前都不要在乎效率问题。进而在出现问题后,查看是哪部分耗时最长,然后再开始用效率来替代优雅。

这并不意味着我们从最开始就应该不在乎性能。正如power函数所显示的,很多情况下优雅的编程方式并不比有效率的编程方式要简化多少。

老马提这个问题主要是考虑到很多新手在起步时太看重效率问题,以至于编写出了许多繁杂又错漏百出的代码。这点应该引起重视。

当然,迭代这个方法并不总是低效的。在一些情况下,它的效率会高于循环。比如无限分支问题

Consider this puzzle: by starting from the number 1 and repeatedly either adding 5 or multiplying by 3, an infinite amount of new numbers can be produced. How would you write a function that, given a number, tries to find a sequence of such additions and multiplications that produce that number? For example, the number 13 could be reached by first multiplying by 3 and then adding 5 twice, whereas the number 15 cannot be reached at all.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function findSolution(target) {
function find(start, history) {
if (start == target)
return history;
else if (start > target)
return null;
else
return find(start + 5, "(" + history + " + 5)") ||
find(start * 3, "(" + history + " * 3)");
}
return find(1, "1");
}

console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

这里涉及到了调用栈和短路评估技术的理解,我把老马的示意图放上来。

1
2
3
4
5
6
7
8
9
10
11
12
13
find(1, "1")
find(6, "(1 + 5)")
find(11, "((1 + 5) + 5)")
find(16, "(((1 + 5) + 5) + 5)")
too big
find(33, "(((1 + 5) + 5) * 3)")
too big
find(18, "((1 + 5) * 3)")
too big
find(3, "(1 * 3)")
find(8, "((1 * 3) + 5)")
find(13, "(((1 * 3) + 5) + 5)")
found!

嗯哼,我也有点小晕。

Growing functions

我们会在两种比较自然的情况下想到命名一个新函数:

  1. 有些代码经常被复用

  2. 有些功能值得被单独写成函数

但给函数起个一眼就懂一看便知一览无余的好名字是很困难的。

老马这个农场例子太长了,这里简明扼要的提点一下:

  1. 函数的名字要简洁,功能有扩展余地
  2. 抵制冲动,在确定函数的某些功能用得上前,不要自作聪明地添加上去。

Functions and side effects

笼统地来说,函数可以分作两个类别:

  1. 对程序产生切实影响的
  2. 返回值的

当然这两个特性可以共存在一个函数中。

一个最终返回值的函数比产生实际影响的函数更易与程序其他部分关联起来。这点倒是易于理解,一个只会console.log的函数还能干嘛呢……

纯函数(pure function)返回值的函数类型特殊情况。它不仅不会对程序产生影响,也不依赖于在程序中产生影响的其他部分代码。比如:纯函数不会使用某些可能会被其他代码更改的全局变量。

纯函数的特点在于,它的输出永远与输入对应(并且不会对程序产生任何影响),不受函数之外的一切因素干扰。相对的,非纯函数might return different values based on all kinds of factors and have side effects that might be hard to test and think about

这并不意味着我们要发动圣战消灭非纯函数……相信没有任何一种纯函数方法来代替console.log……此外,一些操作也会在使用非纯函数的情况下更有效率,所以在出于计算速度的考量时,我们很可能会避免使用纯函数。

Summary

现在我们大概明白了,funtion关键字有两种使用方式,如老马的例子:

1
2
3
4
5
6
7
8
9
// Expression: Create a function value f
var f = function(a) {
console.log(a + 2);
};

// Statement: Declare g to be a function
function g(a, b) {
return a * b * 3.5;
}

理解变量的关键之一是理解作用域的概念。本地变量只在函数内部可见,在函数每次被调用时都会重新创建。外层函数的作用域对内层可见,反之则不行。

函数帮助我们组织代码结构,将不同的功能分离出来,正如日常所见的图书章节目录一般。

Excercise

Minimum

1
2
3
4
5
6
function min(x,y) {
if (x < y)
return x;
else
return y;
}

Recursion

1
2
3
4
5
6
7
8
9
10
11
12
13
function isEven(n) {
if (n === 0) {
return true;
}
else if (n === 1) {
return false;
}
else {
return isEven(n-2);
}
}

// 其实我是4空格党,无奈jsbin是2空格党,不共戴天(麒麟臂发作脸)

Bean counting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function countBs(char) {
char = String(char);
var count = 0;
for(n = 0; n < char.length; n++) {
if (char.charAt(n) === "B")
count++;
}
return count;
}

console.log(countBs("BBC"));

function countChar(char,target) {
char = String(char);
var count = 0;
for(n = 0; n < char.length; n++) {
if (char.charAt(n) === String(target))
count++;
}
return count;
}

console.log(countChar("kakkerlak", "k"));

Chapter 4 - Data Structures: Objects and Arrays

无论是NumbersBooleans还是Strings,都是构建数据结构的基础,好比用来搭建房子的其中一种砖块。而众所周知的是,我们不能指望仅仅用一种砖头就可以搭建好一幢大楼。

所以,Objects来啦。它允许我们将不同类型的值聚拢在一起,构造出更复杂的结构。

The weresquirrel

老贾发现自己会时不时变成松鼠,为了解决这个困境,他开始用科学拯救自己。他觉得自己的变身是由某个位置原因触发的,所以他决定记录自己每天的行为日志。

首先,他得设计一种数据类型来储存这一系列数据。

Data sets

在JavaScript中,数组(array)的存在使我们可以对一系列数据进行操作。

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

没什么特别要注意的点,记得索引数组的符号也是[]就可以了。

Properties

几乎所有的值都有属性,但nullundefined是例外。老马说:

1
2
null.length;
// → TypeError: Cannot read property `length` of null

访问属性的方式有两种:

  1. value.x
  2. value[x]

两种方式的区别在于,使用.访问时,.后面的部分必须是一个有效的变量名,并且这个变量直接命名了一个属性;而在用[]时,它将尝试对表达式x进行评估,并将结果作为属性名(好吧这里我有点疑惑,可能是曲解了原文,等看其他书时反过来验证。)

由于属性名可以是任意字符串,所以当我们想访问的属性名为2或者"John Doe"之流时,必须用[]的方法访问。因为它们都不是有效的变量名。

储存在数组中的元素是以属性形式储存的。因为这些属性的名字是数字,所以我们必须用过[]语法来访问。.length属性会告诉我们数组中有多少个元素,而由length是有效的变量名,所以我们直接使用.语法就可以了。

Methods

对一个值来说,如果它的某个属性包含函数,那么就称这个属性为它的方法……老马例子:

1
2
3
4
5
var doh = "Doh";
console.log(typeof doh.toUpperCase);
// → function
console.log(doh.toUpperCase());
// → DOH

有趣的点在于,尽管我们没有为.toUpperCase函数传入参数,它却能访问到"Doh"。这点老马会在第6章细说。

老马接着演示了一下数组对象所有的一些方法:

1
2
3
4
5
6
7
8
9
10
11
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"]

Objects

回到老贾的故事中,尽管数组可以储存一系列的数据,但似乎还是不能达到我们的要求:我们希望数组中的每一个元素可以包含更多信息。

Objects应运而出,简单的说,它是属性的集合,我们可以自由地增加属性或删除属性。老马接着介绍了一下创建对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
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

我们可以使用大括号来创建一个对象。在对象中,我们使用逗号来分离属性。对于每个属性,我们先写出名字,再写出它的值。对于占据了多行空间的属性值,请注意缩进,以示美观。

对于不符合有效变量命名的属性名,我们需要用引号括起来。老马如是说:

1
2
3
4
var descriptions = {
work: "Went to work",
"touched tree": "Touched a tree"
};

由此我们还可以发现,大括号在JavaScript中有两个作用:

  1. 创建一个对象。
  2. 创建一个区块。

不过这基本上是不会混淆的……

读取一个不存在的属性会返还undefined值。

可以使用=来为已有的对象创建一个新属性或覆盖旧属性。

和之前讲解变量绑定时用的章鱼例子类似,属性绑定也是差不多的。不同的属性名可以同时绑定相同的值。

delete这个一元操作符可以帮我们删除属性。老马例子:

1
2
3
4
5
6
7
8
9
10
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

学到了学到了。

数组是一种专门用来储存有序数据的对象。如果你使用typeof [1, 2]来查看它的类型,那则会毫无意外的返回object。我们可以将数组视为一只每个触手都有数字编号的章鱼。

又来盗图一波。

让我们再再次回到老贾的故事中,重新整理后的信息表:

1
2
3
4
5
6
7
8
9
10
11
12
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... */
];

Mutability

对于我们之前遇到的值类型:NumbersBooleansStrings,它们都是不可变的。没有任何一种方法能在创建它们之后更改它的内容。

而对object来说,情况则有些变化,我们可以通过修改它的属性来改变对象的内容。

对于对象来说,两个对象包含相同的值和两个变量指代同一个对象的含义是不相同的。看看老马的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

object1object2指代的是同一个值,所以改变object1的同时object2也就改变了。

而尽管object3包含了与object1相同的属性内容,它们也不相等。

JavaScript的==操作符在比较对象是,只会在两个对象指代相同的值(物理位置上)时才会返还true。在JavaScript中没有给对象内容进行深度比较方法,但我们可以自己来写一个。

The lycanthrope’s log

再再再回到老贾的故事中,他为自己搞了一个写日志的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var journal = [];

function addEntry(events, didITurnIntoASquirrel) {
journal.push({
events: events,
squirrel: didITurnIntoASquirrel
});

//day1, 2, 3
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);
}

经过几天的记录,老贾打算进行一下相关性研究,看看到底是什么事件和自己的变身有关联。

如何计算两个事件的相关性?好吧其实我真的完全忘了,看了老贾的方法死记硬背了一下。

下面这段是LaTeX语法,GitHub不定能识别:

$$ϕ=\frac{n{11}n{00}-n{10}n{01}}{\sqrt{n{·1}n{·0}n{1·}n{0·}}}$$

Computing correlation

公式有了,如何计算呢?

老马将下标看成二进制数,左边表示是否有变身,右边表示是否该事件发生。接着,他做了二进制到十进制的转换(比如:11→3),然后将四组数据按转换后的顺序放入了一组数组:

1
2
3
4
5
6
7
8
9
10
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

OK,接着我们要考虑怎么将记录的数据(download here)转换成表格,老马给出了下面的函数(我反正是目瞪口呆咯):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 验证是否存在这个事件
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]

好了,有了这个函数,我们就可以计算所有事件和变身的相关性了。但我们应该用什么方式来储存结果呢?

Objects as maps

一种可能的方法是把结果都储存在数组中。但这样会很麻烦,我们需要通过遍历数组的方式找到想要的结果。虽然我们可以写一个函数来完成这个查询工作,但还是有些多此一举了。

更好的方式是创建一个对象,利用[]方法来增加这个对象的属性。老马如是写道:

1
2
3
4
5
6
7
8
9
10
11
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

这就像是一个函数,我们给出X(名字),然后就得到了Y(phi值)。

老马强调道,这样操作对象的方式会有一些潜在的问题,他将在第6章详解一下。

如果我们想遍历这个对象的属性应该如何操作?不像数组那样提供了有序的脚标,遍历对象的属性需要用特殊的for循环(Python啊。。。),老马如此写道:

1
2
3
4
5
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

The final analysis

现在要做的,是要计算机将所有的事件相关度都计算出来,老马给出了这样的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function gatherCorrelations(journal) {
// 定义一个对象来储存事件和对应相关度
var phis = {};
// 准备遍历journal数组的所有元素
for (var entry = 0; entry < journal.length; entry++) {
// 遍历每个元素里面的events属性里的事件集(依然是数组)
var events = journal[entry].events;
for (var i = 0; i < events.length; i++) {
var event = events[i];
// 如果某一事件不存在phis对象的属性中,添加这个事件及相关度
// phi函数计算相关度,table函数加总相关度所需数据
if (!(event in phis))
phis[event] = phi(tableFor(event, journal));
}
}
return phis;
}

var correlations = gatherCorrelations(JOURNAL);
console.log(correlations.pizza);
// → 0.068599434

最后我们就可以检查一下correlations变量了:

1
2
3
4
5
6
7
8
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.1或低于0.1的:

1
2
3
4
5
6
7
8
9
10
11
12
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

有趣的东西找到了:peanutsbrush teeth相关度很高。老马进一步开始了分析:

1
2
3
4
5
6
7
8
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

破费~看来我们已经破解了真相,当吃花生和不刷牙事件同时发生时,老贾一定会变身松鼠。

故事的结局我就不记录了,欢迎给老马捧场。

Further arrayology

老马进一步介绍了一些普遍会用到的数组方法。

pop & push/shift & unshift

这四个方法都会改变原先数组

1
2
3
4
5
6
7
8
9
10
11
12
13
var todoList = [];
// 增加一个任务
function rememberTo(task) {
todoList.push(task);
}
// 返回最左边的任务并在数组中消除
function whatIsNext() {
return todoList.shift();
}
//在最左边增加一个任务
function urgentlyRememberTo(task) {
todoList.unshift(task);
}

indexOf & lastIndexOf

indexOf相反,lastIndexOf从数组最后向前搜索指定元素。

1
2
3
4
console.log([1, 2, 3, 2, 1].indexOf(2));
// → 1
console.log([1, 2, 3, 2, 1].lastIndexOf(2));
// → 3

两种方法都可以接收一个额外参数,用来指定搜索起始index

slice

接收两个参数(start[inclusive], end[exclusive]),截取两个参数之间的元素。

1
2
3
4
console.log([0, 1, 2, 3, 4].slice(2, 4));
// → [2, 3]
console.log([0, 1, 2, 3, 4].slice(2));
// → [2, 3, 4]

如果end参数没有给出,slice方法将截取start参数后的所有元素。字符串也有这个方法,效果是差不多的。

concat

该方法可用来合并数组,类似于+操作符在字符串中的应用。

1
2
3
4
5
6
7
// slice和concat的例子
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"]

Strings and their properties

我们可以从字符串值中读取lengthtoUpperCase之类的属性或方法。但如果我们想添加属性,则就不行了。

1
2
3
4
var myString = "Fido";
myString.myProperty = "value";
console.log(myString.myProperty);
// → undefined

stringnumberBoolean这些值都不是objects。尽管JavaScript不会在你为这些值添加属性时报错,但实际上它并不会储存这些属性。这些值是immutable,别忘咯。

但这些值确实有一些内建属性,字符串常见的方法有:

slice & indexOf

1
2
3
4
console.log("coconuts".slice(4, 7));
// → nut
console.log("coconut".indexOf("u"));
// → 5

字符串中的indexOf方法参数可以写入多个字母,而在数组中它只能用来查询某个元素。

1
2
console.log("one two three".indexOf("ee"));
// → 11

trim

这个方法 1一出字符串中的空格,包括spaces, newlines, tabs, and similar characters

charAt

该方法可以访问字符串中某个具体的字母,也可以用[]方法代替。

1
2
3
4
5
6
7
8
9
10
11
12
var string = "abc";
// 获取字符串长度
console.log(string.length);
// → 3

// 获取字符串第一个字母
console.log(string.charAt(0));
// → a

// 使用[]方法访问
console.log(string[1]);
// → b

The arguments object