第4章 JavaScript基础

4.1 JavaScript语言的发展


JavaScript常简称为JS,是一种被设计在浏览器中执行的脚本语言。

  • 4.1.1 JavaScript与Java
  • 4.1.2 JavaScript的标准
  • 4.1.3 ES6与非浏览器环境
  • 4.1.4 基本语法

4.1.1 JavaScript与Java


Java语言在1991年开始设计,最初被期望运行在嵌入式设备(比如电视机)。但当时没有合适的硬件可以 应用。根据其设计目标,当时的Java语言具有对资源要求低,能较好适应不同硬件的优势。

在那时,万维网的页面只由HTML构成,网页一旦加载,内容就固定了,无法动态更新,也缺乏和用户交互。 1995年,当时最受欢迎的浏览器的开发商网景公司决定开发使网页具备动态交互能力的技术。他们的方案 之一就是在HTML页面中嵌入Java程序

同时,他们也希望开发了一种类似Java的脚本语言,这种语言就是JavaScript。但实际上JavaScript 是一种全新的语言,在很多方面与Java有明显的不同。

但当时Java和JavaScript都可以在浏览器中运行,用来赋予网页与用户实时交互的能力。今天在浏览器 中JavaScript凭借自身的优势已经完全取代了Java。现代浏览器几乎都不支持直接运行Java程序,而 Java也主要应用在服务器应用开发,桌面应用开发和安卓应用开发等领域。

4.1.2 JavaScript的标准


JavaScript的标准是ECMAScript或者ECMA-262。ECMA指ECMA国际(ECMA International),前身是 欧洲计算机制造商协会(European Computer Manufacturers Association)。ECMA国际是一个国际 性的计算机行业的标准化组织,创建了很多计算机领域的标准,例如光盘的CD-ROM标准(后来也被国际标准化 组织批准为ISO 9660)。

ECMAScript标准由网景公司推动,在1997年,其第一个版本被ECMA批准。ECMAScript标准规定了JavaScript 的语法和基本的API。但没有规定语言的具体实现方法,事实上JavaScript有多种不同的实现。

ECMAScript标准在不断更新。5.1版本发布于2011年。 版本6发布于2015年,也常简称为ES6,这一版本中引入了可以声明局部变量的let关键字和声明常量的const 关键字。还引入了class关键字用于声明类。

版本7发布于2016年,版本8发布于2017年,版本9发布于2018年。版本10和11分别发布于2019和2020年。

4.1.3 ES6与非浏览器环境


网景最初设计JavaScript目的是让网页具有动态更新的能力,JavaScript被设计在浏览器中运行,通过文档 对象模型和网页中的元素交互,通过网络接口访问网络资源。

随着以node.js为代表的JavaScript运行环境的出现,JavaScript开始涉足更多的领域。让JavaScript可以像 其他服务器端语言一样可以开发独立的而不是只能在浏览器中运行的程序。

而ES6标准向JavaScript中引入了大量新内容,从功能层面使JavaScript更能胜任大规模的软件的开发任务。

随着Electron的出现,JavaScript又被广泛用于桌面应用的开发。

4.1.4 基本语法


从本节开始我们将聚焦JavaScript语言的细节。作为一种语言,首先关注的是它的语法,即弄清楚在 JavaScript中怎样的句子是正确的。计算机语言的语法是严格的,如果一个语句不符合语法,那么这个句子 就没有意义,也无法被执行,如果一个句子是符合语法的,那么这个句子必然有明确的含义,而不会有歧义。

JavaScript程序由语句构成,语句又由字面值、标识符和运算符构成。如下面一段程序计算了3+4的结果,并随后 把结果输出到控制台。


								 	
							

JavaScript的语句通常以英文的分号结尾。这段程序由两个语句构成。双斜线“//”后面的内容是注释,执行代码 时会直接忽视注释内容。JavaScript还支持跨行注释,使用“/*”作为开始记号,“*/”作为结尾记号。

4.2 使用JavaScript变量


变量是一个符号,用来代表一个可以被改变的值,通常对应计算机内存里的一个空间。可以用来存储计算结果等。 一个变量本身就是一个表达式,变量的值就是表达式的值。

  • 4.2.1 类型与声明
  • 4.2.2 作用域
  • 4.2.3 数组

