第 5 章 JavaScript基础 JavaScript最早源自网景(Netscape)公司,1996年,网景将JavaScript提交给 ECMA(前身为EuropeanComputerManufacturesAssociation)进行标准化,1997 年6月,ECMA 以JavaScrit语言为基础制定了ECMAScrit标准规范 ECMA-262 。 pp 与HTML和CSS不同,前两者是W3C的推荐标准,而JavaScript是ECMAScript 标准规范的实现。除JavaScript外,其他语言也实现了ECMAScript标准,如微软 早期的JScript,但因JavaScript最为著名,因此通常提到ECMAScript指的就是 JavaScript。 最初,JavaScript更多是作为一种运行于浏览器中的小脚本语言,为程序员提 供与浏览器交互的能力,但如今它已走出了浏览器的局限,也可运行于非浏览器 环境,其应用领域远远超出了最初的范围。2015年6月,ECMAScript第6版发布 (常简写作ES6或ES2015),在此版本中有许多重大更新。目前多数情形下,若不 特别声明,程序员口中的JavaScript指的即是ES6之后的版本,本书内容基于ES6 讲解,也加入了一些ES6之后版本的内容。目前,ECMAScript是一个相对活跃的 标准,自2015年起,直至本书出版时,每年ECMA均发布了新的ECMAScript标 准,目前通常使用年份作为版本号,如2022年6月正式发布的版本为ECMAScript 2022 。读者可随时关注新版本的变化,若运行环境支持,不妨大胆使用新的语法 特性 J 。 avaScript虽然在名称上与Java相似,但应注意JavaScript与Java语言有本 质上的区别。其一,JavaScript是一种解释型编程语言,简单地说,JavaScript的代 码会在程序运行时被JavaScript解释器(引擎)逐句解释为机器代码执行,Java则 是一种编译型的语言,它的源代码需要经编译器预编译才可执行。其二, JavaScript是一种动态语言,同时也是一种弱类型语言,而Java是静态的强类型语 言。当然,就程序设计语言本身来说,并无本质上的优劣之分,关键看应用场景。 编译型语言因其预先编译往往带来更高的执行效率,静态语言可帮助程序员发现 很多数据类型兼容问题。 如果读者有学习C/C++/Java的经验,会看到JavaScript的许多基本语法与 它们相似,通常把这类语言称作“类C语言”。因此,本书并不打算按部就班地介 绍JavaScript,多数时候会引导读者换一个角度思考,从需求出发,从程序设计的 理念来学习和理解。当然,即使读者从未接触过任何程序设计语言也不必担心, 第5章JavaScript基础99 本书将由浅入深逐步展开,配合精心准备的示例,让读者学有所得。 图灵奖获得者NiklausEmilWirth曾写过一本书Algorithms+DataStructures= Programs《算法+数据结构=程序》,这句话也成了计算机科学的名言。我们至少可从这句 话中读出以下两个信息。 .计算机程序处理的核心内容是数据,因此首先要解决的问题即是如何合理地表达和 存储数据。 .计算机程序按照一定的算法对数据进行处理,最终输出结果。 因为不同类型的数据在计算机内部有着不同的表达、存储、运算方式,因此发展出了“数 据类型”的概念。计算机程序设计语言中,一般都包括如下基本数据类型。 (1)数值型:如1、-2 、3.14,因内部存储机制的不同,数值型又被细分为定点数 (integer,整数)和浮点数(float,小数) 。根据存储空间大小的不同,以及是否表达负数,又可 再细分为更多子类型。 (2)字符型:如字母、汉字,狭义地指单个的字符(char), 广义地包含字符串(string) 。 (3)布尔型:只表达真(true)或假(false) 。 为了使用方便,许多程序设计语言中还会支持更高级的数据类型,但通常由以上基本数 据类型来表达。 如果数据项之间没有关系,则称作离散型数据,而进入计算机世界的数据之间往往是有 关联的,为表达这种数据之间的关系,发展出了“数据结构”的概念,即定义一定的存储结构 来表达数据之间的关系,同时定义针对不同数据类型和数据结构的运算/操作规则。 读到这里,读者也许会有这样的疑问:这与我们要学习的知识有何关系? 如果读者有 学习任何一门程序设计语言的经验,不妨将教材翻开对照一下,通常第一部分介绍的就是以 上内容在具体程序设计语言中的实现:数据类型、变量、常量、数组、运算符…… 若是面向对 象语言,也许会涉及一些常用的高级数据类型,如String、Date,以及它们所支持的操作。这 些概念其实就可归入上述三个层次:数据类型、数据结构、运算/操作规则。 计算机算法通过将多条语句按一定规则组合在一起以实现特定的功能,这便涉及程序 设计语言中的控制语句。当需要解决的问题变得复杂时,为便于管理和复用,同时保持程序 的可维护性,将相对独立的功能模块进行封装便形成了函数。而在面向对象程序设计语言 中,又更进一步地将数据和功能逻辑封装为一个整体,即对象。 本章内容将围绕上述主题逐层展开,以下是总体规划,希望读者在学习技术细节的同 时,也能理解这些技术所要实现的宏观目标。 1节:介绍常用的数据类型、数据结构和声明方式。 5. 2节:深入讨论数据的存储方式。 5. 3节:介绍基本运算与操作规则。 5. 5.即控制语句。 4节:介绍控制逻辑的表达, 5.即函数的定义和使用。 5节:讨论功能逻辑的封装, 5.即对象和类。 6节:进一步探讨数据与功能逻辑的封装, 1 00 Web应用开发技术(微课版) 视频讲解 .. 5.1 数据类型与数据声明 5.1.1 基本数据类型 基本数据类型也称原始数据类型,其值被称作原始值。JavaScript的基本数据类型包 括:number、bigint、string、boolean、null、undefined和symbol。 1.number JavaScript中,无论是整数还是小数均按number类型处理,始终使用IEEE754标准中 的64位双精度浮点数来表示。 . 表达范围:±1.7976931348623157×10308之间。 . 最小值:±5×10-324,可表达的最接近0的数。 对于整数,可安全表达的范围为±(253-1),超出此范围则不能精确表达,此时可使用 bigint类型。 整数的字面量①可使用十进制、八进制(以0开头)或十六进制(以0x或0X开头)表示。 例如,十进制数14可写作14(十进制)、016(八进制)或0xE(十六进制)。浮点数的字面量 可直接使用十进制数表示,或使用科学记数法,例如,3.14、0.314e1、0.314E1。 以下为常用的number类型特殊值/常量。 . NaN:NotaNumber,即从数据类型看该值属于number类型,但并非合法的数值。 例如,执行"A"*2的结果即为NaN。 .Infinity/-Infinity:正/负无穷。 . Number.MAX_VALUE:1.7976931348623157×10308(number类型最大值)。 . Number.MIN_VALUE:5×10-324,最接近0的正数。 . Number.MIN_SAFE_INTEGER:-253+1,可安全表达的最小整数。 . Number.MAX_SAFE_INTEGER:+253-1,可安全表达的最大整数。 . Number.NEGATIVE_INFINITY:-Infinity,负无穷。 . Number.POSITIVE_INFINITY:Infinity,正无穷。 2.bigint bigint是ES2020新加入的基本数据类型,可以表达任意大小的整数,且不限制其所占 用的字节数。bigint字面量以字母n结尾,例如,3n、-5n。适用于number类型的运算大 部分也适用于bigint类型,但注意表达式中不可混用number和bigint,例如,3+3n是错误 的表达式,应做显式类型转换,如3+number(3n)或bigint(3)+3n。 3.string JavaScript中无论单个字符还是多个字符(字符串)都按string类型处理。string类型 的字面量必须使用单引号(')或双引号(")为定界符。 字符串字面量中若出现特殊字符,应使用反斜杠(\)进行转义,例如,\n(换行,New Line)、\r(回车,CarriageReturn)、\'(单引号)、\"(双引号)。也可使用\xXX 或\uXXXX ① 字面量(literal,直接量)即程序中直接书写的值,如18、3.14。 第5章 JavaScript基础1 01 指定Latin-1或Unicode字符,其中,“X”为十六进制数值,如\u000D(回车符)。 在ES6中增加了反引号(`)可用于定义字符串模板字面量,此时特殊字符无须转义,也 可用于定义多行字符串。下面的代码展示了Hes' oftencalled"Johnny" 这样一个句子,分 别使用单引号、双引号、反引号定义字面量的写法。 He\'s often called "Johnny"' "He's often called \"Johnny\"" `He's often called "Johnny"` 4.boolean 布尔型(boolean)数据的取值只能为true(真)或false(假),称作布尔值或逻辑数。 在布尔上下文①中,false、0、-0、0n(bigint类型的0)、""(空字符串)、null、undefined、 NaN均被认定为false,这些值被统称作falsy(假值),其余情况均认定为true,统称作truthy (真值)。 5.null null表示无效的对象或地址引用。null和undefined有时被统称为nullish。 6.undefined undefined表示未赋初始值的变量,或未获得值的形式参数(形参)。 7.symbol ES6中新增的symbol类型用于表示唯一的、不可更改的值,使用Symbol()函数创建。 常可用作对象属性的键(key)或数据对象的唯一标识(id)。 5.1.2 数据声明 1.变量 程序中若需要空间存储数据,可使用如下代码进行声明(declare)。 let x 以上代码声明了一个变量(variable),x为变量名。此时变量x并没有具体的值,它的 值即为undefined。之所以称作“变量”,是因为其值可以被更改。 作为弱类型的动态程序设计语言,JavaScript中声明变量时无须明确数据类型,而变量 具体的数据类型视其值而定。可看到如下代码中,随着程序逐行向下执行时变量x的值在 变化,其数据类型也随之改变。 let x=3 //x 值为3, number 类型 x='Hello' //x 值变为'Hello', string 类型 x=true //x 值变为true, boolean 类型 代码中的等号(=)意为赋值,即将等号右边的值赋给左边的变量,在声明变量时可以直 接为其赋值(如上述代码第1行)。 “//…”为注释,若需要注释多行内容可使用“/*…*/”。此外,注意JavaScript是一种 区分大小写的语言。 在ES6之前的版本中,声明变量使用关键词var,例如,varx=3,但ES6之后更建议使 ① 布尔上下文(booleancontext):可理解为程序中需要使用布尔表达式的位置,如if(…)的“()”内。 1 02 Web应用开发技术(微课版) 用let,5.4.1节将详细对比它们的区别。 接下来学习如何将JavaScript代码嵌入网页。请创建如下所示的HTML文件,并将 JavaScript代码写入<script>…</script>标签内,浏览器加载页面时便会自动执行。 code-5.1.html <!DOCTYPE html> <html lang="en"> <head> <title>5.1</title> <script> let x=3 console.log(x) //>>>3 (输出x 的值) console.log(typeof x) //>>>number (typeof 取得变量x 的数据类型) x='Hello' console.log(x) //>>>Hello console.log(typeof x) //>>>string x=true console.log(x) //>>>true console.log(typeof x) //>>>boolean </script> </head> <body> <p>请打开"开发者工具"切换至"Console"标签页</p> </body> </html> 以上代码中console.log()用于向控制台输出信息,为便于理解,注释中以“>>>”表示 程序在此行向控制台实际输出的内容。 在浏览器中打开code-5.1.html,并切换至开发者工具的Console标签页(控制台),便可 看如图5.1所示的程序运行结果。 图5.1 在开发者工具中查看控制台输出 更佳的做法是将JavaScript代码移至独立的js文件中(jscode.js),并使用<script>标 签链入,代码如下。 <!DOCTYPE html> <html lang="en"> <head> <!--./jscode.js 为外部js 文件的相对路径 --> 第5章 JavaScript基础1 03 <script src="./jscode.js"></script> </head> <body></body> </html> 为节省篇幅,若不特别说明,下文中的JavaScript代码请读者自行创建上述HTML文 档框架进行实验。为便于阅读,多数情况下本书将HTML、CSS、JavaScript代码都写在一 个HTML文件中,实践中建议将三者分离。 2.常量 在一些情况下,为避免变量的值被更改,可将其声明为常量(constant)。 对于常量,一旦赋值则不可以重新赋值。例如,下面的代码使用关键词const定义了常 量c并将其赋值为3,此后若再对常量c进行赋值程序将报错。 const c=3 c=4 //程序执行至此行将报错: Uncaught TypeError: Assignment to constant variable 因为常量不可重新赋值,所以声明常量时必须为其赋予初始值,如上述第1行代码。 常量与变量的区别仅在于其操作规则的限制(是否可重新赋值),与数据类型以及数据 的表达、存储方式无关。若无特别说明,本书关于变量的描述也同样适合于常量。 在程序设计语言中,标识符(identifier)用于为变量、变量、函数等命名,如前面示例中的 x、c。JavaScript标识符命名规则与其他程序设计语言类似,可以由字母、数字、下画线(_) 和$ 组成,但第一个字符不可以是数字,此外应避免使用语言自身的保留字(关键字)作为 标识符,如let、const、if等。这里所说的字母和数字包括Unicode字符集中的所有字符,因 此也可使用汉字,但不建议这样做。 JavaScript程序中变量名一般使用小驼峰命名风格,即第一个单词首字母小写,其余单 词首字母大写,如userName、lastModified;常量名一般使用全大写的蛇形命名风格,如 USER_KEY。 若读者有使用C/C++/Java等程序设计语言的经验,可能习惯于在每条语句末尾添加 分号(;),但JavaScript中,若语句置于不同行,则语句末尾的分号可以省略,当然有也无妨, 这仅是风格问题,看团队要求或个人习惯而定。 多条let/const语句可合作一行,中间使用逗号分隔,例如: let x=1, y, z=3.14 const c=3, d=4 5.1.3 常用引用类型 除5.1.1节介绍的基本数据类型外,JavaScript中其余类型均属于对象类型,也称作引 用类型,其值被称作引用值。我们将在5.2节进一步探讨基本数据类型与引用类型的区别, 本节介绍常用的几种引用类型。 1.数组 数组(Array)用于封装有序的成组元素。数组字面量中使用中括号括起0个或任意多 个组成数组的元素,其间使用逗号分隔,数组元素可以是任何数据类型,其个数称作数组长 度,可通过访问数组对象的length属性获得。与其他强类型语言不同,JavaScript数组中各 1 04 Web应用开发技术(微课版) 元素的数据类型可以不相同,请参看如下代码。 const arr0=[] //空数组,没有任何元素,长度为0 const arr1=[1, 2, 3] //3 个元素均为number 类型 const arr2=['zhangsan', true, 60.5] // 3 个元素依次为string, boolean, number 类型 当需要访问数组中的某个元素时,可使用元素的索引值(index,序号)来找到它,例如: const arr=['A', 'B', 'C'] let a1=arr[1] //程序运行至此,变量a1 赋值为'B' arr[2]='X' //程序运行至此,arr 为['A', 'B', 'X'] arr[3]='Y' //程序运行至此,arr 为['A', 'B', 'X', 'Y'], console.log(arr.length) //>>>4(当前数组长度为4) arr=['D'] //程序报错:arr 为常量,不可重新赋值 从上面几行代码至少可获得如下知识。 . 数组元素的索引值是从0开始的正整数,即例子中数组元素'A'的索引值为0。 .JavaScript中的数组长度是可变的。 . 即便数组arr本身是常量,也可以改变数组中元素的值,但不可对arr本身重新 赋值。 2.对象 与数组不同,对象(Object)使用键值对(key-valuepair)的形式封装数据,例如: const boy={ //男孩对象 name: '张三', //姓名属性 height: 1.7, / /身高属性 weight: 65 //体重属性 } 上述boy即为对象,其封装了一个男孩的基本信息。大括号“{}”括住的部分描述了该 对象特征:姓名、身高和体重。这些所谓的特征被称作对象的属性(property),其中,name、 heigh、weight为属性名,'张三'、1.7、65 为属性值,属性名与属性值之间使用冒号分隔,属 性之间则使用逗号分隔。属性可以是任意数据类型,当然也包括对象类型。 不仅如此,还可以为对象boy增加一些功能逻辑,例如,计算并返回这个男孩的BMI值 (体质指数=体重除以身高的平方)。请尝试运行如下代码,并观察其输出。 const boy={ name: '张三', height: 1.7, weight: 65, //定义"方法",计算该男孩的BMI 值 getBMI() { //this 指代"这个男孩",this.weight 即这个男孩的体重 return this.weight / (this.height * this.height) } } console.log( boy.name ) //>>>张三 console.log( boy.getBMI() ) //>>>22.49134948096886 上述代码中,getBMI(){…}被称作“方法”(method),它封装了计算BMI的功能, getBMI为方法名,方法名之后小括号“()”中的内容称作参数表(上例为空),再之后的{…} 第5章 JavaScript基础1 05 部分称作方法体,是具体功能的实现。在对象的方法体中可使用this指代当前对象,因而 this.weight即为当前男孩对象的体重,此处等价于boy.weight。方法体中的return语句用 于将计算结果返回。代码的最后两行分别通过访问boy对象的name属性和调用其getBMI() 方法取得该男孩的姓名和BMI数值。 综上所述,对象使用“属性”对数据进行封装,而使用“方法”对功能进行封装,因此对象 是数据和功能的封装体。 方法如果脱离了对象即是函数(5.5节介绍),在许多概念和语法上方法与函数是相通 的,JavaScript中也可以使用如下方式定义方法。 const boy={ ... getBMI: function() {…} } 这样看起来方法更像是对象的一个属性,只是该属性的值是一个函数。 在其他一些面向对象程序设计语言中(如C++/Java等),若要构建对象必须先定义类 (class),然后再创建该类的实例,即对象。而从上面的例子可以看到,JavaScript允许直接 使用对象字面量定义对象,而跳过定义类的步骤。对象与类是面向对象程序设计语言中一 个重要的话题,将在5.6节重点讨论。 3.Set和Map Set和Map是ES6新增的数据类型,它们在一定程度上像是数组和普通对象的补充, Set用于存储一组唯一的元素,而Map以键值对的形式存储数据(以前通常使用对象)。 下面的例子演示了Set和Map的简单用法。 //此处基于数组构建set 对象, 也可不带参数, 使用new Set()语句构建空的set 对象 const set=new Set([1, 2]) set.add(3) //向set 对象中添加新元素 console.log(set) //>>>{1, 2, 3} //此处基于数组构建map 对象, 也可不带参数, 使用new Map()语句构建空的map 对象 const map=new Map([['name', 'Johnny'], ['weight': 65]]) map.set('height', '1.7') //设置height 属性为1.7 console.log(map.get('name')) //>>>Johnny(取得name 属性值) console.log(map.get('height')) //>>>1.7(取得height 属性值) 与数组不同,Set中的元素是唯一的,因此可利用其进行去重操作。 对象的键(属性名)只可使用string或symbol类型,而Map的键可以使用任意类型,除 此之外,在进行遍历、计数等操作时Map有一定优势,因此若只是为封装数据,可使用Map 替代普通对象。 .. 5.2 基本类型与引用类型 5.1节中基本建立了数据类型的概念,也初步认识了一些常用的数据结构。在继续学 习之前,有必要深入地讨论一下前文划分基本数据类型(基本类型、原始类型)与引用类型 (对象类型)的依据到底是什么? 明白这些原理将有助于掌握后续内容。 1 06 Web应用开发技术(微课版) 本节涉及一些暂未介绍的概念,若读者感到晦涩难懂可略读,但本节内容值得精读,后 续章节中会适时指引读者返回本节学习。 程序设计语言中,基本类型所占用的空间大小是固定的。例如,JavaScript中number 类型的数值在内存中始终占用8B(64个二进制位),无论是存储一个1还是210,并且数值被 直接存储于变量所指向的内存单元。 但程序中也免不了存储一些占用空间大小不固定的数据,如数组、对象等,它们所占用 的内存空间大小可能因其包含的元素/属性的数量而不同,并且有可能随时变化。对于这些 占用空间大小不固定的数据类型,其值不便直接存储于变量所指向的内存单元,而是被存储 于动态分配的内存空间内,变量指向的内存单元中存放的却是值所在的动态空间地址。一 些程序设计语言中将内存地址称作“指针”(如C/C++语言),而JavaScript中称作“引用”,相 应地,此类数据被称作引用类型数据。图5.2展示了基本类型与引用类型数据的不同存储 方式。 图5.2 基本类型与引用类型数据的不同存储方式 赋值操作时,无论是基本类型或是引用类型,复制的都是变量所指向的内存单元内的 值,做等值比较时,也是比较该值是否相等(即图5.2中左侧空间中的值)。请参看如下 程序。 let x=3 //基本类型number let obj={ x: 3 } //引用类型object let x2=x //将x 赋值给x2 let obj2=obj //将obj 赋值给obj2 x2=4 obj2.x=4 console.log(x) //>>>3 (x 的值未发生变化) console.log(obj.x) //>>>4 (obj.x 的值变成了4) console.log(x==x2) //>>>false (二者指向的内存单元内的值不相等) console.log(obj==obj2) //>>>true (二者指向的内存单元中存有相同的内存地址) 观察上述代码的运行结果可看到,虽然x2= x、obj2= obj两行赋值语句让x2和obj2 复制了原变量的值,但对于引用类型复制的只是指向对象实际存储空间的内存地址,因而 obj2和obj通过相同的地址间接地引用了同一对象,此后对obj2.x属性值的更改其实也是 在更改obj.x属性值,最后1行的等值比较结果也证明了这一点。图5.3呈现了上述代码执 行结束时内存中的状态。 第5章 JavaScript基础1 07 图5.3 基本类型与引用类型赋值操作示意 在函数调用时,同样会因传递的参数类型不同(基本类型/引用类型),导致函数内部对 值的更改所产生的影响也不同,请参看如下代码。 let x=3 let obj={ x: 3 } function foo(x2, obj2) { //函数内分别对x2 和obj2.x 赋值 x2=4 obj2.x=4 } foo(x, obj) //调用函数foo,并将x 和obj 作为实参传入 console.log(x) //>>>3 (foo 函数内对x2 的更改未影响到函数外的x) console.log(obj.x) //>>>4 (foo 函数内对obj2.x 的更改同样影响到obj.x) 从此段代码的运行结果可看出,作为参数传递时,引用类型因为传递的是内存地址,所 以函数内的形参obj2事实上与函数外的实参obj引用了同一对象,因此对obj2.x的更改同 样影响了obj.x的值。基本类型变量作为参数传递时,传递的是变量值,因此x2与x是完 全独立的复本,函数内对x2的更改并未影响函数外x的值。原理与前一示例类似。简而言 之,作为参数传递时,基本类型传值,而引用类型传引用(传址)。 下面的例子中同样涉及基本类型与引用类型数据的复制问题,请读者尝试使用前面所 述的原理解释程序的输出结果。 let arr=[3, {x : 3}] let arr2=[arr[0], arr[1]] //将数组arr 中的两个元素复制到arr2 arr2[0]=4 //arr2[0]重新赋值对arr[0]无影响 arr2[1].x=4 //arr2[1].x 重新赋值同时也改变了arr[1].x 的值 console.log(arr[0]) //>>>3 console.log(arr[1]) //>>>{x: 4} 在上述数组元素的复制过程中,arr2[1]和arr[1]事实上指向了同一个对象(仅复制了 内存地址),因此对arr2[1]的操作其实也是在操作arr[1]。 通常将类似上例这样的复制过程称作浅拷贝,换言之,若在赋值、复制、参数传递等过程 中仅复制了对象的引用(地址),并未产生新对象,则称作浅拷贝,相对地,若产生了新的对象 复本则称作深拷贝。有时这两个术语也用作名词,例如上例中将arr2[1]称作arr[1]的浅