第一章:深入程序设计
欢迎阅读 你不了解的JS(YDKJS) 系列。
新手入门(Up & Going) 一书主要介绍程序设计(编程)的几个基本概念——当然我们侧重讲JavaScirpt(通常缩写为JS)——以及如何掌握并理解本系列书籍剩下的标题(的知识点)。尤其是对于刚接触编程或者刚接触Java的读者来说,本书将简要浏览你从入门到进阶所需要掌握的知识。
本书一开始从高层次水平来解释程序设计的基本概念。按照YDKJS的标题,本书主要针对没有编程经验的读者,从JavaScirpt语言的角度帮助读者逐步理解编程的概念。
第一章主要概括深入程序设计过程中你想深入了解的知识和实践,也有许多其他能够帮助你在未来深入这些话题的很棒的编程入门资源,希望读者能在本章之外好好学习它们。
一旦你掌握了一般的编程基础,第二章会指导你熟悉JavaScirpt的编程风格。第二章介绍了JavaScirpt是什么,再一次声明,它不是一份完整的指南——这是YDKJS系列剩下的部分要做的事情。
如果你已经相当熟悉JavaScirpt了,先阅读第三章来快速预览你能从YDKJS中学到什么,然后开始学习吧!
代码 Code
让我们从头开始吧!
一个程序,也常叫做源代码或代码,是一系列告诉计算机该执行什么任务的特殊指令集合。通常代码被保存在文本文件中,当然你也可以直接在浏览器的控制台中输入JavaScript,我们后面会讲到。
计算机语言 即是有效的格式和指令组合的规则,有时候也称为 语法,就像英语告诉你怎样拼写单词,怎样利用单词和标点构建有效语句一样。
语句
在计算机语言中,由词语、数字和操作符组合并执行指定任务的即是语句。在JavaScript中,一条语句可能如下所示:
a = b * 2;
字符a
和b
称为 变量,就好比你用来存储东西的盒子。在程序中,变量用于保存程序中用到的值(如数字42
)。可以把变量想象成真实值的占位符。
相比之下,2
则就是一个值,称为字面值,因为它是独立的而不是保存在变量中。
字符=
和*
是操作符——它们对值和变量执行赋值和算术乘法等操作。
JavaScript中的语句末尾以分号(;)结束。
语句a = b * 2;
告诉计算机先取得保存在变量b
中当前值,乘以2
,然后将计算结果保存在另一个变量a
中。
程序就是多条这样的语句的组合,它们描述了实现程序的目的所需要的所有步骤。
表达式
语句由一个或多个表达式组成。一个表达式可以是对一个变量或值的引用,也可以由一系列变量和值通过操作符连接组合而成。
例如:
a = b * 2;
这条语句中包含了4个表达式:
2
是 字面值表达式b
是 变量表达式,表示取得变量的当前值b*2
算术表达式,表示乘法运算a = b * 2
是一个 赋值表达式,表示将b*2
的计算结果赋给变量a
(后文将介绍更多赋值操作)
一般单独存在的表达式也叫做 表达式语句,如:
b * 2;
这种风格的表达式语句不常见,也不实用,通常不会对程序的运行产生任何影响——它会取得变量b的值并乘以2,但是不会对计算结果做任何操作。
更常见的表达式语句是 调用表达式语句,因为整条语句本身即为函数调用表达式:
alert(a);
执行程序
这些编程语句的组合是怎样告诉计算机该做什么呢?程序需要被执行(才能起作用),或者说运行程序。
像a = b * 2
这样的语句有助于开发者阅读和书写,但这并不是计算机能够直接理解的形式。所以计算机里需要有特定的工具(解释器 或 编译器)来将你编写的代码翻译成计算机能够理解的命令。
对有些计算机语言来说,每次运行程序时,命令的翻译(通常叫做 解释 代码)一般是自上而下、一行接一行进行的。而对于其他语言,翻译过程(通常叫做 编译 代码)在程序运行之前进行,所以之后当程序运行时,实际上运行的是已经编译好的计算机能够理解的指令。
我们通常认为JavaScript是一门解释型语言,因为JavaScript源代码每次运行时都会被执行。但是这种说法不完全准确,JavaScript引擎实际上在运行程序时先编译然后立即执行编译后的代码。
注: 更多关于JavaScript编译的知识,请参考本系列的 作用域&闭包 一书的前两章。
动手试一试
本章将通过一些简单的代码片段来介绍每个编程概念,当然都是用JavaScript写的。
再一次强调:读者在阅读本章时——可能需要花时间复习几遍——应该自己动手敲这些代码来熟悉这些概念。最简单的方法是打开你所用的浏览器(Firefox、Chrome、IE等)的开发者工具。
建议: 一般来说,可以通过快捷键或菜单选项来打开开发者工具。更多关于在你喜欢的浏览器中启动和使用开发者工具的信息,请参考"Mastering The Developer Tools Console"。想在控制台中一次输入多行,可以通过<shift> + <enter>
来换行。一旦输入<enter>
,控制台就会执行你刚刚输入的所有内容。
我们先来熟悉下如何在控制台中执行代码。首先,建议在浏览器中打开一个新的空白标签页,我喜欢在地址栏中输入about:blank
来打开。然后,确保按照刚刚提到的方法打开了开发者控制台。
现在,输入下面的代码,看看是怎么执行的:
a = 21;
b = a * 2;
console.log( b );
在Chrome的控制台中输入上面的代码会得到下面的输出:
好了,试一试吧。学习编程的最佳方法就是动手敲代码!
输出
在前面的代码片段中,我们用到了console.log(..)
,我们来简要看下这行代码做了什么。
你可能已经猜到了,这就是通过它在控制台中打印文本(对用户来说也叫输出)。这条语句有两点需要解释下。
第一,log( b )
部分是函数调用,我们将变量b
传递给该函数,函数得到b
的值并打印到控制台。
第二,console.
部分是log(..)
函数所在的对象的对象引用,我们会在第二章中详细介绍对象及其属性。
另一种输出的方式是执行alert(..)
语句,如:
alert( b );
执行上述语句,不会在控制台打印输出,而是弹出一个包含b
变量内容的"OK"弹出框。一般在控制台中用console.log(..)
比用alert(..)
更有助于学习编程和执行你的程序,因为你可以一次输出很多值而不影响浏览器的运行和交互。
本书中,我们都用console.log(..)
来输出。
输入
我们在讨论输出的同时,也对 输入(即:从用户那里获得信息)好奇。
获得输入最常见的方式是在HTML页面中想用户展示一个可以输入内容的元素(如文本框),然后用JS读取这些值并保存到程序的变量中。
对于像本书一样只是简单的学习和展示来说,也可以用另一种方法来获得输入,用prompt(..)
函数:
age = prompt( "Please tell me your age:" );
console.log( age );
可以猜到,传递给prompt(..)
的信息——本例中的"Please tell me your age:"——会打印到弹出框中,结果如下所示:
点击"OK"提交输入的文本之后,你会发现刚才输入的值被保存到了变量age中,并通过console.log(..)函数输出。
在学习基本编程概念的过程中,为了简单起见,本书中的实例不要求输入。但你现在已经知道如何使用prompt(..)了,如果你想挑战一下自己,可以在你的练习中使用输入。
操作符
操作符是对变量和值进行的操作。我们已经用过两个JavaScript操作符=和了。
`操作符执行算术乘。很简单,对吗?
=`操作符用于赋值——先计算=号右侧的值(源值),然后将其存入左边指定的变量(目标变量)中。
注意: 指定赋值可能看起来是一个奇怪的逆序。有些人可能喜欢将源值放在左边而将目标值放在右边,如42 -> a(这在JavaScript中是非法的),而不是a = 42。不幸的是,a=42这样的形式及其类似的变体形式,在现代编程语言中是相当普遍的。如果觉得不自然,请花点时间在脑海中默记它然后习惯它。
想象:
a = 2;
b = a + 1;
这里,我们将值2赋给变量a,然后我们得到变量a的值(仍为2),加上1后结果等于3,并保存在变量b中。
从技术上讲,关键词var
不是操作符,但是在每个程序中都要用到它,因为它是 声明(也叫 创建)变量 的主要方式。
记住要在使用变量之前声明它。但是在每个 作用域 中的变量只能声明一次;声明之后便可使用任意多次。例如:
var a = 20;
a = a + 1;
a = a * 2;
console.log( a ); // 42
这里列举下JavaScript中常见的操作符:
- 赋值:如a=2中的“=”
- 算术计算:+(加),-(减),
*
(乘)和/(除),如a*3 - 复合赋值:
+=
,-=
,*=
,/=
是将算术操作与赋值操作组合起来的操作符,如a+=2(等效于a = a+2) - 自增/自减:
++
,--
,如a++(等效于a = a+1) 访问对象属性:
.
,如console.log()对象是保存其他具有特定名字(属性)的值的值。obj.a表示对象obj有一个名字为a的属性。也可以通过obj["a"]的方式被访问对象的属性。
- 相等:
==
(松散相等),===
(严格相等),!=
(松散不等),!==
(严格不等),如a==b。参见第二章及“值&类型”。 - 比较:
<
,>
,<=
,>=
,如a<=b - 逻辑:
&&
,||
, 如 a||b表示选择a或b。
注: 更多关于操作符的知识,请参考MDN(Mozilla Developer Network)的官方手册。
值和类型
如果你询问手机店的员工某款手机的售价是多少,他们可能会说“99,99”(即$99.99),他们告诉你的是一个实际数字值,表示购买该款手机你需要支付的金额(含税)。如果你买两台手机,只需将这个数值乘以2得到$199.98,即为你的消费额。
如果同一个员工拿另一款相似的手机说“free”(可能用手势),他们并没有给你一个确切的数值来表示你需要花费的金额($0.00),而是单词“free”。
随后你继续问手机是否带充电器时,得到的答案可能仅仅是“yes”或“no”。
类似的,在程序中表示值的时候,基于你想用这些值来做什么来选择这些值的不同表示方式。
在编程术语里,值的这些不同表示叫做 类型。JavaScript有几种内置类型,每种类型都有其初始值:
- 当你想做数学运算时,你需要
number
类型; - 当你想在屏幕上打印某个值时,你需要
string
类型(一个或多个字符、单词、句子); - 当你想在程序中做判断时,你需要
boolean
类型(true
或false
)。
直接包含在源代码中的值叫做 字面量。string字面量由双引号"..."
或单引号'...'
括起来——两者没有什么区别。number字面量和boolean字面量就是他们表示的字面意思(即:42、true等)。
比如:
"I am a string";
'I am also a string';
42;
true;
false;
除了string/number/boolean值类型之外,编程语言中一般还支持数组,对象,函数 等类型。我们会在之后的章节中讨论更多的值和类型。
类型之间的转换
如果你有一个number类型的值,但是想要打印到屏幕上,那么你需要将其转换为string类型的值,在JavaScript中这种转换是强制的。相似的,如果有人在一个商务网站上的表单中输入了一串数字字符,这是string类型的值,但是如果之后你要用这个值进行算术运算的话,你需要 强制 将其转换为number类型。
JavaScript提供了几种不同的方式来在不同 类型 之间强制转换,如:
var a = "42";
var b = Number( a );
console.log( a ); // "42"
console.log( b ); // 42
用Number(...)
(内置函数)将任意其他类型 显式 转换为number类型,这种方式简单粗暴。
但是当你试着比较两个不是同一类型的值时会产生歧义,这就需要 隐式 转换了。
当将字符串"99.99"与数字99.99进行比较时,大部分人都认为它们是相等的,但是它们并不严格相等,不是吗?这是同一个值的两种不同表示,属于两种类型。你可以说它们是“松散相等”,是吗?
所以如果你用 == 对两者进行比较:"99.99" == 99.99,JavaScript会将左边的"99.99"转换为number类型的99.99。现在比较就变成了:99.99 == 99.99,结果当然是true。
尽管设计隐式转换是为了帮助你,但是如果不花时间学习隐式转换的规则并熟练掌握它的话它也可能带来困扰。大多数JS开发者都没有掌握隐式转换,所以共识是隐式转换容易造成困扰,可能给程序带来意想不到的bug,因此应该避免使用。有时候这甚至被认为是JavaScript语言的设计缺陷。
但是,隐式转换机制是可以掌握的,而且也是每个想要掌握JavaScript的人必须掌握的。一旦你掌握了它的规则,它不但不会给你造成困扰,实际上还会帮助你写出更好的程序!所以花精力学习它是值得的。
注: 更多类型转换的知识,请参考本书的第二章和 类型&语法 一书的第四章。
代码注释
手机店的员工可能会需要记一些新发布的手机的特性或者公司公布的新计划等笔记。这些笔记仅供员工查看——而不是供消费者阅读的。无论如何,通过记录所有这些销售相关的信息,这些笔记帮助员工更好的完成他们的工作。
在学习码代码的过程中学到的最重要的一条经验是,代码不仅仅是给计算执行的。代码对于开发者与编译器而言都是一样的,每一bit都至关重要。
你的电脑只关心机器码,汇编产生的一系列二进制0和1。你几乎可以写出无限多种程序来产生相同的二进制序列。你选择怎样编写你的程序不仅关乎你自己,更关乎你组里面的其他成员,甚至是未来的自己。
一方面,你应该努力写出可以正确工作的程序;另一方面,当你的程序被他人阅读时应该清晰合理。为你的变量和函数取好的名字会极大地加强程序的易读性。
另一个重要的方面是代码注释。注释是插入程序中纯粹为了帮助人理解代码的文本。解释器/编译器会忽略这些注释。
关于怎样才能写出注释良好的代码有很多不同的观点;我们没办法制定绝对的通用规则。但是有些原则和指南还是很有帮助的:
- 没有注释的代码肯定是次优的;
- 太多的注释(例如每行一句)是糟糕代码的标志;
- 注释应该解释 why 而不是 what。如果代码特别难理解,注释也可以用来解释how。
在JavaScript中有两种类型的注释:单行注释和多行注释。 例如:
// This is a single-line comment
/* But this is
a multiline
comment.
*/
如果你想在某条语句上或行末加一句注释,可以用“//”单行注释。“//”后面的所有内容都会被当作注释(因而会被编译器忽略),直到行末。对于单行注释中可以写什么没有限制。 如:
var a = 42; // 42 is the meaning of life
如果你的注释需要几行才能解释清楚,可以用/* .. */
多行注释。
这里有一个多行注释的通常用法:
/* The following value is used because
it has been shown that it answers
every question in the universe. */
var a = 42;
多行注释也可以用在一行中的任何位置,甚至是一行的中间,因为有*/
表示注释的结尾。如:
var a = /* arbitrary value */ 42;
console.log( a ); // 42
多行注释中唯一不能出现的内容是*/
,因为它会中断注释。
你肯定会想带着一个好的注释代码的习惯来开启编程的学习之旅。在本章的后面,你会看到我用注释帮助解释一些东西,所以在你的练习中你也应该这么做。相信我,每一个阅读你的代码的人都会感谢你!
变量
大多数有用的程序需要追踪某个值,这个值会随着程序过程的变化、程序中特定任务调用不同的运算符处理它而发生改变。
在程序中实现这个目的的最简单方式是将这个值赋给一个符号容器,称为 变量——这么叫是因为容器中的值会随着时间的变化而变化。
有些编程语言需要声明变量(容器)类型来保存特定类型的值,如number
或string
。静态类型,或称为强类型,主要是为了提高程序的健壮性,因为可以避免意外的值类型转换。
另外一些语言则强调值的类型而不是变量的类型。弱类型,或称为动态类型,允许变量在任意时刻保存任何类型的值。这有利于提升程序的灵活性,因为单个变量可以在程序的逻辑流程中的任意时刻表示任何类型的值。
JavaScirpt是 动态类型 语言,这意味着变量可以保存任意类型的值而不用强制声明类型。
如前所述,我们用var
语句来声明变量——没有其他的方式来声明变量。考虑这个简单的程序:
var amount = 99.99;
amount = amount * 2;
console.log(amount); // 199.98
// convert `amount` to a string, and
// add "$" on the beginning
amount = "$" + String( amount );
console.log(amount); // "$199.98"
变量amount最开始保存了数字99.99,然后保存了amount*2
的结果,数字199.98。第一个console.log(...)
命令将number类型的值隐式地转换为string类型并打印出来。
然后语句amount="$"+String(amount)
显示地将值199.98转换为string类型,然后在前面加上字符$
。现在变量amount保存的是string类型的值"$199.98"
,所以第二个console.log(...)
在输出时就不需要做类型转换了。
JavaScirpt开发者要注意使用amount变量的灵活性,它的值可以是99.99、199.98和"$199.98"。为便于区分,喜欢使用静态类型的人可能更喜欢用amountStr
来保存最后的值"$199.98",因为这是另外一种类型的值了。
无论使用那种方式,你需要注意的是变量amount保存了一个随着程序运行而变化的值,这正式变量的主要作用:管理程序 状态。
也就是说,状态 用于追踪程序运行时值的变化。
变量的另一个作用是统一管理值的设置。当你声明了一个变量并赋值,在整个程序中这个变量的值都不会改变时,这个变量就称为 常量。
通常在程序的顶部声明这些 常量,方便需要改变常量的值时能够快速定位到。按照惯例,JavaScirpt中的常量应该大写,并以_
来连接单词。
这里有个简单的例子:
var TAX_RATE = 0.08; // 8% sales tax
var amount = 99.99;
amount = amount * 2;
amount = amount + (amount * TAX_RATE);
console.log( amount ); // 215.9784
console.log( amount.toFixed( 2 ) ); // "215.98"
注意: 跟console.log(...)中log(...)是console对象的一个属性一样,这里的toFixed(...)是number类型值的一个函数属性。JavaScript中number不会自动格式化为美元格式——引擎不知道你要格式化成什么,因此没有货币的对应类型。toFixed(...)用于指定number类型的值小数点后保留的位数,并返回string值。
照例,变量TAX_RATE表示的是一个常量——其实上面的程序中它的值也是能够被改变的。但是如果将营业税率提高至9%,我们只需在一处将TAX_RATE的值设置为0.09就可以了,而不用遍历整个程序找到所有值为0.08的地方,然后把它们都改为0.09。
最新版的JavaScript(ES6)引入了用const
关键字来声明 常量 新方法,而不是用var
:
// as of ES6:
const TAX_RATE = 0.08;
var amount = 99.99;
// ..
常量与保存不变值的变量一样,是很有用的。除此之外,常量还能够在初始化之后阻止意外改变常量值的行为。如果你在第一次声明之后尝试给TAX_RATE重新赋值,程序会拒绝这个改变(在严格模式中会报错——见第二章中的“严格模式”)。
另外,这种防止出错的“保护机制”与静态类型的强制类型转换相似,因此你会发现其他语言中的静态类型是很优美的!
注: 更多有关程序中变量支持的不同类型的值,请参见 类型&语法 一书。
代码块
你到手机店买新手机时,店里的员工需要经过一系列步骤才能完成结账。
类似地,写代码时我们经常需要将一系列语句组合在一起,我们称之为 代码块。在JavaScript中,代码块由一对花括号{...}
包裹的一条或多条语句组成。如:
var amount = 99.99;
// a general block
{
amount = amount * 2;
console.log( amount ); // 199.98
}
这种独立使用{...}
的普通代码块是合法的,不过在JS程序中不常见。通常,代码块与其他的控制语句一起使用,如if
语句或循环语句。例如:
var amount = 99.99;
// is amount big enough?
if (amount > 10) { // <-- block attached to `if`
amount = amount * 2;
console.log( amount ); // 199.98
}
下一节中我们会讨论if语句,上面的程序中,if(amount>10)
后面紧接着{...}
及其中的两条语句;之后if条件语句成立时,代码块内的语句才会被执行。
注: 与console.log(aomunt)
等语句不一样,代码块语句后面不需要加分号;
条件语句
"多加$9.99即可买一张屏幕保护膜,需要吗?"友好的手机店店员请你做一个选择。而你首先需要考虑你的财务状况再来回答这个问题。但很明显,这是一个简单的“yes or no”问题。
在程序中我们可以用很多方式来表示 条件判断 (也即判断)。
最常见的是if
语句,实际上表达的意思是“如果这个条件成立,就做下面的事”。例如:
var bank_balance = 302.13;
var amount = 99.99;
if (amount < bank_balance) {
console.log( "I want to buy this phone!" );
}
if语句的( )
中要求有一个结果为true或false的表达式。上面的例子中,我们的表达式是amount<bank_balance
,实际上表达式的结果要么为true,要么为false,这取决于bank_balance变量值的大小。
如果条件不成立,还可以增加一个else子句,如:
const ACCESSORY_PRICE = 9.99;
var bank_balance = 302.13;
var amount = 99.99;
amount = amount * 2;
// can we afford the extra purchase?
if ( amount < bank_balance ) {
console.log( "I'll take the accessory!" );
amount = amount + ACCESSORY_PRICE;
}
// otherwise:
else {
console.log( "No, thanks." );
}
如果amount<bank_balance
为true,则会输出“I'll take the accessory!”,且amount变量加9.99。如果不成立,则执行else子句,礼貌地输出“No, thanks.”,amount保持不变。
我们在“值&类型”一节中讨论过,如果值的类型不是期望的类型会被强制转换为期望的类型。if语句需要boolean类型的值,但是如果传入的不是boolean类型值,就会做强制类型转换。
JavaScript定义了一些被当做“假值”的特定值,因为当它们被强制转换为boolean类型时,它们的值会变为false
——这些值包括0
和""
等。任何其他的值则自动表示“真值”——当被强制转换为boolean类型时,它们的值为true
,如99.99和"free"等。详细参加第二章的“真值&假值”一节。
除了if之外,条件语句 还有其他形式。如switch
语句可以用来简化一系列的if...else
语句(见第二章);循环语句用一个条件语句来判断继续循环还是停止。
循环
在业务高峰期,顾客们排着长队等待店员的服务。只要队伍中还有人在排队,店员就需要继续服务下一个顾客。
重复一系列动作直到某个特定的条件不成立——或者说,条件成立时一直重复——就是循环语句干的活;循环语句有不同的形式,但是它们做的事情基本都是这样的。
一个循环包含一个测试条件和一个代码块。循环代码块执行一次,称为一次 迭代。
例如,while
循环和do...while
循环表示重复执行一个代码块直到条件不在为true:
while (numOfCustomers > 0) {
console.log( "How may I help you?" );
// help the customer...
numOfCustomers = numOfCustomers - 1;
}
// versus:
do {
console.log( "How may I help you?" );
// help the customer...
numOfCustomers = numOfCustomers - 1;
} while (numOfCustomers > 0);
这两个循环语句的不同之处在于条件的判断是在第一次迭代之前(while)还是在第一次迭代之后(do...while)。不管是那种形式,如果条件判断为false,则不再执行下一次迭代。这意味着,如果初识条件为false,那么while循环将永远不会运行,但是do...while循环仍旧会运行一次。
有时候需要让循环执行给定数字表示的次数来实现某种功能,如从0到9(10个数字)。可以设置循环的初始变量如i为0,然后在每次迭代中自加1。
注意: 出于诸多历史原因,编程语言中几乎总是从0开始计数,而不是同1开始。如果不习惯这种思维,会觉得很困扰。花点时间来练习从0开始计数并习惯这种方式吧!
每次迭代都会做条件判断,就好像循环中有一个隐式的if语句一样。
我们可以用JavaScript的break
语句来中止一个循环。当然我们会发现很容易就会写一个不能被中止的死循环。
例如:
var i = 0;
// a `while..true` loop would run forever, right?
while (true) {
// stop the loop?
if ((i <= 9) === false) {
break;
}
console.log( i );
i = i + 1;
}
// 0 1 2 3 4 5 6 7 8 9
除了while和do...while之外,另一种循环的语法格式是for
循环:
for (var i = 0; i <= 9; i = i + 1) {
console.log( i );
}
// 0 1 2 3 4 5 6 7 8 9
可以看到,两个例子中前10次迭代的条件i<=9
都为true(i的值从0到9),但是i值为10时则变为false。
for循环有三条子句:初始化子句(var i=0)、条件测试子句(i <=9)以及更新子句(i = i+1)。所以如果你想用循环来计数的话,for循环更加简洁且便于理解和书写。
还有其他的用于对特定值进行迭代的特殊循环形式,如迭代对象的属性(见第二章),它的隐性条件测试为是否所有的属性都被处理了。不管是何种形式的循环,“条件不成立则中止循环”的概念都是适用的。
函数
手机店店员可能没有带计算器,但是她需要计算出税费和应付金额。这是一个定义一次,可以反复使用的工作。公司很有可能有一个内置了这些功能的结账寄存器(电脑、平板等)。
相似地,你的程序几乎肯定会将代码完成的任务分成一个个可重复使用的片段,而不是你自己反反复复地重写。我们可以通过定义function
来实现。
函数通常是一个命名的且可以通过该名字被调用的代码片段,每次被调用时,函数内部的代码都会执行一遍。例如:
function printAmount() {
console.log( amount.toFixed( 2 ) );
}
var amount = 99.99;
printAmount(); // "99.99"
amount = amount * 2;
printAmount(); // "199.98"
函数也可以接收参数——你传入的值。也可以选择性地返回一个值。
function printAmount(amt) {
console.log( amt.toFixed( 2 ) );
}
function formatAmount() {
return "$" + amount.toFixed( 2 );
}
var amount = 99.99;
printAmount( amount * 2 ); // "199.98"
amount = formatAmount();
console.log( amount ); // "$99.99"
函数printAmount(..)接收一个叫做amt的参数。函数formatAmount()返回一个值。当然我们也可以在同一个函数中既接收参数又返回值。
通常将需要被多次调用的代码定义成函数,但是有时候尽管你只会调用一次,将代码组织成命名的片段集合也是很有用的。如:
const TAX_RATE = 0.08;
function calculateFinalPurchaseAmount(amt) {
// calculate the new amount with the tax
amt = amt + (amt * TAX_RATE);
// return the new amount
return amt;
}
var amount = 99.99;
amount = calculateFinalPurchaseAmount( amount );
console.log( amount.toFixed( 2 ) ); // "107.99"
尽管calculateFinalPurchaseAmount(..)只被调用了一次,将它的行为组织为一个单独的命名函数使得使用这段逻辑的代码更加清晰。如果函数有很多条语句,这样做带来的好处将更加明显。
作用域
如果你向店员询问这个手机店没有的手机型号,店员将没法买给你你想要的手机。她只能拿到她的店里库存的手机。你不得不换一家店看能不能找到你想要的手机。
编程中这个概念的术语是:作用域(技术上称为 词法作用域)。在JavaScript中,每个函数都有自己的作用域。作用域实际上包括变量的集合和通过名字访问这些变量的规则。只有函数内部的代码才能访问该函数作用域内的变量。
在同一个作用域内的变量名必须是唯一的——不能同时存在两个a变量。但是同一个变量名a可以出现在不同的作用域中。
function one() {
// this `a` only belongs to the `one()` function
var a = 1;
console.log( a );
}
function two() {
// this `a` only belongs to the `two()` function
var a = 2;
console.log( a );
}
one(); // 1
two(); // 2
当然,一个作用域可以被嵌套在另一个作用域内,就好像生日Party上小丑刺破一个气球,然后刺破这个气球里面的气球一样。如果一个作用域被另一个作用域嵌套,那么最内层作用域中的代码可以访问到其他作用域中的变量。 如:
function outer() {
var a = 1;
function inner() {
var b = 2;
// we can access both `a` and `b` here
console.log( a + b ); // 3
}
inner();
// we can only access `a` here
console.log( a ); // 1
}
outer();
词法作用域的规则:一个作用域中的代码能够访问这个作用域中的变量,也能访问到这个作用域外面的任何作用域中的变量。 因此函数inner()内的代码可以同时访问变量a和b,但是outer()只能够访问a——不能访问b,因为它只存在与inner()内部。
回忆前面的代码片段:
const TAX_RATE = 0.08;
function calculateFinalPurchaseAmount(amt) {
// calculate the new amount with the tax
amt = amt + (amt * TAX_RATE);
// return the new amount
return amt;
}
因为有词法作用域,常量TAX_RATE可以在函数calculateFinalPurchaseAmount(..)内被访问到,尽管我们没有将其传入函数中。
注: 更多词法作用域的知识,请参考 作用域&闭包 一书的前三章。
练习
学习编程,除了多练习绝对别无它法。仅仅阅读我写的这些表达性文字不能使成为一名程序员。
牢记这一点,现在我们来练习在本章中学到的了一些概念。我给出要求,你们先自己动手尝试,然后参考我在下文给出的代码,看我是怎么实现的。
- 写一个程序计算你购买手机所需的总金额。你一直买直到你的银行卡上没钱了。只要售价低于你的心理预期值,你就会给每台手机购买配件。
- 计算好你的消费总额之后,加入税费,输出计算后的消费额,需要格式化。
- 最后,检查你的银行账户余额,看是否足够支付。
- 你应该将“税率”、“手机价格”、“配件价格”及“心理预期值”设为常量,将银行账户余额设为变量。
- 你应该定义函数来计算税费、格式化价格(添加$,保留2位小数)。
- 加分项:在程序中加入输入,可以用prompt(..)函数。例如,你可以提示用户输入银行帐号余额。 好了,开始练习吧。在你自己尝试之前不要偷看我下面的代码!
下面是我用JavaScript写的参考答案:
const SPENDING_THRESHOLD = 200;
const TAX_RATE = 0.08;
const PHONE_PRICE = 99.99;
const ACCESSORY_PRICE = 9.99;
var bank_balance = 303.91;
var amount = 0;
function calculateTax(amount) {
return amount * TAX_RATE;
}
function formatAmount(amount) {
return "$" + amount.toFixed( 2 );
}
// keep buying phones while you still have money
while (amount < bank_balance) {
// buy a new phone!
amount = amount + PHONE_PRICE;
// can we afford the accessory?
if (amount < SPENDING_THRESHOLD) {
amount = amount + ACCESSORY_PRICE;
}
}
// don't forget to pay the government, too
amount = amount + calculateTax( amount );
console.log(
"Your purchase: " + formatAmount( amount )
);
// Your purchase: $334.76
// can you actually afford this purchase?
if (amount > bank_balance) {
console.log(
"You can't afford this purchase. :("
);
}
// You can't afford this purchase. :(
怎么样?现在看过我的代码之后再返回去试一试吧。试着改变一些常量的的值,看看程序的运行结果会有什么不同。
复习
学习编程不一定是一个复杂而艰难的过程,你只需在脑海中谨记一些基本概念。
这个过程就更积木游戏一样,想搭建一座高塔,首先需要一块一块堆叠开始。编程也是这样的。下面是一些编程中必要的砖块:
- 你需要 操作符 来对值进行操作
- 你需要值和 类型 来执行不同的操作,如对number进行计算,或输出string。
- 在程序执行时,你需要 变量 来保存数据(也即 状态)
- 你需要类似if语句一样的 条件语句 来做判断
- 你需要 循环 来重复执行特定工作,直到条件变为false
- 你需要 函数 将代码组织成逻辑性强、可复用的代码块。
使用代码注释可以使代码更具可读性,使得你的代码更易于理解、便于维护、如果出现bug也更好解决。
最后,不要忽略练习的重要性。学习怎么写代码的最佳方法是动手写代码。
现在,我很高兴你在学习如何编写代码!继续坚持。别忘了查阅其他的编程入门资料(书籍、博客、在线培训等)。学习本章及本书是一个不错的开始,但是这仅仅是简要介绍而已。
下一章将重温本章中的很多概念,但是会更多地从JavaScript的角度出发,突出本系列剩下的内容中将进行深入讨论的主题。