4.2.1 类型与声明


变量通常需要先声明后使用。每个变量需要都有一个确定的类型。JavaScript是弱类型语言,即变量的类型无需 固定。强类型语言的变量一旦声明,类型就固定了,比如声明了“整数”类型的变量,这个变量名就不能再作为 字符串类型的变量使用,强类型语言通常需要在声明变量时指明变量类型(也可以是隐式的指明)。

JavaScript在声明变量时,无需指定类型,而且变量类型在使用过程中可以随时改变。

4.2.2 作用域


作用域指变量在代码中可以被使用或者说可以起作用的“区域”。JavaScript中使用大括号“{}”划分代码块。使用 let声明的变量的作用域被限制在一个代码块中。而全局变量的作用域是整个代码文件。

4.2.3 数组


数组是由多个变量构成的对象,这些变量被称为数组的元素,可以使用“[ ]”定义一个空的数组,也可以用字面值 初始化一个含有元素的数组,如“["January ", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]”定义了包含十二个月的 英文名称字符串的数组。数组元素可以是任意的类型。

4.3 运算符


运算符是代表某种特定操作的符号,比如通常计算机语言都会提供如加减乘除这样的基本的数学运算符。运算符 会对一个或多个输入做特定操作,运算符的输入被称为操作数。运算符和操作数共同组成了新的表达式

  • 4.3.1 运算符的种类
  • 4.3.2 算数运算符
  • 4.3.3 逻辑运算符
  • 4.3.4 关系运算符
  • 4.3.5 位运算符
  • 4.3.6 赋值运算符
  • 4.3.7 其他运算符
  • 4.3.8 运算符的优先级

4.3.1 运算符的种类


按照运算符接受的操作数的数量可分为一元运算符(也称单目运算符)、二元运算符等。按照使用场景可分为算术 运算符、逻辑运算符、关系运算符、位运算符等。

4.3.2 算数运算符


“+”运算符代表加法,用于求两个数的和。“-”、“*”、“/”分别代表减法、乘法、除法,它们都是二元运算符。

“%”是取模运算符,也就是求两数除法的余数。

“+”和“-”还可以作为一元运算符使用,用于表示数字正负。

4.3.3 逻辑运算符


逻辑运算符返回的结果是布尔类型,就是说只有true和false两种结果。“&&”是逻辑与运算符,任何值逻辑与 true的结果仍是原值,任何值逻辑与false的结果都是false。“||”是逻辑或运算符,任何值逻辑或false 仍是原值,任何值逻辑或true仍是true。它们都是二元逻辑运算符。

“!”是非运算符,true的非运算是false,false的非运算是true。

“??”是空值合并运算符,该运算符前面的值如果是null或者undefined,则运算结果是运算符后面的值, 否则结果是前面的值。

4.3.4 关系运算符


“in”运算符用来判段对象是否包含某个属性。与某些语言不同,“in”运算符不能判断数组是否包含某个元素。

“instanceof”用于判断一个对象是否是另一个对象的实例。

“<”、“>”、“<=”和“>=”分别是“小于”、“大于”、“小于等于”和“大于等于”,用于比较两个值的大小关系。

“==”(连续的两个等号)、“!=”用于比较两个是否相等和是否不等于。“===”(连续的三个等号)和“!==” (叹号和连续的两个等号)用于判断两个值(包括类型)是否严格相等和是否严格相等。

4.3.5 位运算符


计算机内部数据是以二进制的形式存储的。所以计算机语言通常会提供位运算符以支持二级制位的运算,通常 是针对整数的运算。

4.3.6 赋值运算符


前面已经使用过“=”用于把符号右边表达式的值赋给符号左边的变量。赋值运算符都要求左边的操作数都应该 是可被赋值的表达式,通常是变量。

4.3.7 其他运算符


“++”和“--”是自增、自减运算符,是一元运算符,可以使一个变量的值增加1或减少1。

new和delete两个运算符是用于创建和删除对象一元运算符。

void运算符用于表示一个表达式放弃其返回值。

“.”是属性访问运算符,用于访问一个对象的属性。

条件运算符有一个“?”和一个“:”组成,其中“?”前面需要提供一个表达式,“?”后面又有两个由“:”隔开的表达式。 如果表达式的值是true,则该运算符的值是冒号前面的表达式的值;否则是冒号后面表达式的值

4.3.8 运算符的优先级


JavaScript中的运算符是具有优先级的,这与数学中的运算符优先级比较类似。

4.4 函数


函数是一个代表某种操作的符号。JavaScript提供了一些内置函数,同时也允许用户 创建函数。函数是一种封装技术,它和运算符类似,接收一个或多个输入,并返回一个 结果。或者应该说运算符就是一种特殊的函数。

  • 4.4.1 创建函数
  • 4.4.2 调用函数
  • 4.4.3 函数的返回值

4.4.1 创建函数


创建函数要使用function保留字,并需要指定函数的名称、接收的参数以及函数要执行 的具体操作。比如创建一个名为SumOfConsecutiveInteger的函数用于求一组连续 整数的和,输入是两个整数,第一个代表这组连续整数的第一个整数,第二个代表最后 一个整数,例如输入(1,10)代表求1+2+3+4+5+6+7+8+9+10的值。

通过下面的代码可以在JavaScript中定义上述函数。


function SumOfConsecutiveInteger(a1, an)//这一行是函数声明开始
{   // 花括号内是函数体,这里面定义函数要执行的操作
	let n = an - a1 + 1;
	let S = n * (a1 + an) / 2;
	return S;
}
							

4.4.2 调用函数


使用“函数名(参数列表)”的形式可以调用函数。比如调用上一小节的“SumOfConsecutiveInteger”函数可以用下面的写法。


function SumOfConsecutiveInteger(a1, an)//这一行数函数声明开始
{   // 花括号内是函数体,这里面定义函数要执行的操作
	let n = an - a1 + 1;
	let S = n * (a1 + an) / 2;
	return S;
}
let result_1_100 = SumOfConsecutiveInteger(1, 100);
let result_1_1000 = SumOfConsecutiveInteger(1, 1000);
console.log(result_1_100, result_1_1000); 
						

输出的结果是“5050 500500”。console.log也是函数,调用该函数可以向控制台输出文本。

4.4.3 函数的返回值


函数体内的return语句后面表达式的值就是函数的返回值。如果函数体内没有return语句,则函数 没有返回值,那么函数调用后,调用者将得到undefined。

4.5 流程控制


程序通常是从上到下逐语句执行的,通过流程控制可以改变当前程序执行的顺序,从而实现跳过语句 或者重复执行语句

  • 4.5.1 if…else语句
  • 4.5.2 switch…case语句
  • 4.5.3 循环语句

4.5.1 if…else语句


if语句用于根据条件决定是否执行语句块。

if…else分为if子句和else子句两个部分。if子句后面必须有一个括号,括号内的表达式的值 如果为true,则执行if子句后的语句块,否则执行else子句的语句块。其中else子句可以省略, 如果else子句省略,表达式的值为true执行if的语句块,为false则什么也不执行。

4.5.2 switch…case语句


switch…case语句用于实现多分枝的流程控制。switch后面是一个表达式,之后的语句块中,可以 有多个case子句,每个case也有一个表达式。如果switch后的表达式和case后面的表达式相等, 则会执行这个case后的语句。

4.5.3 循环语句


循环语句用于重复执行指令。循环语句可以执行类似的需要重复操作的任务,可以根据程序执行情况 决定操作重复的次数。循环语句可通过for或者while两种关键字实现。

4.6 内置数据结构


数据结构是计算机用于存储、组织数据的方法,是有结构特性数据的集合。除了4.2节中介绍的基本 数据类型,JavaScript还提供了丰富的内置数据结构,包括Map、Set、Date等。

  • 4.6.1 字符串
  • 4.6.2 Map
  • 4.6.3 Set
  • 4.6.4 日期时间
  • 4.6.5 Number
  • 4.6.6 正则表达式

4.6.1 字符串


字符串就是一串字符或者一串文字。JavaScript中可以直接用一对引号创建字符串(单引号或双引号都可)。引号内的字符就是字符串的内容


let str = "你好!";
						

上面代码创建了一个包含三个字符的字符串。

4.6.2 Map


Map是一种用于存储键值映射的数据结构。键和值也常称作key和value。比如有一个英文单词的数组。


let words = ['to', 'be', 'or', 'not', 'to', 'be', 'that', 'is', 'a', 'question'];
console.log(words.length);	
						

这个数组中有10个单词。如果我们希望统计每个单词出现的次数,可以定义一个Map,并令单词为键,以出现次数为值。


let wordCount = new Map();

for (let word of words) {
if (wordCount.has(word)) { // 判断当前Map中是否已经包含这个单词
    let currentCount = wordCount.get(word);
    wordCount.set(word, currentCount + 1);  // 如果已经包含,则在原来的数量基础上加一
}
else {
wordCount.set(word, 1); // 如果还未包含这个单词,把这个单词的数量设为1
}
}
// 单词数量统计完成

// 下面遍历Map,输出统计结果
for (let pair of wordCount) {
console.log(pair[0], ':', pair[1]);
}		
					    

这段代码输出的结果如下。


to : 2
be : 2
or : 1
not : 1
that : 1
is : 1
a : 1
question : 1
						

通过new Map()创建新的Map对象。通过对象的has方法可以检查一个键是否在这个对象中存在。通过set把几个键值对存入对象。通过get获取一个键对应的值。

Map对象还提供keys()方法和values()方法,用于获取这个对象的所有键以及所有值。delete()方法用于从Map中删除键值对

4.6.3 Set


Set与数组类似,用于存储一批元素,但Set中不能包含重复元素。用add()方法向Set中添加元素,用has()方法判断一个元素是否存在。用delete()方法删除一个元素。

Set允许多次添加重复元素,但无论添加多少次,Set中只会保存一个该元素。与数组类似,Set提供forEach方法用于遍历Set中的元素。

4.6.4 日期时间


JavaScript的Date类型用于表示日期时间。Date类型基于Unix时间戳,是自UTC时间1970年1月1日起以来的秒数。可使用new Date()创建Date类型。

4.6.5 Number


Number是表示数字的对象。整数,小数(或者叫浮点数)都可以使用Number表示。Number对象的方法也就是数字变量所拥有的方法。

4.6.6 正则表达式


正则表达式是一种用于文本操作的工具。正则表达式通过特定符号定义匹配的模式,常用于文本匹配和替换。JavaScript中有RegExp类型的对象表示正则表达式。JavaScript中可使用“/”定义正则表达式常量,类似于字符串常量。

4.7 内置对象


JavaScript的内置对象提供了通用的方法或者常数,使用这些内置对象有利于提高开发效率。有些高级功能也需要内置对象实现

  • 4.7.1 Math对象
  • 4.7.2 JSON对象
  • 4.7.3 全局函数
  • 4.7.4 Web Storage

4.7.1 Math对象


Math对象提供数学中常用的常数和函数。

1. 常数

    Math.E为自然对数函数的底数,是一个无限循环小数。其值约为2.71828。也称自然常数、自然底数,或是欧拉数(Euler's number)。Math.PI是圆周率,是一个无限循环小数,约等于3.14159。

2. 函数

    Math.abs(x)用于计算一个数x的绝对值。Math.ceil(x)和Math.floor(x)分别是向上取整和向下取整。Math.round(x)则是求数字x四舍五入后的整数。Math.trunc(x)用于返回一个数字的整数部分,而不管这个数的正负。Math.sign(x)返回一个数字的符号,负数返回-1,正数返回1, 0返回0

4.7.2 JSON对象


JSON指JavaScript Object Notation,是一种可以用字符串描述(部分)JavaScript对象的数据记录格式。

JSON是一种语法,可以用来描述JavaScript中的对象、数组、字符串、布尔值和null这些数据结构。JSON通常用于数据的传输和储存。

JSON对象提供stringify()函数实现了编码或者序列化,即把JavaScript对象转换为字符串。parse()函数实现了解码或者反序列化,即把 字符串转换为JavaScript对象。

4.7.3 全局函数


parseInt(字符串,基数)用于解析字符串,并得到整数。

parseFloat(字符串)用于解析字符串得到浮点数。

eval(字符串)用于把一个字符串作为JavaScript代码执行。

btoa和atob两个方法用于Base64编码和解码。

4.7.4 Web Storage


Web Storage提供一种基于浏览器的简单易用的持久化的键值存储。

键值存储指的是类似于Map数据结构提供的接口,要存储的数据分为键(Key)和值(Value)两部分。可以通过键写入或者读取值。

持久化值数据不会随页面关闭也丢失。4.6节介绍的数据结构只能临时保存数据,当用户关闭页面、刷新页面或者关闭浏览器时,那些数据结构中存储的值都会丢失。但Web Storage中存储的数据在页面关闭或甚至计算机关闭后都可以保存。

基于浏览器指这些数据由用户的浏览器保存,这些数据仅仅保留在用户本地,而且如果浏览器被卸载或者损坏,这些数据可能丢失,同时用户可以随时清理这些数据。

Storage提供sessionStorage和localStorage两种实现。sessionStorage中存储的数据会在当前session结束时被清理,localStorage则不会。

4.8 小结


本章主题是JavaScript语言,是HTML5开发中的“控制器”,直接接收用户输入,并能快速给出反馈。

JavaScript可以实现复杂的计算逻辑,是一种完备的程序设计语言。同时JavaScript和浏览器生态紧密结合,是开发HTML5应用最佳的选择。

4.9 课堂练习—开发HTML5计算器


开发一个在浏览器中运行的计算器,实现最基本的计算功能,支持通过输入按钮输入要计算的表达式,对于表达式计算不支持处理运算符 优先级,也不支持括号。

  • 4.9.1 计算器界面
  • 4.9.2 输入按钮事件的处理
  • 4.9.3 实现功能

4.9.1 创建计算器界面


计算器界面由“输入区”、“结果区”、“数字输入按钮”、“运算符输入按钮”和“功能按钮”组成。用户点击输入按钮后,输入区显示已输入内容,结果区则输出计算结果。数字输入按钮是0到9共10个数字以及负号、小数点。运算符有加减乘除、阶乘、根号和平方。功能按钮有等号和清除按钮(C按钮)。

4.9.2 输入按钮事件的处理


第二章介绍过,点击HTML元素会触发onclick事件。可以使用JavaScript实现函数钮向输入框中插入指定字符,然后把所有输入按钮的onclick属性设置为上述函数。可通过函数参数实现不同按钮输入不同字符。

4.9.3 实现功能


先明确各运算符的语法,“+”、“-”、“*”、“/”、“!”、“√”和“^”分别表示“加”、“减”、“乘”、“除”、“阶乘”、“根号”和“平方”。

实现一个函数function calculate()用于计算“输入区”中的表达式。

4.10 课堂练习—2048小游戏实现逻辑


上一章3.10课堂练习实现了2048小游戏的方格绘制,支持适应屏幕大小和根据参数绘制方格数量。本章将实现小游戏的逻辑。

  • 4.10.1 在方格内填入数字
  • 4.10.2 在空白格子中随机填入数字
  • 4.10.3 合并数字操作
  • 4.10.4 处理用户输入

4.10.1 在方格内填入数字


3.10中,使用了rect标签绘制方框,对于文本,可通过text标签绘制。text标签内的文字就是要显示的文字,它的x和y属性控制 文字的位置,fill属性控制填充颜色。

4.10.2 在空白格子中随机填入数字


在游戏初始化时或者每次用户合法操作后,都需要随机选择一个空格子填入数字。游戏初始化时,填入两个数字“2”和“4”。用户操作后则填入“2”。

为了实现随机选择格子,需要生成随机整数,可以借助Math.random函数实现一个生成指定范围整数的函数


function get_random(n) {
	return Math.round(Math.random() * n);
}		
						

该函数返回0到参数n之间的随机整数(包含0和n)。

为简便可直接随机获取格子,如选到的格子不是空的则重新选择,直到选到空格子为止。


function add_a_number_in_empty_block(num) {
	if (!num) {
		num = 2;  // 默认数字是 2
}
// 下面代码用于检查是否还有空格子
	empty_block = 0;
	for (let i = 0; i < cnt; i++) {
		for (let j = 0; j < cnt; j++) {
			if (mp[i][j] === 0)
				empty_block ++;
		}
	}
	if (empty_block === 0) {
		return 0;    // 此时没有剩余的空格子
	}
	let x, y;
	while (true) {   // 不断随机选取格子,有可能选到非空格子,选到非空格子时会自动重新选择
		x = get_random(cnt-1);
		y = get_random(cnt-1);
		if (mp[x][y] == 0) {  // 如果格子是空格子则退出当前循环
			break;
		}
	}
mp[x][y] = num;  // 向选中的格子填入数字
return num;
}
						

至此向随机的空白格子填入数字的功能完成。如下图所示,执行这些代码后,除了3.10中绘制的棋盘格外,还会在随机的格子中显示一个数字2和一个数字4。

4.10.3 合并数字操作


2048游戏中,玩家可选择四种方向对棋盘格上的数字进行合并,即水平方向的向左、向右和垂直方向的向上、向下。可以分别实现水平和垂直两个方向的合并函数,并通过参数控制合并的方向。

以向右合并为例,合并的操作可以描述为,先把棋盘格上所有的数字都尽量往右移动,即如果一个数字的右侧有空格格子则把数字移动到这个格子。然后如果一个数字右侧紧邻一个相同数字,则把当前数字加到右侧数字上,当前数字的格子清空。如下图:

执行向右合并后的结果如下图:

纵向合并的方法也类似,这里不再具体介绍。

4.10.4 处理用户输入


主要考虑支持触屏和键盘操作。对于触屏操作,可通过向body元素添加ontouchstart、ontouchmove和ontouchend三个事件的监听,它们分别对应了触摸开始、触摸点移动、触摸结束。实现函数touchStartHandler用于处理触摸事件。


let startx, starty;  // 在函数外定义变量用于保存触摸开始事件的 x, y 坐标
function touchStartHandler(e, t) {  // 参数 e 是触摸事件,参数 t 则用于区分前文所述的三种触摸事件
    // 这段代码暂时为处理 t === 1 的情况
	e.preventDefault();  // 阻止事件的默认行为
    if (t === 0) {  // 如果是 ontouchstart,则记录触摸事件的 x,y坐标
        startx = e.changedTouches[0].pageX;
        starty = e.changedTouches[0].pageY;
    }
    let r = false;
    if (t === 2) { // 对于触摸结束则计算次此触摸的方向并调用相应的合并操作的函数
        let deltaX = e.changedTouches[0].pageX - startx;
        let deltaY = e.changedTouches[0].pageY - starty;
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            r = mergeX(deltaX);
        }
        else {
            r = mergeY(deltaY);
        }
        if (r) {        
            add_a_number_in_empty_block ();
            putNumber();
        }
    }
}
					    

然后向HTML代码中添加body标签,并把touch相关的事件绑定到上面的函数。


<body ontouchstart=" touchStartHandler(event, 0)" 
ontouchmove="touchStartHandler(event, 1)"
    ontouchend="touchStartHandler(event, 2)">
						

对于键盘输入,可以通过document的onkeydown事件监听所有按键,并针对不同按键做出操作。实现函数keydownHandler用于处理方向键输入。


function keydownHandler(e) {
	let r = false;
	switch(e.keyCode) {
		case 37: // ←
			r = mergeX(-1);
			break;
		case 38: // ↑
			r = mergeY(-1);
			break;
		case 39: // →
			r = mergeX(1);
			break;
		case 40: // ↓
			r = mergeY(1);
			break;
	}
	if (r) {        
		add_a_number_in_empty_block ();
		putNumber();
	}
}
						

该函数接收键盘按下事件,通过switch case语句,只对上下左右四个方向键做处理。分别调用mergeX和mergeY两个函数,最后在合并操作合法的情况下,在随机的空格子里放入数字“2”,并重新绘制所有数字。需要通过下面的代码把document的keydown事件指向该函数。


document.onkeydown = keydownHandler;			
						

至此,实现了2048小游戏通过触屏和键盘的操作。