五、JavaScript(一)
一、JavaScript简介
在 HTML5 教程中,已经介绍了前端页面的构成,主要包括:
- HTML:用于描述页面的结构;
- CSS:用于控制页面中元素的样式;
- JavaScript:用于响应用户操作;
通过 JavaScript 可以对用户操作(如点击、输入)作出响应,增强用户与页面的互动性。还可以在数据提交前检查表单输入,保证数据有效性;动态改变网页的内容、结构和样式等等。
1.1 JavaScript发展史
JavaScript发展史可谓跌宕起伏,快意恩仇!
在互联网早期的江湖中,有一个公司叫网景公司,其推出的网景浏览器浏览器,凭借着领先的技术和出色的用户体验,迅速风靡全球,市场占有率一路飙升,稳稳占据着浏览器界的霸主地位,是众多用户遨游网络世界的首选工具,关键还是收费的。这就好比在武林中,网景派凭借着一门绝世武功,称霸武林,风光无限。
初出茅庐:诞生于网景
彼时的互联网虽日渐繁荣,但网页大多是静态的,缺乏交互性。为了让网页 “动” 起来,1995 年,网景公司找来 Brendan Eich,希望他能开发出一种能在浏览器上实现简单交互功能的语言。Brendan Eich(布兰登·艾克) 临危受命,仅用了短短 10 天(你就说牛逼不牛逼吧),就创造出了最初版本的 JavaScript,当时它还叫 LiveScript。后来为了蹭 Java 的热度,便改名为 JavaScript (简称 JS)。一经推出,JavaScript 就迅速在浏览器端流行起来,它赋予了网页弹窗、表单验证等动态效果,为网页注入了灵魂,一时间让整个网页江湖热闹非凡。
微软入局:开启 “浏览器大战”
微软看着网景浏览器在市场上独占鳌头,自然是心有不甘,坐立难安。1996 年,微软凭借自身强大的技术实力和市场影响力,推出了自己的浏览器 Internet Explorer(简称 IE),你收费,我还免费,恶心死你。同时还搞出了与 JavaScript 类似的语言 JScript。微软这一招,显然是想在浏览器和网页脚本语言这两个战场上,与网景一较高下。由此,著名的 “浏览器大战” 爆发,网景和微软互不相让,不断更新浏览器功能,而 JavaScript 和 JScript 也在各自阵营中发展。然而,两者的语法和特性不完全兼容,这可让网页开发者们叫苦不迭,为了确保网页在不同浏览器上都能正常显示,他们不得不编写大量的兼容代码,就像在两个不同门派的规矩下生活,累得够呛。
标准化之路:ECMAScript 的诞生
随着 “浏览器大战” 愈演愈烈,JavaScript 的混乱局面也急需解决。当时的情况是,网景和微软各自为政,网页开发者们被不同的标准折磨得苦不堪言,整个互联网开发生态陷入了一种混乱无序的状态。网景公司为了结束这种乱象,主要是自己也要不行了,扛不住了,为了给自己辛苦孕育的 JavaScript 谋一个更广阔、更健康的发展空间,决定将 JavaScript 的相关技术和标准捐献给 ECMA 国际组织 。1997 年,ECMA 国际组织介入,制定了 JavaScript 的标准,也就是 ECMAScript (简称 ES)。这就像是江湖中出现了一个公认的武林盟主,制定了统一的武林规矩。
然而,微软仗着自己在操作系统领域的绝对统治地位以及 IE 浏览器的庞大用户基础,牛逼得很。虽然 ECMA 制定了标准,但微软对 ECMAScript 标准阳奉阴违,在 IE 浏览器中对 JavaScript 的支持依旧我行我素,不严格遵循标准。这使得开发者们仍然无法摆脱兼容不同浏览器的噩梦,微软在浏览器市场可谓是独霸武林,享受着一家独大的红利。在这之后的 10 年,网页开发领域虽然在艰难前行,但因为微软的不配合,很多新技术、新想法的推进都受到了极大的阻碍,这 10 年对于那些希望有统一开发标准的开发者来说,像是沉沦的 10 年。
谷歌崛起:V8 引擎改变战局
直到 2008 年,谷歌推出了 Chrome 浏览器,同时带来了一个超级强大的 JavaScript 引擎 ——V8。谷歌早就看到了微软在浏览器市场的 “霸权” 对整个互联网发展的限制,决心打破这种局面。V8 引擎就像是给 JavaScript 装上了一个超级涡轮增压发动机,让 JavaScript 的执行速度大幅提升。以前 JavaScript 在复杂计算和大型应用上表现不佳,有了 V8 引擎后,JavaScript 在浏览器端的性能有了质的飞跃。
Chrome 浏览器凭借着 V8 引擎带来的高性能体验,迅速吸引了大量用户。用户们发现,Chrome 浏览器在加载网页速度、运行复杂网页应用时的流畅度等方面,都远远超过了 IE 浏览器。与此同时,Chrome 对 ECMAScript 标准的支持十分积极,这让开发者们看到了希望,纷纷开始为 Chrome 浏览器优化网页和应用。此消彼长之下,微软的 IE 浏览器逐渐失去优势,市场份额不断被 Chrome 蚕食,最终惨淡出局,结束了它在浏览器市场的霸主统治。
走向后端:Node.js 横空出世
2009 年,Ryan Dahl 基于 V8 引擎创造了 Node.js,这无疑是 JavaScript 发展史上的又一个重大里程碑。Node.js 让 JavaScript 得以脱离浏览器,在服务器端运行,就好比一位大侠不仅在自己的地盘称霸,还前往别人的地盘开疆拓土。这使得 JavaScript 能够进行全栈开发,前后端均可使用同一门语言,大大提高了开发效率,吸引了更多开发者投身到 JavaScript 的阵营。
1.2 ECMAScript的版本
ECMAScript 规范不断发展,主要的版本包括:
- ES3(1999年):最初的广泛标准,几乎所有浏览器都支持这一版本。
- ES5(2009年):增加了许多新特性,例如
strict mode、JSON支持、Object.create()等。 - ES6(2015年,通常称为 ES2015):引入了大量的语法新特性,是 JavaScript 语言史上的一个重要更新,例如
let和const、箭头函数、类(class)、模板字符串、模块(import/export)、Promise 等。 - ES7(2016年,ES2016):主要包括指数运算符
**和Array.prototype.includes()方法。 - ES8(2017年,ES2017):引入了
async/await语法、Object.entries()、Object.values()等。 - ES9(2018年,ES2018):引入了异步迭代器(
for await...of)和扩展对象字面量的语法。 - ES10(2019年,ES2019):新增了
Array.prototype.flat()、flatMap()、Object.fromEntries()等。 - ES11(2020年,ES2020):引入了可选链(
?.)、空值合并操作符(??)、BigInt 等。 - ES12(2021年,ES2021):增加了逻辑赋值运算符(
&&=,||=,??=)等。
现在了解一下即可。
通过上面的描述,我们知道 ECMA 国际组织制定了一个标准,也就是 ECMAScript 简称 ES,JavaScript 其实是 ECMAScript 标准的一个实现。我们学习 JavaScript 需要学习 ECMAScript 的 基础语法(如变量、数据类型、控制结构、函数等等),还要学习 ES 的各种扩展,包括 DOM(文档对象模型,HTML等文档元素)、BOM(浏览器对象模型,例如浏览器窗口、浏览器)等等。
1.3 JavaScript的特点
1 面向对象
虽然 JavaScript 是一种函数式语言,但它同样支持 面向对象编程(OOP),并且其对象系统有一些独特之处。在 JavaScript 中,几乎所有的东西都是对象,甚至函数也可以视为对象。
2 动态类型
JavaScript 是动态类型语言,变量类型在运行时确定,而非在声明时指定。例如,开发者可以先将变量赋值为数字,随后又赋值为字符串,无需进行显式的类型转换。这一特性赋予了代码编写极大的灵活性,特别适用于快速迭代开发的场景。但同时也要求开发者在编写代码时更加注意变量类型的一致性,避免因类型错误导致的运行时异常。
3 跨平台性
JavaScript 具有出色的跨平台能力,只要运行环境支持 JavaScript 引擎,无论是 Web 浏览器、服务器端(如 Node.js 环境),还是移动端应用(通过混合开发框架),甚至是物联网设备,都能够运行 JavaScript 代码。这使得开发者能够使用同一门语言进行多平台开发,有效提升开发效率,降低开发成本。
4 单线程执行
JavaScript 在浏览器环境和 Node.js 环境中都是单线程执行的。这意味着在同一时间内,JavaScript 引擎只能处理一个任务。这种设计使得 JavaScript 在处理 DOM 操作和用户交互时,不会出现多线程竞争导致的复杂问题,保证了操作的一致性和可预测性。例如,在浏览器中,JavaScript 负责处理用户的点击、输入等事件,同时操作 DOM 元素进行页面更新,如果是多线程环境,可能会出现多个线程同时修改 DOM 而导致的冲突。然而,单线程也存在局限性,对于一些耗时较长的任务,如网络请求、文件读取等,如果直接在主线程中执行,会导致页面卡顿或阻塞。为了解决这个问题,JavaScript 引入了异步编程机制,如回调函数、Promise 对象、async/await 语法等,使得这些耗时任务可以在后台执行,主线程不会被阻塞,从而保证了程序的流畅运行。
5 解释性语言
JavaScript 属于解释性语言,其代码执行过程与编译型语言有所不同。编译型语言(如 C、C++)在执行前,需要通过专门的编译器将源代码一次性编译成目标机器码,这个过程会生成可执行文件。而解释性语言 JavaScript 则是在运行时,由解释器逐行读取并执行代码。在浏览器中,JavaScript 解释器会逐行解析网页中的 JavaScript 代码,将其转换为机器能够理解的指令并立即执行。这种执行方式使得 JavaScript 开发和调试更加便捷,开发者可以实时看到代码修改后的效果,无需等待漫长的编译过程。但由于解释器每次执行代码都需要进行解析,相比编译型语言,JavaScript 在执行效率上可能会稍显逊色,尤其是在处理复杂计算和大规模数据时。不过,随着 JavaScript 引擎的不断优化,如 Chrome 浏览器的 V8 引擎采用了即时编译(JIT)等技术,在一定程度上提高了 JavaScript 的执行效率,缩小了与编译型语言的性能差距。
二、HelloWorld
2.1 编写 helloWorld
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>For技术栈</title>
<!-- JS代码 -->
<script type="text/javascript">
//弹出框
alert('Hello World')
//控制台打印
console.log('Hello World')
//网页中显示
document.write('Hello World!');
</script>
</head>
<body>
</body>
</html>


上面的 alert() 、console.log() 、document.write() 都是输出语句,只是输出的位置不同而已,在实际的开发中,使用console.log() 多一些,因为不会对网页产生任何影响,只是输出在控制台,不打开开发者工具不会被用户发现。
2.2 JS代码的编写位
上面 JS 代码是在网页的 <script> 标签中编写的,除了这种方式,还有一些地方也可以编写 JS 代码。
1)在script标签中
就像上面写的那样。
2)在js文件中编写
可以像CSS一样,将js的代码单独写在.js结尾的文件中。
举个例子:
创建一个script文件夹,在其中新建.js结尾的文件,名称自定义,例如index.js ,将代码编写在JS文件中即可。

然后在网页中使用<script>标签引入JS文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>For技术栈</title>
<!-- 引入JS -->
<script src="script/index.js"></script>
<script>
</script>
</head>
<body>
</body>
</html>
- 使用
<scripts>标签引入,使用src属性执行 JS 文件的路径; - 注意:用来引入 JS 文件的
<scripts>标签,不能在该标签中直接写 JS 代码了。 - 一个网页可以引入多个 JS 文件,也可以编写多个
<scripts>标签。
在实际的开发中, JS 代码可能很多,都写在 HTML 网页中,会很臃肿,且不易维护,写在单独的 JS 文件中,在不同的网页中都可以引入并使用,JS 文件可以被浏览器缓存,页面结构清晰,代码复用性高,利于维护。
3)事件属性中
HTML 标签有很多的事件属性,可以在触发事件的时候,执行 JS 代码。例如在鼠标点击的时候、在鼠标移入的时候、在获取焦点的时候等等。
举个栗子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>For技术栈</title>
</head>
<body>
<button onclick="alert('Hello World!')">点击</button>
</body>
</html>
在上面的代码中,在页面添加了一个按钮,给按钮添加了一个 onclick 事件属性,这个属性表示在鼠标点击的时候会触发该事件,当鼠标点击的时候,会执行属性中的 JS 代码。
关于事件,后面再学习。
显示如下:

4)超链接href属性中
还可以将 JS 代码写在超链接的 href 属性中
举个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>For技术栈</title>
</head>
<body>
<a href="javascript:alert('Hello World!');">超链接</a>
</body>
</html>
- 在上面的代码中,当点击超链接的时候,会执行
<a>标签中href属性中javascript:后面的代码。
显示如下:

上面那么多方式,应该使用哪一种呢?
JS 具体书写的位置还是要看具体的场景,每个场景都可能用到,如果是大量的 JS 代码,还是要写在单独的 .js 文件中。
下面我们的学习为了方便,就直接写在 <script></script> 标签中了。
三、基础语法
3.1 注释
在学习基础语法之前先介绍一下注释。
我们在学习任何语言,都会有注释,注释的作用就是向别人解释我们编写的代码的含义和逻辑,使代码有更好的可读性。
注释不是程序,是不会被执行的。所以我们也可以使用注释,注释掉暂时不需要执行的代码。
注释一般分类两类,单行注释和多行注释。
1)单行注释
单行注释以 // 开头,井号右边为注释内容。
例如:
// 我是单行注释,打印Hello World
alert('Hello World!') // 这里是注释
注意:// 号和注释内容一般建议以一个空格隔开,这是代码规范,建议大家遵守。
单行注释一般用于对一行或一小部分代码进行解释。
2) 多行注释
多行注释以 /**/ 括起来,中间的内容为注释内容,注释内容可以换行。
/*
我是多行注释,
我来哔哔两句
*/
alert('Hello World!')
多行注释适合对一段代码块、一个函数或者一个较大的功能模块进行详细的说明,关于函数,后面会讲。
3.2 代码格式
1)大小写
JS 代码是严格区分大小写的。
举个例子:
// 语句1
alert('Hello!')
// 语句2
Alert('Hello!')
上面的两句代码是不一样的,alert() 是浏览器提供的功能,所以 Alert 在执行的时候,会报错,找不到。
可以在浏览器的控制台看到错误:

2)空格与换行
JS 代码中的空格和换行会被忽略。
举个栗子:
alert( 'Hello' );
alert(
'Hello'
);
上面的写法都是一样的,空格和换行会被忽略;
但你别傻乎乎的在 alert 单词或 'Hello' 中间空格和换行。
3)语句结束符
JS 的每条语句应该以分号 ; 结束。
alert('Hello');
console.log('Hello');
但是不写分号也能正常运行,JS 解释器会自动添加分号(ASI机制),但是并不能在所有场景下都能准确无误的添加分号。
所以推荐手动加上。如果语句都添加分号,多条语句是可以写在一行的。没加是不行的,需要换行写,否则解释器不知道语句结束。
但是也有一些语法检查工具,推荐是不写分号的,所以根据自己喜好吧。
3.3 字面量
什么是字面量?
字面量就是在代码中,写下来的固定的值。
举个栗子:
alert(123)
alert(3.14)
alert('Hello')
alert(true)
上面的 123 、3.14 、'Hello' 、true 就是 4 个字面量,它们的类型分别是 整数、浮点数(也就是小数)、字符串 、布尔类型(只有 true 和 false 两个值),还有一些类型字面量后面用到再说。
这种直接在代码中写的值就是字面量。但是如果我们要输出 100 次 Hello ,就要写 100 次 Hello 字面量,要修改 Hello,就要修改 100 次,那就要疯掉了,所以我们可以将字面量保存到变量中,然后直接输出变量就可以了。
3.4 变量
在代码中,数据是保存在哪里的呢?
在代码中,数据需要保存在变量中(在内存中),变量是在程序运行的时候存储数据用的,可以想象变量为一个盒子。
整数、浮点数(小数)、字符串(文本)、布尔等类型,都可以放在变量中。
1)变量的定义
let age; // 定义变量
age = 18; // 为变量赋值
console.log(age); // 打印age的值,18
定义变量使用 let 关键字,上面定义了一个变量,变量名称是 age ,变量名称自定义;
给变量赋值使用 = 。
为变量赋值后,就可以使用变了,上面使用 console.log() 打印变量的值。
也可以在定义变量的时候同时赋值:
let age = 18; // 定义变量并赋值
console.log(age); // 打印age的值
使用 let 变量名 = 值; 的方式,声明变量的时候同时赋值。
我们可以定义不同的变量,存储不同的数据:
// 定义变量
let name = 'foooor.com'; // 定义一个变量,存储姓名,字符串类型的数据
let age = 18; // 定义一个变量,存储整形数据年龄
let height = 1.78; // 定义一个变量,存储浮点数据身高
let isAdult = true; // 定义一个变量,存储是否成年,数据类型是bool类型
// 定义完变量就可以使用变量了
console.log(name); // foooor.com
console.log(age); // 18
console.log(height); // 1.78
console.log(isAdult); // true
上面定义了4个变量,并赋值,值分别是字符串(string)、整数(int)、浮点型(float)、布尔型(bool)。
**和 C 和 Java 语言不同,JS 是弱类型语言,不需要指定变量的类型。**在 Java 中,定义变量需要指定变量的类型,整形的变量只能存储整形的数值。JS 随意,啥数据都能存。
2)变量的特征
变量变量,从名字中可以看出,表示“量”是可变的。所以,变量被定义后,可以在后面的代码中使用了,而且还可以重新修改变量的值。
例如:
let age = 18;
age = 16; // 可以修改变量的值
console.log(age); // 打印:16
age = age + 3; // 还可以在当前值的基础上进行运算
console.log(age); // 打印:19, 在前面16的基础上+3
age = "Hello";
console.log(age); // 打印:Hello, 变量的值可以修改为任意类型
可以看到变量的值可以被修改,而且可以参与运算。而且一个变量可以被赋予任意类型的数据,可以说是有一点放肆了。
通过在变量中存储数据,然后就可以在很多地方使用变量了,还可以改变变量的值。
3)旧的定义变量的方式
定义变量使用 let 关键字,以前 JS 中定义变量是使用 var 关键字的:
var age = 18;
现在也可以用,但是这两者有一些区别,var 没有块级作用域,有时候会有一些问题,作用域先不用了解。
在现代 JS 开发中,推荐使用 let 关键字来定义变量。
甚至你不使用关键字都可以:
age = 18;
console.log(age);
但是不推荐。
3.5 常量
变量是可以变的,常量就是不可以变的,定义了以后,值就不能修改了。有时候我们定义了一个值就不想它被修改,例如定义了一个圆周率,这个值就是固定的,不能在其他地方修改这个值。
常量使用 const 关键字定义。
举个栗子:
const PI = 3.1415926;
console.log(PI);
- 推荐常量的名称使用大写字母来定义(多个单词使用下划线分隔),这样一看到就知道是常量,无法修改。
如果修改常量的值,会发生错误:

3.6 标识符
什么是标识符?
标识符就是名字,例如变量的名字、方法的名字、类的名字,我们可以自定义名字的都。
起名字肯定会有限制,肯定不能 张Three 这样起名字,所以标识符也有限制。
1)标识符命名规则
标识符需要遵守一下规则:
只能是字母、数字、下划线_、$;
不能使用数字开头,可以英文、下划线、$ 开头;
a // 可以
a_b // 可以
_a // 可以
a123 // 可以
a_b_a // 可以
a$ // 可以
$_a // 可以
123 // 错误
123_ // 错误
123_a // 错误
小写敏感,大小写不同是不同的标识符;
name = "zhangsan"
Name = "lisi" // 和 name 是不同的标识符
不能使用关键字,关键字就是 JS 中保留的一些单词,有特殊的用途,不能被用作标识符,但是标识符中可以包含保留字。
JS 有如下关键字:
| abstract | byte | char | double | final |
|---|---|---|---|---|
| arguments | case | catch | export | float |
| await | class | const | extends | goto |
| boolean | default | delete | false | implements |
| break | do | else | enum | import |
| continue | debugger | finally | in | instanceof |
| delete | export | int | interface | let |
| do | extends | long | native | new |
| else | false | null | package | private |
| eval | for | function | protected | public |
| if | implements | return | short | static |
| in | instanceof | super | synchronized | this |
| of | throw | throws | transient | true |
| try | typeof | var | void | volatile |
| while | with | yield |
这么多怎么记?不用记!不用记!不用记!后面每个关键字都会学到,自然知道每个关键字是做什么用的,另外你用了代码会报错。
不要使用内置的变量、函数或类名作为变量名:
let alert = 123;
alert('Hello');
alert 是 JS 内置的用于在浏览器弹出提示框的,现在定义了一个变量叫 alert,会覆盖内置的变量、函数等。
2)变量命名规范
使用上面说的规则,我们可以定义变量名了。
但是为了优雅、统一、规范,我们在定义变量名时,还应该遵守以下规范,虽然你不遵守,没人开枪打死你,但是建议你遵守。
1 见名知意
看见一个变量名,就知道这个变量名是干嘛的。
let a = "zhangsan" // 看到a,鬼知道a是干嘛的
let name = "zhangsan" // 看到name,就知道这是个名字,简单明了
2 变量名使用首字母小写的驼峰命名规则:
let firstName = "Jim"
let studentNickname = "胖墩"
3 类名使用首字母大写的驼峰命名规则:
class Person {
}
类后面再讲,不着急。
4 常量使用全字母大写,多个单词使用下划线分隔
const MAX_USER_NAME_LENGTH = 24;
3.7 数据类型
刚才在定义变量的时候,我们使用了整数、浮点型、字符串、布尔型(bool)4个类型的数据。
JS 中的数据类型有很多,主要分为两类:原始数据类型(原始类型) 和 引用数据类型(对象类型)。上面说的四种类型都是原始数据类型。
JS 中一共有七种原始数据类型:
- 数值(number)
- 字符串(string)
- 布尔值(boolean)
- 大整数(bigint)
- 空值(null)
- 未定义(undefined)
- 符号(symbol)
JS 中,对象数据类型也是由原始数据类型构成的,下面介绍一下元素数据类型。
1 数值number
在 JavaScript 中 number 类型用于表示整数和浮点数。JS 中的数字没有区分整数和浮点数,所有的数字都被当作浮点数处理。
举个栗子:
let age = 30;
let price = 19.99;
console.log(age); // 输出 30
console.log(price); // 输出 19.99
JS 数值有很多特别的地方,让你意想不到。
number 类型是64位的,它可以表示的最大和最小值是有限制的,范围在 -(2^53 - 1)(即 -9007199254740991)~ 2^53 - 1(即 9007199254740991)之间。超过了范围,表示就不准确了。
举个栗子:
let largeNumber = 9007199254740992; // 2^53
let result = largeNumber + 1;
console.log(largeNumber); // 输出: 9007199254740992
console.log(result); // 输出: 9007199254740992,结果不准确
console.log(9999999999999999); // 输出: 10000000000000000,结果不准确
- 可以看到超过范围就不准确了,所以在 JS 中进行一些精度比较高的运算一定要注意。
我们可以使用 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 表示最大的数值和最小的数值。
let num1 = Number.MAX_SAFE_INTEGER;
let num2 = Number.MIN_SAFE_INTEGER;
console.log(num1); // 输出: 9007199254740991
console.log(num2); // 输出: -9007199254740991
另外,计算机不能进行精确的小数运算,这个不是 JS 的问题。
举个栗子:
let num = 0.1 + 0.2;
console.log(num); // 输出:0.30000000000000004
如果数值太大,会使用 Infinity 来表示:
let num = 9999 ** 9999; // 9999的9999次方
console.log(num); // 输出:Infinity,结果不准确
如果一个计算结果得到的不是数值,会使用 NaN (Not a Number)来表示:
let num = 123 - 'abc'; // 数字减去字符串
console.log(num); // 输出:NaN
所以值 Infinity 和 NaN 都是数字类型的,但是我们在写代码的时候,尽量避免出现 Infinity 和 NaN 。
2 大整数bigint
上面说了 number 类型表示的数字有范围限制,如果要表示很大的数该怎么办呢?
在 ES11 中引入了 bigint,可以处理非常大的整数,只要有内存。大整数以 n 结尾。
举个例子:
let bigIntNum = 1234567890123456789012345678901234567890n;
console.log(bigIntNum); // 输出 1234567890123456789012345678901234567890n
需要注意 bigint 和 number 是不能直接进行运算的:
let a = 10n;
let b = 20;
console.log(a + b);
上面的代码会报错,不能进行运算,因为 10n 和 20 是不同的数据类型,如果要进行运算,需要转换为相同的数据类型,下面类型转换的时候再讲。
typeof运算符:用于检查不同的值的类型,他会根据不同的值返回不同的结果。注意是值的类型不是变量的类型,变量在js中是没有类型的。
let c = 10;
let d = 10n
console.log(typeof c);
console.log(typeof d);

3 字符串string
下面单独介绍。
4 布尔值boolean
boolean 类型只有两个值:true 和 false,通常用于逻辑运算和控制流中,如条件判断,下一个章节再讲。
例如:用户名是否合法,true 表示合法,false 表示不合法。
let isValid = true;
let isExpired = false;
console.log(isValid); // 输出 true
console.log(isExpired); // 输出 false
5 空值null
null 是一个特殊的原始值,表示“空”或“无值”。
举个栗子:
let b = null;
console.log(b); // 输出 null
- 如果有一个变量不想再用了,可以将其设置为 null。
- 如果有一个变量需要初始化,但是不知道赋值多少,可以赋值为 null,表示它尚未获得有效值,等待后续赋值。
6 未定义undefined
undefined 是一种基本数据类型,表示变量声明了但未被赋值,所以一个变量未赋值的会自动被赋予 undefined 值。
举个栗子:
let a;
console.log(a); // 输出 undefined,因为 a 没有赋值
注意区分 null 和 undefined,null 是赋值了,赋的空值,undefined 是未赋值。
很多语言变量定义后未初始化,直接使用是会报错的,但是 JS 为了避免执行的过程中出现错误而中断执行,所以尽可能不报错。
7 符号symbol(ES6 引入)
symbol 是一种新的原始数据类型,表示唯一且不可变的值。通常用于对象的属性键。每个 symbol 值都是唯一的,不能与其他 symbol 值相等。
const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2); // 输出 false,因为每个 Symbol 都是唯一的
Symbol 可以用于定义对象的私有属性,防止属性被意外修改,后面再讲解。
3.8 字符串
1 字符串定义方式
字符串有3种不同的定义方式:
方式一:双引号定义法
let text1 = "我是一个字符串";
方式二:单引号定义法
let text2 = '我是一个字符串';
方式三:模板字符串定义法
模板字符串定义法使用反引号定义,反引号是 1 左边的那个键,反引号定义的字符串可以换行,其中的:
let text3 = `我是一个字符串,
我还可以换行`;
- 内容输出的时候,也会换行。
2 转义字符
如果我们的字符串中包含引号,该怎么处理呢?
如果字符串中包含单引号,可以使用双引号定义法:
let str = "Hello 'JavaScript'";
如果字符串中包含双引号,可以使用单引号定义法:
let str = 'Hello "JavaScript"';
还有一个通用的方法,就是使用转义符 \ ,在引号前面加上 \ 表示后面的字符是普通字符串:
let str1 = "Hello \"JavaScript\"";
let str2 = "Hello \'JavaScript\'";
console.log(str1); // Hello "JavaScript"
console.log(str2); // Hello 'JavaScript'
同样,如果你想在字符串中输出 \t ,你可能会这样写:
let str = "Hello \t \"JavaScript\""
console.log(str); // Hello "JavaScript"
但运行完成结果却没有 \t ,因为 \t 是制表符;同样 \n 是换行符,如果想在字符串中输出 \t 或 \n 等特殊字符,也是需要对 斜杠 \ 进行转义:
let str = "Hello \\t \"JavaScript\""
console.log(str); // Hello \t "JavaScript"
同样,输出 \ 也需要转义,要输出两个 \\ ,都需要进行转义:
let str = "Hello \\\\ \"JavaScript\""
console.log(str); // Hello \\ "JavaScript"
3 字符串拼接
1 使用加号拼接
两个字符串拼接,直接使用 加号+ 拼接即可,例如:
console.log("Hello " + "World") // 输出:Hello World
但是上面的拼接纯属脱裤子放屁,直接写成 console.log("Hello World") 就完了,所以一般字符串拼接都是在字面量和变量、变量和变量之间拼接:
let name = "张三";
let age = 10;
console.log("Hello,我是" + name + ",我今年才" + age + "岁,我是法外狂徒") // 输出:Hello,我是张三,我今年才10岁,我是法外狂徒
2 使用模板字符串
反引号定义的模板字符串,可以通过 ${} 插入变量或表达式。
举个栗子:
let name = "张三";
let age = 10;
console.log(`Hello,我是${name},我今年才${age}岁,我是法外狂徒`) // 输出:Hello,我是张三,我今年才10岁,我是法外狂徒
- 这种方式可以说是非常适合字符串拼接了。
3 使用concat()方法
使用字符串的 concat() 方法也可以用于拼接字符串。
let str1 = "Hello";
let str2 = "World";
let result = str1.concat(" ", str2, "!"); // 拼接三个字符" "、str、"!"
console.log(result); // "Hello World!"
- 需要注意
concat()方法不会修改原字符串,而是返回一个新的字符串。 concat()方法在现代 JavaScript 中使用较少,因为+和模板字符串更简单。
3.9 类型转换
1 typeof
上面在将 数字 和 字符串 进行运算的时候,会返回 NaN ,所以我们在运算和转换之前,可以先对数据进行类型判断。
使用 typeof 运算符 ,可以得到数据的类型。
举个栗子:
let num = 123;
console.log(typeof num); // "number"
console.log(typeof 42); // "number"
console.log(typeof 3.14); // "number"
console.log(typeof 100n); // "bigint"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (这是 JavaScript 的一个历史遗留问题)
console.log(typeof Symbol()); // "symbol"
typeof的结果是字符串。typeof null返回的是"object",这是 JavaScript 的一个历史遗留问题,应该算一个设计缺陷,所以typeof无法判断一个变量是否为null。
需要注意, typeof 不是检查变量的类型,变量是没有类型的,检查的是变量中数据的类型。
let value = "foooor";
console.log(typeof value); // "string"
value = 123;
console.log(typeof value); // "number"
- 可以看到 value 中存储的值不同,
typeof value的结果也不同。
2 数据类型转换
数据类型转换就是不同的数据类型之间是可以进行转换的,例如在某些时候,我们需要将数字转换为字符串,或者字符串转换为数字。
为什么需要进行类型转换呢?
例如我们从网页的文本框中读取的数字,默认是字符串类型的,我们读取以后需要转换为数字类型,例如:将字符串"100"转换为数字100。
1) 转换为字符串
先介绍一下将其他数据类型转换为字符串,只需要调用 toString() 函数即可。
举个栗子:
let age = 18;
let str = age.toString(); // 将18转换为字符串复制给 str 变量
console.log("age=", str, ", type:", typeof str); //输出:age= 18 , type: string
console.log("str=", str, ", type:", typeof str); //输出:str= 18 , type: string
- 调用方法,就是
.函数名(),函数主要就是实现某种功能,关于函数后面再讲。 console.log()可以接收多个参数,会将多个参数拼接起来。- 上面
age.toString();将age中的数值转换为字符串复制给str。
上面是通过变量调用 .toString() 函数来实现转换,还可以使用字面量来调用,但是数值的字面量需要添加 括号() 。
举个栗子:
let intStr = (18).toString(); // 需要添加括号
let boolStr = true.toString(); // boolean字面量不需要添加括号
上面 .toString() 函数有一个局限性,就是如果一个变量的值为 null 或 undefined,是无法调用 .toString() 方法的,会报错!值为 null 和 undefined 的变量是无法调用方法的。
我们还可以使用另外一种方法,就是使用 String() 函数,将变量作为 String() 的参数。
举个栗子:
let age = 18;
let value = null;
console.log("age=", age, ", type:", typeof age); // age= 18 , type: number
console.log("value=", value, ", type:", typeof value); // value= null , type: object
age = String(age); // 将数值转换为字符串,重新赋值给age
value = String(value);
console.log("age=", age, ", type:", typeof age); // age= 18 , type: string
console.log("value=", value, ", type:", typeof value); // value= null , type: string
- 其实在调用
String()函数的时候,实际还是调用toString()函数,null和undefined没有toString()函数,会直接转换为"null"和"undefined"。
2 转换为数值
下面介绍如何将其他类型的数据转换为数值类型。
首先是如何将字符串转换为数值,只用 Number() 函数即可。举个栗子:
let str = '123';
let num = Number(str);
console.log(num, typeof num); // 123 'number'
- 上面将字符串
'123'转换为数值 123 ; - 如果字符串无法转换为数值,例如
123abc,则会转换为NaN; - 如果是空字符串
''或空格字符串' ',则会被转换为0; - 如果是布尔值,则
true转换为1,false转换为0; - 如果是
null值,则被转换为0,如果是undefined,则会转换为NaN。
上面的 Number() 函数适用于所有的数据类型转换为数值类型,还有两个函数专门针对字符串转换为数值:
parseInt():将字符串转换为一个整数;parseFloat():将字符转换为一个浮点数。
举个栗子:
let str1 = '123';
let str2 = '3.14159';
let num1 = parseInt(str1);
let num2 = parseFloat(str2);
console.log(num1, typeof num1); // 123 'number'
console.log(num2, typeof num2); // 3.14159 'number'
parseInt()在解析字符串的时候,会从左到右读取字符进行解析,如果是123abc,会被读取并转换为123,但是如果是abc123则无法被解析,会被转换为NaN,如果是123.456,则会转换为123,只能解析整数。
3 转换为布尔值
将其他的数据类型转化为布尔类型,使用 Boolean() 函数即可。
举个栗子:
let num = 1;
let bool = Boolean(num);
console.log(bool, typeof bool); // true 'boolean'
- 对于数值转布尔值而言,
0和NaN转换为false,其余都转换为true; - 对于字符串而言,空字符串
''转换为false,其余都是true,包括空格字符串' '和'false'字符串; null和undefined转换为false- 对象都转换为
true。
上面讲的转换的方式是显式转换的,也就是需要明确写出转换的代码,在后面学习运算符的时候,还会讲解如何使用隐式转换,也就是自动转换。
3.10 精度控制
在前面演示 0.1 + 0.2 的时候,结果是 0.30000000000000004,而不是预期的 0.3 ,我们可以如下的几种方式,来控制小数的精度。
1 toFixed()
toFixed() 方法可以将数字格式化为指定的小数位数,它会对数字进行四舍五入。
举个栗子:
let num = 3.14159;
let result = num.toFixed(2); // 输出:"3.14"
- 需要注意
toFixed()方法返回的是字符串!需要用parseFloat()转换回数值。
2 toPrecision()
toPrecision() 方法用于将数字格式化为指定的总有效数字,并返回字符串表示。
举个栗子:
let num = 2.456789;
let precisionNum = num.toPrecision(4); // 结果是 "2.457"
它适用于需要控制整体有效数字的场景。
3 使用Math对象
Math 对象提供了一些方法来辅助精度控制。例如: Math.round()、Math.floor() 、 Math.ceil() 、Math.trunc() 。
通过它们结合乘法和除法,可以控制小数位数。
下面依次介绍一下。
1) Math.round()
Math.round() 方法可以将数字四舍五入到最接近的整数。
举个栗子:
let num = 3.4275;
let result = Math.round(num * 100) / 100;
console.log(result); // 3.43
Math.round(num * 100)先将数字扩大 100 倍,变成342.75,然后将小数位四舍五入,则变成了343;- 然后除以
100,得到3.43,实现了小数位的控制。
这种方法返回的是数字类型,方便进一步的数学运算。
2) Math.floor()
Math.floor() 方法向下舍入。它们也可以通过乘法和除法来控制小数位数。
举个栗子:
let num = 3.4275;
let result = Math.floor(num * 100) / 100;
console.log(result); // 3.42
Math.floor(num * 100)先将数字扩大 100 倍,变成342.75,然后将小数位向下舍入,小数点后7大于5也不要了,则变成了342;- 然后除以
100,得到3.42,实现了小数位的控制。
3 Math.ceil()
Math.ceil() 方法是向上舍入。
举个栗子:
let num = 3.4215;
let result = Math.ceil(num * 100) / 100;
console.log(result); // 3.43
Math.ceil(num * 100)先将数字扩大 100 倍,变成342.15,然后将小数位向上舍入,小数点后1不够5也进位,则变成了343;- 然后除以
100,得到3.43,实现了小数位的控制。
4 Math.trunc()
Math.trunc() 是直接去除小数部分。
举个栗子:
let num = 3.4215;
let result = Math.trunc(num);
console.log(result); // 3
- 直接去掉小数部分,变成整数,没什么好说的。
3.11 进制
JavaScript 支持多种进制,包括 二进制、八进制、十进制 和 十六进制,但是实际在开发中用的不多。
这里简单了解一下即可。
1) 进制表示
let decimal = 123; // 十进制
let binary = 0b101; // 二进制表示 5
let octal = 0o10; // 八进制表示 8
let hexadecimal = 0x1F; // 十六进制表示 31
console.log(decimal); // 输出 123
console.log(binary); // 输出 5
console.log(octal); // 输出 8
console.log(hexadecimal); // 输出 31
- 正常写的数字就是十进制;
- 二进制数字使用
0b或0B开头; - 八进制数字使用
0o或0O开头; - 十六进制使用
0x或0X开头。
但是在输出数字的时候,都是使用十进制输出的,如果想按指定进制输出,可以以字符串的形式输出:
let number = 255;
console.log(number.toString(2)); // 输出:11111111,将十进制转换为二进制字符串表示
console.log(number.toString(8)); // 输出:377,将十进制转换为八进制字符串表示
console.log(number.toString(16)); // 输出:ff,将十进制转换为十六进制字符串表示
2) 进制转换
parseInt() 方法可以将字符串转换为整数,并指定进制。它接受两个参数:要转换的字符串和进制。
举个栗子:
let bin = "101";
let oct = "10";
let hex = "1F";
console.log(parseInt(bin, 2)); // 输出 5,将字符串以二进制转换为十进制
console.log(parseInt(oct, 8)); // 输出 8,将字符串以八进制转换为十进制
console.log(parseInt(hex, 16)); // 输出 31,将字符串以十六进制转换为十进制
四、运算符
在编写有逻辑的代码前,先学习一下 JS 中的运算符。
下面介绍 JS 中常见的运算符:算数运算符、赋值运算符、关系运算符
4.1 算数运算符
算数运算符是干嘛的?
就是进行数学运算的,就是小学学的加、减、乘、除等。
JS 中常见的算数运算符如下:
| 运算符 | 描述 | 示例 |
|---|---|---|
| + | 加法 | a + b |
| - | 减法 | a - b |
| * | 乘法 | a * b |
| / | 除法 | a / b |
| % | 取余,模运算 | a % b |
| ** | 幂运算 | a ** b |
1 普通使用
举个例子:
let a = 10;
let b = 3;
console.log(a + b); // 输出: 13
console.log(a - b); // 输出: 7
console.log(a * b); // 输出: 30
console.log(a / b); // 输出: 3.3333333333333335
console.log(a % b); // 输出: 1,10除以3,余数为1,取模就是取余数
console.log(a ** b); // 10的3次方,输出: 1000
console.log(100 ** 0.5); // 100的0.5次方,相当于100开平方,输出: 10
加减乘除在运算中用的是最多的,但是模运算(取余数)有时候也会用到,下面举个栗子:
判断一个数是否是 3 的倍数,那么判断这个数 % 3 是否等于0 就知道了。
let i = 27;
let j = i % 3; // 除以3,余数是0,就是3的倍数
console.log(j); // 结果为:0
同样,如果判断一个整数是否是偶数,那么可以将这个数 % 2,然后查看结果是否是 0,如果是 0 就是偶数,否则是奇数。
一个数除以0,在其他语言中一般都是报错的,因为除数不能为0,但是 JS 不同。
console.log(3 / 0); // 结果为:Infinity
- 结果为
Infinity,在 JS 中进行运算,要尽量避免出现Infinity和NaN。
算数运算符也是有优先级的,和在小学的时候学习的一样。
let i = 3 + 2 * 4 ** 2;
console.log(i); // 结果为:35
幂运算优先级 > 乘、除、取模运算 > 加、减运算。
2 JS的变态之处
继续看一下算数运算,这些在其他语言就做不到,但是这是 JS !
let result = 10 - '5'; // 数值减去字符串
console.log('result:', result) // 5
- JS 是弱类型语言,在进行算数运算的时候,除了加法(会拼接字符串),操作数是非数值时,都会转换为数值后,再进行运算。
- 所以上面会将
'5'转换为数值 5 ,再进行运算。
再举几个例子:
console.log(10 - '') // 10
console.log(10 - true) // 9
console.log(10 - null) // 10
console.log(10 - undefined) // NaN
- 上面够变态吧,在数据转换章节讲到不同类型的数据,转换为数值时候对应的值,此时在进行算数运算的时候,会自动将其他数据类型转换为数值(加法除外)。
空格转换为0,true转换为1,null转换为0,undefined为NaN。如果忘记了,看一下上一章的数据转换小节。
通过这种方式,我们可以轻松将字符串转换为数值,举个栗子:
let a = '123'; // a是字符串
a = a - 0;
console.log(a, typeof a);
- 上面的代码会将 a 先转换为数值,然后减0,然后赋值给a,a就变成了数值。
还可以直接将两个字符串进行运算,会自动转换为数值进行运算:
console.log('10' - '5'); // 5
console.log('10' * '5'); // 20
console.log('10' / '5'); // 2
console.log('10' % '5'); // 0
console.log('10' ** '5'); // 100000
加法比较特别,下面单独看一下加法,字符串和任意数据做加法运算,会将其他的数据转换为字符串。
举个栗子:
console.log("hello" + 123); // 'hello123'
console.log("hello" + 1 + 2 + 3); // 'hello123'
console.log('' + 123); // '123' , 可以将数值转换为字符串
- 上面字符串在前面,会将后面的数据都转换为字符串进行拼接。
- 通过上面的方式,可以将数值转换为字符串,这是隐式的转换方式。
如果数值放在前面,则会先进行数值的计算,如下:
console.log(1 + 2 + 3 + "hello"); // '6hello'
console.log('' + 123); // '123' , 可以将数值转换为字符串
拼接 null 和 undefined 的结果如下:
let a = null;
console.log("hello" + a); // 'hellonull'
let b;
console.log("hello" + b); // 'helloundefined'
除了 NaN ** 0 ( NaN 的 0 次幂)结果为 1,NaN 和其他非字符串数据的算术运算的结果都是 NaN。如果 NaN 和字符串相加,则是字符串拼接。
console.log(3 + NaN); // NaN
console.log('3' - NaN); // NaN
console.log('3' + NaN); // 3NaN
console.log(NaN ** 0); // 1
console.log(true + NaN); // NaN
console.log(null + NaN); // NaN
console.log(NaN + NaN); // NaN
console.log(undefined + NaN); // NaN
4.2 赋值运算符
赋值运算符就是用来给变量赋值的。
1 赋值运算符
| 运算符 | 描述 | 举例 |
|---|---|---|
| = | 赋值运算符 | 就是把 = 右边的值赋值给左边的变量,之前用过了。 |
例如:
let a = 2;
let b = 3;
let c = a + b;
c = c + a;
如果要交换两个变量的值,那么一般需要借助第三个变量:
let a = 2;
let b = 3;
// 交换两个变量的值
let temp = a;
a = b;
b = temp;
除了赋值运算符,下面还有复合赋值运算符。
2 复合赋值运算符
复合赋值运算符就是经过计算后,将值赋给前面的变量。
JS 中常用的复合赋值运算符如下:
| 运算符 | 描述 | 举例 |
|---|---|---|
| += | 加法赋值运算符 | c += a 等效于 c = c + a |
| -= | 减法赋值运算符 | c -= a 等效于 c = c - a |
| *= | 乘法赋值运算符 | c *= a 等效于 c = c * a |
| /= | 除法赋值运算符 | c /= a 等效于 c = c / a |
| %= | 取模赋值运算符 | c %= a 等效于 c = c % a |
| **= | 幂运算赋值运算符 | c **= a 等效于 c = c ** a |
举个栗子:
let a = 5;
let b = 3;
a += b; // a = a + b
console.log(a); // 8
a = 5;
a -= b; // a = a - b
console.log(a); // 2
a = 5;
a *= b; // a = a * b
console.log(a); // 15
复合赋值运算符感觉这么写可读性还变差了,有什么好处呢?
- 更加简练;
- 有微小的性能优势;
3 空赋值运算符
空赋值运算符( ??= )是一种较新的特性,于 ECMAScript 2020(ES11)中引入。它的作用是为变量赋值,但仅当该变量的值为 null 或 undefined 时才会执行赋值操作。
举个栗子:
let num = null;
num ??= 10; // num为null,所以此时会为num赋值为10;
num ??= 20; // num此时为10,不为null和undefined,所以无法为num赋值为20,num的值仍然为10
console.log(num); // 10
4.3 一元正负运算符
一元运算符就是只有一个参数的运算符,例如 typeof 就是只有一个参数, + 需要两个数相加,所以是二元运算符。
下面介绍的一元正负运算符,就是正负号,给一个数值添加正号,不会改变一个数,给一个数值添加负号,会取反,和初中学习的一样。
举个栗子:
正号如下:
let a = 10;
let b = +a; // 添加+号不会影响数值
console.log(b); // 10
a = -10;
b = +a; // 添加+号不会影响数值
console.log(b); // -10
- 正号添加了和没添加一样,看不出任何效果。
负号是取反,如下:
let a = 10;
let b = -a; // 取反,变为-10
console.log(b); // -10
a = -10;
b = -a; // 取反,变为10
console.log(b); // 10
正号看不出效果,但是可以使用正号隐式的将字符串转换为数值。
举个例子:
let a = '123';
a = +a;
console.log(a, typeof a); // 123 'number'
- 上面通过
a = +a,a 已经变为数值。和使用 Number() 函数是一样的,就是更简便一些。
4.4 自增自减运算符
JS 中的自增自减运算符包括++和--。它们可以用于对变量进行加1或减1操作。
举个栗子:
let a = 5;
a++; // a自增1
console.log(a); // 输出 6
a--; // a自减1
console.log(a); // 输出 5
需要注意的是,自增自减运算符可以放在变量的前面或后面,这会影响到表达式的值。
如果运算符放在变量的前面(a++),表示先加减,后使用;如果运算符放在变量的后面(++a),表示先使用,后加减。
举个栗子:
let a = 5;
let b = a++; // 先使用a的值还是5,后加1,所以执行此行代码后,b为5,a为6
console.log(b); // 5
console.log(a); // 6
let c = 5;
let d = ++c; // 先加减c+1变为6,后使用,d的值为6
console.log(d); // 6
console.log(c); // 6
-- 操作符也是一样的。
4.5 关系运算符
关系运算符也就是比较运算符,主要有以下几种:
| 运算符 | > | < | >= | <= |
|---|---|---|---|---|
| 含义 | 大于 | 小于 | 大于或者等于 | 小于或者等于 |
举个栗子:
let a = 10;
let b = 3;
let c = a > b;
console.log(c); // true
console.log(a < b); // false
console.log(a >= b); // true
console.log(a <= b); // false
- 关系运算符得到的结果都是布尔类型。
如果比较的是两个字符串,会逐位比较每个字符的 Unicode 编码。
举个栗子:
console.log('a' > 'b'); // false
console.log('abc' > 'b'); // false
console.log('12' > '2'); // false
- 在 Unicode 编码中
a是 97,b是 98 ,所以'a' < 'b'为false; - 在比较的时候,逐位比较,得到结果了就终止比较了,所以和字符串长度无关;
- 即使比较的是字符串数字,因为两个都是字符串,所以不会转换为数值,
1在 Unicode 编码中是 49,2在 Unicode 中是50,所以'12' > '2',不会比较后面的字符了。
除了数值和数值、字符串和字符串之间可以进行比较,不同的数据类型之间也可以比较。
举个栗子:
console.log(5 < '10'); // true
console.log('1' > false); // true
console.log(1 > null); // true
- 对不同的数据类型进行比较的时候,会先将其转换为数值,然后在进行比较。
需要注意,任何数值和 NaN 比较,结果都返回false。
let a = 'abc';
console.log(a >= 3); // false
console.log(a <= 3); // false
- 上面是字符串和数值比较,会将 a 先转换为数值,变为 NaN,然后和 3 比较,会发现结果都是 false。
4.6 相等运算符
相等运算符也是关系运算符,判断两个值是否相等,但是 JS 中的相等运算符比较特别,单独说一下。
相等运算符有四个:
| 运算符 | == | != | === | !== |
|---|---|---|---|---|
| 含义 | 相等 | 不相等 | 全等 | 不全等 |
先来说一下相等和不相等。
举个栗子:
let a = 1;
let b = 1;
let c = '1';
let d = true;
console.log(a == b); // true;
console.log(a != b); // false;
console.log(a == c); // true;
console.log(a == d); // true;
- 相等运算符就是判断两个值是否相等,不相等就是相等的取反。
- 在比较的时候,如果是不同的数据类型,会先转换为相同的数据类型(通常转换为数值),然后在进行比较。
有几种特殊的情况需要注意一下:
let a = null;
let b = undefined;
let c = NaN;
let d = NaN;
console.log(a == b); // true;
console.log(c == d); // false;
console.log(c == c); // false,离谱吧;
- 上面的情况比较特殊,当比较 null 或 undefined 的时候,并不是转换为数值,因为 null 转换为数值是 0,undefined 转换为数值是 NaN,应该不相等,但是实际**
null是 ==undefined的**。 - 另外需要注意的是 ,NaN 不和任何值相等,包括 NaN 自己,够离谱。
全等运算符是用来比较两个数值是否全等,全等和相等的区别就是不会进行数据类型的转换。
举个栗子:
let a = 1;
let b = 1;
let c = '1';
let d = true;
let e = null;
let f = undefined;
console.log(a === b); // true;
console.log(a !== b); // false;
console.log(a === c); // false;
console.log(a === d); // false;
console.log(e === f); // false;
- 结果显而易见,不同的数据类型的值肯定是不全等的;
- 不全等就是对全等的取反。
推荐用全等来比较两个值,正常情况下比较两个数值的时候,最好先确定它们的数据类型是一致的。
4.7 逻辑运算符
逻辑运算符主要有以下三种:
| 运算符 | && | || | ! |
|---|---|---|---|
| 含义 | 逻辑与,两边条件同时满足 | 逻辑或,两边条件满足一个即可 | 逻辑非,取反值,真为假,假为真 |
1 逻辑非
举个栗子:
let a = !true;
let b = !a;
console.log(a); // false
console.log(b); // true
- 逻辑非就是对一个布尔值取反,
true变false,false变true; - 所以上面
a的值变为false,对a取反赋值给b,b的值变为true;
但是在 JS 中,逻辑非不仅可以对布尔值取反,还可以对其他数据类型取反。
举个栗子:
console.log(!123); // false
console.log(!''); // true
console.log(!' '); // false
console.log(!0); // true
console.log(!NaN); // true
console.log(!null); // true
console.log(!undefined); // true
- 取反的时候,会首先将其他数据类型转换为布尔值(参考
Boolean()函数),然后取反。
通过上面的特性,可以隐式的实现将其他数据类型转换为布尔值,只需要进行两次取反就可以。
let a = !!123;
console.log(a, typeof a); // true 'boolean'
123进行一次取反是false,再进行一次取反就变为true了,和Boolean(123)函数执行的结果一致,但是更方便。
2 逻辑与
逻辑与是对两个值进行运算,当左右两边的值都是 true,则返回true,否则返回false。
举个栗子:
let a = true;
let b = true;
let c = false;
console.log(a && b); // true
console.log(a && c); // false
- a 和 b 都为 true,结果为 true;a 和 c 有一个不为 true,则结果为 false;
如果要判断一个数是否在 5 到10 之间,可以这样写:
let num = 7;
let result = num > 5 && num < 10; // 判断 num 是否 大于5 同时 num 小于10
console.log(result); // true
num > 5 && num < 10表示同时满足左右两个条件,注意不能写成5 < num < 10。
逻辑与还可以对非布尔值数据进行运算,会将数据转换为布尔值,但是最终会返回数据的原值。
在进行逻辑与运算的时候,是找 false 的,当找到 false 了,就返回 false 数据的原值,如果一直没找到,就返回最后一个值。
举个栗子:
let a = 10;
let b = 0;
let c = NaN;
console.log(a && b); // 0
console.log(b && a); // 0
console.log(a && c && b); // NaN
a && b:a 为 10,转换为 true,b 为 0,转换为 false,a 为true,继续找 false,b 为 false,找到了b,返回 b 的值 0;b && a:b 转换为 false,找到了 false,返回 b 的值 0;a && c && b:a 转换为 true,继续找,c 转换为 false,找到了false,返回 c 的值 NaN 。
3 逻辑或
逻辑或是对两个值进行运算,当左右两边的值有一个为 true,就返回true,否则返回false。
举个栗子:
let a = true;
let b = true;
let c = false;
let d = false;
console.log(a || b); // true
console.log(a || c); // true
console.log(c || d); // false
- 两边都为 false ,结果才为 false。
如果要判断一个人岁数是否小于3 或者 大于 60,可以这样写:
let age = 48;
let result = age <= 3 || age >= 60; // 判断age是否小于等于3或者大于等于60
console.log(result); // false
age <= 3 || age >= 60表示两边有一边满足即可。
逻辑或同样可以对非布尔值数据进行运算,运算的时候,是找 true 的,当找到 true 了,就返回 true 数据的原值,如果一直没找到,就返回最后一个值。
举个栗子:
let a = 10;
let b = 0;
let c = 'hello';
console.log(a || b); // 10
console.log(b || a); // 10
console.log(b || c || a); // hello
a || b:a 转换为 true,找到了 true,则返回 a 的值 10 ;b || a:b 转换为 false,则继续找 true,找到了 a,转换为 true,找到了 true,则返回 a 的值 10 ;c || b || a:b 转换为 false,继续找,c 转换为 true,找到了true,则返回 b 的值 'hello' 。
4 逻辑运算符的优先级
优先级:! > && > ||
举个栗子:
let a = 3;
let b = 12;
let c = 6;
let d = 10;
console.log(a > 5 && b > 10 || c > 5 && d > 10); // false
当一个逻辑运算语句中同时包含与、或 、 非运算的时候,会按照优先级进行运算。
所以上面先运算 a > 5 && b > 10,然后运算 c > 5 && d > 10,最后运算 || 两边的结果。
再举个例子:
let a = true;
let b = false;
let result = !a || a && b;
console.log(result); // false
先运算 !a ,结果为 false,然后运算 a && b,结果为 false,最后运算 || 两边的结果,为 false。
如果搞不清楚先后运算的顺序,就用括号 () 括起来,准没错。
5 逻辑短路
逻辑短路就是在进行逻辑运算的时候,如果已经能够得到最后的值,就不会再继续进行判断了。
只有 与运算符 && 和 或运算符 || 存在逻辑短路。
举个栗子:
let num = 10;
let b = num > 20 && num++ < 40;
console.log(num); // 10
在执行上面代码的时候,num > 20 已经不成立了,而 && 运算需要两边都满足,所以后面的条件成不成立都不会影响最终逻辑表达式的结果,所以 && 后面的 num++ < 40 就不会再继续执行了,那么 num 的值就没有 ++,这就是逻辑短路。
其实逻辑或就是找 false,找到了就不继续了。
同样 || 运算符:
let num = 10;
let b = num < 20 || num++ > 40;
console.log(num); // 10
在执行上面代码的时候,num < 20 已经成立了,|| 运算只要一边满足即可,所以整个逻辑表达式肯定是成立的,所以 || 后面的 num++ > 40 就不会继续执行了,那么 num 的值就没有 ++。
逻辑或就是找 true,找到了就不执行了。
如果不想逻辑短路,可以使用 & 、| 运算符,一个& 或 | 时,左边无论真假,右边都进行运算。
以逻辑与,修改上面的代码:
let num = 10;
let b = (num > 20) & (num++ < 40);
console.log(num); // 11
使用 & 运算符,就不存在逻辑短路问题,无论左边的运算结果为 true 或 false,右边都会执行运算。
& 、| 运算符用的不多,开发中主要使用 && 和 || 。
5.8 总结隐式类型转换
总结一下数据类型转换的方式:
| 类型转换 | 显式转换 | 隐式转换 |
|---|---|---|
| 转换为字符串 | String(a) | '' + a |
| 转换为数值 | Number(a) | +a |
| 转换为布尔值 | Boolean(a) | !!a |
5.9 运算符优先级
上面那么多运算符,如果同时运算的时候,优先级是怎么样的呢?
优先级:算术运算符 > 关系运算符 > 逻辑运算符
再细分的话,优先级从高到低如下:
** 指数
* / % 乘除取模
+ - 加减
> < >= <= 比较
== != === !== 相等性
&& 逻辑与
|| 逻辑或
?? 空值合并
举个栗子:
let value = 5 + 3 * 2 ** 2 > 20 && 10 < 15 || false;
console.log(value); // 输出: false
看着上面的代码,眼不眼晕。在实际的开发中,不要这样写,可读性太差了。
怎么办?加括号就行了,括号的优先级最高!
所以上面的代码,根据优先级括号,应该是这样的:
let value = ((5 + (3 * (2 ** 2))) > 20) && (10 < 15) || false;
console.log(value); // 输出: false
在开发中,不知道优先级高低,添加括号就完事了。
五、流程控制
5.1 代码块
代码块(Block) 是由一对大括号 {} 包围的零条或多条语句的集合。
举个栗子:
下面的代码是两个代码块:
{
let a = 2;
console.log(a); // 2
}
{
let b = 5;
console.log(b); // 5
}
在前面介绍变量的时候,说过声明变量可以使用 let 或者 var ,var 没有块级作用域,let 有块级作用域。
什么意思呢?
就是使用 let 声明的变量,只能在当前的代码块中进行使用:
{
let a = 2;
}
console.log(a); // 报错,提示a没有定义
var 声明的变量,没有块级作用域,都可以访问:
{
var a = 2;
}
console.log(a); // 可以访问a,2
{
console.log(a); // 可以访问a,2
}
5.2 分支结构
正常情况下,我们的代码都是顺序执行的,一句一句从上到下执行。但是不可能什么情况下都是顺序执行,例如判断在生活中无处不在,我们登录某个系统,需要判断密码是否正确,正确才能登录,否则登录失败。

1 if-else 语句
在 JS 中通过 if else 来进行条件的判断,格式如下:
if (condition) {
// condition为true时执行的代码块
} else {
// condition为false时执行的代码块
}
也可以不需要 else:
if (condition) {
// 条件成立,要做的事情
}
if 语句首先会对 condition 表达式进行求值判断,结果为 boolean 值,也就是 true 或 false 。
如果为 condition 表达式的结果为 true,则执行 if 后的语句。如果 condition 表达式为 false 且有 else 代码块,则执行 else 代码块,如果没有 else,那么什么都不执行了。
举个栗子,判断一个数是否为偶数:
let num = 5;
if (num % 2 == 0) { // 除以2余数为0,是偶数
console.log(num + " 是偶数");
} else {
console.log(num + " 是奇数");
}
执行结果:
5 是奇数
如果 condition 表达式不是 boolean 值,则会自动转换为 boolean 值,所以可以对任何数值进行判断:
let a = 5;
let b; // 未初始化undefined
if (a) {
console.log("a 被转换为了 true");
}
if (!b) { // 进行了boolean取反
console.log("b 被转换为了 false");
}
- 根据前面数据类型转换,
5转换为true,undefined转换为false。
一般情况下,我们经常通过 关系运算 和 逻辑运算 作为 if 的判断条件。
举个栗子:
let a = 5;
if (a > 0 && a < 10)
console.log("a在0~10之间");
如果 if 或 else 后面只有一条语句,可以省略 {} ,但是建议加上,结构更清晰。
2 if-elseif-else 语句
if-else 只能进行是和否的判断。
if-elseif-else 可以进行多条件判断。
语法格式:
if (condition1) {
// condition1为true时执行的语句块
} else if (condition2) {
// condition2为true时执行的语句块
} else {
// 所有条件都为false时执行的语句块
}
else-if 可以有0个或多个,最后的 else 可以省略。
当满足一个条件后,其他的条件就不会再判断了,如果所有条件都不满足,会执行 else。
下面通过练习来举个栗子:
给出成绩,经过判断,打印出输入的成绩是优秀、良好、中等、及格还是不及格。
let score = 85;
if (score < 0 || score > 100) {
console.log("成绩不正确");
} else if (score >= 90) {
console.log("优秀");
} else if (score >= 80) {
console.log("良好");
} else if (score >= 70) {
console.log("中等");
} else if (score >= 60) {
console.log("及格");
} else {
console.log("不及格");
}
- 上面从上到下依次判断,当满足
score >= 80后,就不会对后面的条件进行比较了。
执行结果为:
良好
这里需要注意,条件的书写顺序,千万不要下面这样写:
let score = 85;
if (score >= 60) {
console.log("及格");
} else if (score >= 70) {
console.log("中等");
} else if (score >= 80) {
console.log("良好");
} else if (score >= 90) {
console.log("优秀");
} else if (score < 0 || score > 100) {
console.log("成绩不正确");
} else {
console.log("不及格");
}
- 上面只要成绩大于 60 就会进第一个判断,不会走到后面的判断。
if-else语句都是可以相互嵌套的,可以在 if-else 判断中再进行 if-else 判断。
举个栗子:
let x = 10;
let y = 20;
if (x > 5) {
if (y > 15) {
console.log("x 大于 5,y 大于 15");
} else {
console.log("x 大于 5,y 小于等于 15");
}
} else {
if (y > 10) {
console.log("x 小于等于 5,y 大于 10");
} else {
console.log("x 小于等于 5,y 小于等于 10");
}
}
重新修改一下程序,从弹出的对话框中接收输入的值,这里介绍一个 prompt() 函数,可以弹出一个对话框,接收用户的输入。因为输入的值是字符串,需要转换为数值。
let score = prompt("请输入成绩"); // 接收输入的值
if (null == score || score.trim() == "") {
alert("请输入成绩");
} else {
score = +score;
if (isNaN(score) || score > 100 || score < 0) {
alert("请输入正确的成绩");
} else {
if (score >= 90) {
alert("优秀");
} else if (score >= 80) {
alert("良好");
} else if (score >= 70) {
alert("中等");
} else if (score >= 60) {
alert("及格");
} else {
alert("不及格");
}
}
}
score.trim()将会去掉输入内容前后的空格,这里是为了防止输入空格内容;isNaN()函数可以判断数据是否是NaN;
运行后,页面会弹出一个对话框输入成绩:

3 三目运算符
假设现在有两个数字,我们希望获得其中较大的一个,那么可以使用 if-else 语句,例如:
let a = 4;
let b = 5;
let max;
if (a > b)
max = a;
else
max = b;
console.log("最大值为:" + max); // 最大值为:5
我们可以使用三目运算符来完成上面的功能,代码会更简洁的写法:
let a = 4;
let b = 5;
let max = a > b ? a : b;
console.log("最大值为:" + max); // 最大值为:5
let max = a > b ? a : b; 的意思是:先判断 a > b,如果成立,就返回 a,不成立就返回 b。
代码简洁了很多。能用三目运算符的地方都可以使用 if-else 代替。
4 switch语句
switch语句也是一种判断语句,通常用于对多个可能的值进行比较。switch语句的语法如下:
switch (value) {
case value1:
// value === value1 时执行的代码块
break;
case value2:
// value === value2 时执行的代码块
break;
default:
// value 不等于上面所有的case 时执行的代码块
}
注意:switch(value) 中的value,和 case 中的 value 是做全等判断,等于哪个 case,就执行哪个。 default 选择可以省略。
以下是一个示例代码,通过数字判断季节:
let day = 2;
switch (day) {
case 1:
console.log("春季");
break;
case 2:
console.log("夏季");
break;
case 3:
console.log("秋季");
break;
case 4:
console.log("冬季");
break;
default:
console.log("错误的季节");
}
当 day 满足哪个 case,就会执行哪个 case 后面的代码。
执行结果:
夏季
注意,每个case后,都需要进行 break 或 return,否则会跳到下一个 case。
举个栗子:
let day = 2;
switch (day) {
case 1:
console.log("春季");
break;
case 2:
console.log("夏季");
case 3:
console.log("秋季");
case 4:
console.log("冬季");
break;
default:
console.log("错误的季节");
}
因为 case 2 没有 break,所以执行完成,会跳到 case 3 执行,执行完 case 3 没有 break 或 return,会继续跳到 case 4执行,执行完成,case 4 有 break,则跳出。
执行结果:
夏季
秋季
冬季
如果对多个 case 的处理是一样的,就可以将 break 省略。
例如下面的代码中,day 为1,2,3,打印的都是秋季。
let day = 2;
switch (day) {
case 1:
case 2:
case 3:
console.log("秋季");
case 4:
console.log("冬季");
break;
}
5.3 循环语句
循环在日常生活中也很常见,例如我们要重复做某件事,就需要用到循环,循环不停的做某件事。

举个栗子,每天向小红送一条玫瑰花,坚持送100天,这个应该怎么实现呢?
1 for 循环
for循环是一种常用的循环语句,可以指定循环变量的初始值、终止条件和步长。
for循环的语法如下:
// 对应下面的各个部分
for(①初始化部分; ②循环条件部分; ④迭代部分){
③循环体部分;
}
// 执行过程是①-②-③-④-②-③-④-②-③-④......②
// 执行①,然后执行②,判断②是否成立,如果成立,执行③,③执行完成,执行④,④执行完成,再次判断②是否成立,成立继续一轮循环,执行③,②不成立,退出循环。
举个栗子:
for (let i = 1; i <= 5; i++) {
alert(i);
}
先执行 let i = 1,定义一个变量 i ,然后执行 i <= 5 ,判断 i 是否小于等于5,如果为 true,则执行循环体中的代码 alert(i) ,执行完循环体中的代码,执行 i++ ,然后再次执行 i <= 5 ,判断i 是否小于等于5,直到 i <= 5 不满足,则终止循环。
打印结果:
1
2
3
4
5
再举个例子:
for (let i = 1, j = 10; i <= 5 && j > 0 ; i++, j--) {
alert(i + j);
}
- 从上面可以看出,初始化可以定义多个变量,对多个变量进行判断。
当然你也可以将变量定义在外面:
let i = 1;
let j = 10;
// 或者 let i = 1, j = 10;
for (; i <= 5 && j > 0; i++, j--) {
alert(i + j);
}
在实际的开发中,for 循环是用的最多的。
2 while 循环
可以使用 while进行循环操作。
while 循环的语法:
while (condition) {
// condition为真时执行的代码块
}
代码执行到 while,会判断是否满足条件,如果满足,就会进入while循环,执行满足条件要做的事情,执行完成,会重新判断while后面的条件,如果满足会继续循环,如果不满足就不在进入循环。
回到一开始的提问:每天向小红送一条玫瑰花,坚持送100天,这个应该怎么实现呢?
let i = 1;
while (i <= 100) {
console.log("第" + i +"天,送你一朵玫瑰花"); // 控制台打印
i += 1;
}
首先我们定义了一个变量 i ,用来记录当前是第几天。
我们一开始定义了 i 为 1,执行到 while 循环时,先判断 i 是否小于等于100,如果小于等于100,则进入到循环,打印语句,然后执行 i += 1; 将 i 加1,然后再执行while后面的条件判断,判断 i <= 100,这样一直循环执行,当 i = 101时,刚好已经执行了100次,且不再满足 i < =100,此时终止循环。
注意,一定要对 i 进行累加,如果 i 不进行累加,永远会满足 i <= 100,那么循环将永远不会结束,变成死循环。
执行结果:
第1天,送你一朵玫瑰花
第2天,送你一朵玫瑰花
……
第99天,送你一朵玫瑰花
第100天,送你一朵玫瑰花
我们在计数的时候,i 也可以从0开始,例如:
let i = 0;
while (i < 100) {
console.log("第" + (i + 1) + "天,送你一朵玫瑰花"); // 控制台打印
i += 1;
}
判断的条件的设定是很灵活的,根据实际需求来就可以了,不是一成不变的。
练习:计算从1累加到100的和
let sum = 0; // 定义一个变量,存储累加的和
let i = 1; // 定义一个变量,标识累加到几了,从1开始累加
while (i <= 100) {
sum += i; // 将和累加
i += 1; // 将i加1,继续累加
}
console.log("1~100的和为:" + sum); // 打印最终的结果
执行结果:
1~100的和为:5050
使用 while 循环的地方都可以使用 for 循环代替。
3 do-while 循环
do...while 循环与 while 循环的区别在于,do...while 循环先执行一次循环体中的代码,再检查条件是否为真。如果条件为真,则继续执行循环体中的代码。
do...while 循环的语法如下:
do {
// 循环体中的代码块
} while (condition);
举个栗子,再送一遍花:
let i = 1;
do {
console.log("第" + i + "天,送你一朵玫瑰花");
i += 1;
} while (i <= 100);
4 死循环
什么是死循环,就是一直循环,不会终止的循环。
举个栗子:
下面的循环中,不执行步长的增长,i 始终为1,则 i <= 5 永远成立,所以循环不会终止。
for (let i = 1; i <= 5;) {
alert(i);
}
还可以这样写:
let i = 0;
for (;;) {
alert(i++);
}
上面的 for 循环会一直执行,不停的打印 i 的值,i 的值会一直增长。
同样 while 循环也可以定义死循环,非常简单:
while(true) {
}
死循环除非是自己需要,需要不停的循环,否则使用的时候要小心一点。
5 循环嵌套
循环是可以相互嵌套的,while 和 for 循环中可以再次嵌套 while 和 for 循环。
例如每天给小红送10朵玫瑰花,坚持100天。
let day = 1; // 记录第几天
while (day <= 100) { // 外层循环用于循环天数
for (let roseCount = 1; roseCount <= 10; roseCount++) { // 内层循环用于循环每一天送的花的朵数
console.log("第" + day + "天,第" + roseCount + "朵玫瑰花");
}
day += 1; // 天数+1
}
for、while、do-while 循环都可以进行多层嵌套。
一般我们使用 for 循环是最多的,要简洁一些:
for (let day = 1; day <= 100; day++) { // 外层循环用于循环天数
for (let roseCount = 1; roseCount <= 10; roseCount++) { // 内层循环用于循环每一天送的花的朵数
console.log("第" + day + "天,第" + roseCount + "朵玫瑰花");
}
}
6 中断循环
我们在前面终止循环,主要是靠不满足条件时自动跳出。这样的话,必须每一次的循环都执行完成,到达条件判断的时候才能跳出。
但是有时候,我们不得不提前退出循环,或者终止当前的循环继续后面的循环,这个时候就需要 break 和 continue 关键字了。
break 语句:
break 关键字用于直接结束当前所在的循环。
举个栗子:
如果一个循环,想在执行第三次的时候跳出:
let i = 0;
while (i < 10) {
if (i === 3) {
break;
}
console.log("i=" + i);
i++;
}
- 当
i = 3时,跳出了循环。
执行结果:
i=0 i=1 i=2
break只会跳出其所在的循环,举个栗子,循环有嵌套的时候:
for (let i = 0; i < 10; i++) {
for (let j = 0; i < 10; j++) {
if (j === 3) {
break;
}
console.log("i=" + i + ", j=" + j);
}
}
- 上面的代码,break只能跳出内部的那一个循环。无法直接跳出外部的循环。
如果就是想跳出外部的循环呢?
那就需要给外部的循环添加一个标签,举个栗子:
outer : for (let i = 0; i < 10; i++) {
for (let j = 0; i < 10; j++) {
if (j === 3) {
break outer;
}
console.log("i=" + i + ", j=" + j);
}
}
- 上面的代码中,给外层循环添加了一个
outer标签,标签名称是自定义的;在循环内,可以通过break 标签来跳出指定的循环。
可以为每个循环添加标签,如果有多层循环,可以通过这样的方式跳出想要跳出的循环。while 循环也可以添加标签。
continue 语句
continue的作用是中断本次循环,直接进入下一次循环。
举个栗子:
循环5次,在第三次的时候跳过,继续后面的执行:
for (let i = 0; i < 5; i++) {
if (i === 3) {
continue;
}
console.log("i=" + i);
}
- 在上面
i == 3的时候,终止了本次循环,直接跳到i++,继续后面的执行。
执行结果:
i=0 i=1 i=2 i=4
同样,continue 和 break 一样,也可以选择终止的是哪一层的循环。
举个栗子:
outer : for (let i = 0; i < 10; i++) {
for (let j = 0; i < 10; j++) {
if (j === 3) {
continue outer;
}
console.log("i=" + i + ", j=" + j);
}
}
- 在上面的代码中,给外层循环定义了一个标签,每次执行到
j === 3,就会终止本次循环,直接跳转到外层循环的i++,继续后面的循环。
练习:求素数
求 100 以内的质数(素数):
// 从 2 开始遍历到 100
for (let i = 2; i <= 100; i++) {
let isPrime = true;
// 检查 i 是否为质数
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) { // 能够整除,说明不是素数
isPrime = false;
break; // 直接跳出,判断下一个数是否是素数
}
}
// 如果是质数,则打印该数
if (isPrime) {
console.log(i);
}
}
- 外层 for 循环用来遍历从 2 ~ 100 的数值;
- 内层 for 循环用来判断每个数值是否能被
2 ~ Math.sqrt(num)整除,正常情况下求一个数是否是素数,你可能判断该数能否被2 ~ 该数-1整除,但是只需要判断2 ~该数的开方即可。
6.4 调试代码
在实际的开发中,编写的代码逻辑不可能一点不错,有时候代码逻辑有问题,就需要查看原因,通过打印日志的方式,会有一些麻烦。
我们可以在代码执行的时候,添加断点。
举个栗子:
下面的代码在运行的时候,运行到 debugger 会停住,我们可以一句一句运行。
<script>
debugger;
for(let i = 0; i < 5; i ++) {
console.debug();
}
</script>
- 上面的程序,执行到 debugger 的地方,会停住,等待调试。
- 程序可以添加多个 debugger 点。
运行的时候,打开浏览器的开发者工具,刷新页面,会进入到调试:

可以一行一行的运行,调试代码,还可以在监视中,添加要监视的变量,查看变量的具体值:

6.5 练习
随机生成一个1-100的整数,然后来猜这个数字的大小,通过 prompt() 函数进行输入,猜不对,提示大了还是小了,猜对了,跳出循环。在这里我们需要生成一个随机数,使用 Math.random() 来生成:
// 随机生成一个 1 - 100 的整数
const randomNumber = Math.floor(Math.random() * 100) + 1;
while (true) {
const userGuess = prompt("请猜1-100的整数");
// 检查用户输入是否为空或非数字
if (userGuess === null || isNaN(userGuess)) {
alert("输入无效,请输入一个数字!");
continue;
}
const guess = parseInt(userGuess);
if (guess === randomNumber) {
alert("恭喜你,猜对了!");
break;
} else if (guess > randomNumber) {
alert("猜大了,再试一次");
} else {
alert("猜小了,再试一次");
}
}
Math.random()是 JS 的内置函数,用于生成一个>= 0 且 < 1的随机浮点数,也就是范围[0, 1)。那么Math.random() * 100的范围是[0 ~ 100)的随机小数。Math.floor()同样是 JavaScript 的内置函数,它的功能是对传入的数字进行向下取整,对[0 ~ 100)的随机小数向下取整,得到的数值在[0, 99]范围的整数。再加1 就得到[1,100]的整数范围。- 如果想得到
[0,100]范围的随机整数,可以使用Math.floor(Math.random() * 101)。
六、对象
在前面讲到 JS 中的数据类型主要分为两类:原始数据类型(原始类型) 和 引用数据类型(对象类型)。
原始数据类型有:
- 数值(number)
- 字符串(string)
- 布尔值(boolean)
- 大整数(bigint)
- 空值(null)
- 未定义(undefined)
- 符号(symbol)
但是这些数据类型只能表示简单的数据,如果想表示一个人的信息,例如包括姓名、性别、年龄等,用基本数据类型只能定义三个变量表示,没办法从整体通过一个数据类型来表示。
现在来学习一个新的数据类型,对象类型,对象数据类型可以由多个各种数据类型组合而成,甚至其中还可以包含对象类型。
6.1 对象的基本操作
下面就来创建对象,添加属性、访问对象中的属性,以及修改属性、删除属性。
1 创建对象
举个栗子:
// 创建一个对象,赋值给person变量
let person = new Object();
// 给对象添加属性
person.name = '逗比';
person.gender = '男'
person.age = 18;
console.log(person); // 打印person变量
- 使用
new Object()创建一个对象; - 对象中添加的一个个选项叫属性,我们可以随意添加属性,并赋值,格式为:
对象.属性=值;上面给person对象添加了三个属性,并分别赋值。
执行结果:
{name: '逗比', gender: '男', age: 18}
创建对象还有简单的写法:
let person = Object(); // 可以省略new
console.log(person); // {}
- 打印没有添加属性的对象,结果为
{}。
创建对象还可以通过字面量 {} 的方式来创建,举个栗子:
// 创建一个对象,赋值给person变量
let person = {};
person.name = '逗比';
console.log(person); // 逗比
这种方式可以在创建对象的时候,直接添加属性:
// 创建一个对象,赋值给person变量
let person = { name: '逗比', gender: '男', age: 18 };
console.log(person); // {name: '逗比', gender: '男', age: 18}
2 访问属性
访问对象中的属性,直接通过 对象.属性 访问:
// 创建一个对象,赋值给person变量
let person = new Object();
// 给对象添加属性
person.name = '逗比';
person.gender = '男'
person.age = 18;
console.log(person.name); // 逗比
console.log(person.gender); // 男
console.log(person.age); // 18
3 修改属性值
修改属性值和添加属性是一样的:
// 创建一个对象,赋值给person变量
let person = new Object();
person.name = '逗比'; // 没有该属性的时候就是添加属性
person.name = '牛比'; // 修改属性值
console.log(person); // {name: '牛比'}
4 删除属性
可以使用 delete 删除对象中的属性。
举个栗子:
// 创建一个对象,赋值给person变量
let person = new Object();
person.name = "逗比"; // 添加属性
delete person.name;
console.log(person); // {}
console.log(person.name); // undefined
delete person.name;删除 person 对象中的 name 属性,删除属性后,再次访问,结果为undefined。
6.2 对象属性
1 属性名
对象中的属性名通常就是一个字符串,所以属性名可以是任何值,包括特殊字符,但是如果属性名太特别,不能直接通过 对象.属性 的格式进行赋值和访问,需要使用 对象['属性'] 这样的格式。
举个栗子:
let person = {};
// 给对象添加属性
person.name = "逗比";
person["gender"] = "男";
person["年龄"] = 18;
person["^*(&%)"] = "乱七八糟";
console.log(person); // {name: '逗比', gender: '男', 年龄: 18, ^*(&%): '乱七八糟'}
// 获取属性
console.log(person.name); // 逗比
console.log(person['name']); // 逗比
console.log(person['年龄']); // 18
console.log(person['^*(&%)']); // 乱七八糟
- 上面通过
person['属性']可以给 person 对象添加属性。 - 虽然属性名没有限制,但是强烈建议属性名和标识符一样,遵守命令规则,别浪!
通过 对象['属性'] 这样的方式操作属性,要比 对象.属性 更为灵活,可以完成 对象.属性 无法实现的功能。
举个栗子:
let person = {};
let str = "name";
person[str] = "逗比"; // 等价于person['name'] = "逗比";
// 获取属性
console.log(person.name); // 逗比
对象['属性']中的属性可以传递一个变量,这个是对象.属性无法实现的。你不能person.str='逗比',这个添加的是str属性。
属性名除了可以使用字符串,还可以使用符号来作为属性名。
举个栗子:
let person = {};
// 定义符号
let symbol1 = Symbol();
let symbol2 = Symbol();
// 给对象添加属性
person[symbol1] = '通过symbol1添加的属性';
console.log(person[symbol1]); // 打印: 通过symbol1添加的属性
console.log(person[symbol2]); // undefined
- 在上面通过符号作为对象的属性,需要注意,通过哪个符号添加的属性,只能通过哪个符号获取。
使用符号作为属性名不太常用,只有特殊的场景下才会使用,一般在不希望属性被外界访问的时候用到。
在使用字面量进行对象初始化的时候,也可以使用特殊字符和符号属性,写法如下:
// 创建一个符号
let mySymbol = Symbol();
// 创建一个对象,赋值给person变量
let person = {
name: '逗比',
gender: '男',
['age']: 18,
[mySymbol]: '符号通过symbol1添加的属性'
};
console.log(person); // {name: '逗比', gender: '男', age: 18, Symbol(): '符号通过symbol1添加的属性'}
2 属性是否存在
如何判断一个对象中是否有指定的属性呢?可以通过 in 关键字来判断。
举个栗子:
let person = {};
person['name'] = "逗比";
// 查看是否有指定的属性
console.log('name' in person); // true,有name属性
console.log('age' in person); // false,没有age属性
- 使用
in关键字来判断,有就返回 true,没有返回 false。
3 对象类型的属性
对象的属性还可以是对象,属性是可以相互嵌套的,可以举个栗子:
let person = {};
person.name = "逗比";
person.birthday = {}; // 生日定义为对象类型
person.birthday.year = 2025; // 通过多层访问属性,就不停的'点'就行了
person.birthday.month = 12;
person.birthday.day = 10;
// 访问属性
console.log(person.name); // 逗比
console.log(person.birthday); // {year: 2025, month: 12, day: 10}
console.log(person.birthday.year); // 2025
console.log(person.birthday.month); // 12
- 对象属性多层嵌套其实就是获取到属性后,继续按照访问对象的访问方式继续操作就可以了。
- 如果有更多层,也是按照这样的讨论继续访问就可以了。
其实上面的初始化太复杂了,像下面这些写就方便很多:
let person = {
name: '逗比',
birthday: {
year: 2025,
month: 12,
day: 10
}
};
4 枚举属性
有时候别人传递给我们一个对象,我们不知道其中有什么属性,那么可以遍历一下对象中的属性,看一下有哪些属性。
方法如下:
// 创建一个符号
let mySymbol = Symbol();
// 创建一个对象,赋值给person变量
let person = {
name: "逗比",
gender: "男",
["age"]: 18,
[mySymbol]: "符号通过symbol1添加的属性",
};
// 遍历对象中的属性
for (let propName in person) {
console.log("属性:", propName, ", 值:", person[propName]);
}
- 在上面的代码中使用
for-in遍历对象中的属性,对象有几个属性,for 循环就会执行几次;但是并不是所有的属性都会被枚举,使用符号创建的属性,是无法被枚举的。 - 每一次执行,就会将属性名赋值给 propName(这个名称是自定义的),获取到属性后,通过
person[propName]获取到属性的值。
执行结果:
属性: name , 值:逗比 属性: gender , 值:男 属性: age , 值:18
6.3 变量内存结构
1 基本数据类型变量
先看一下下面的代码:
let a = 2;
let b = a;
a = 5;
console.log(b); // 2
- 在上面的代码中,修改 a 的值,并不会影响 b 的值。
let a = 2;会在栈中为 a 分配内存空间,并赋值为 2;let b = a;会在栈中位 b 分配内存空间,并复制 a 的值给 b;a = 5;会修改 a 的内存空间的值为 5,并不会修改 b 的值。
如下图:

在 JS 中基本类型(undefined、null、boolean、number、 symbol)的变量在内存中是直接存储在栈中的,存储的是值本身。但是字符串比较特别,一般是存储在堆中的,因为字符串可能比较长,会占用比较大的内存空间。
2 引用类型变量
在看一下下面的代码:
let a = {name:'doubi', age:18}
let b = a;
a.name = 'niubi';
console.log(b.name); // niubi
- 上面的代码修改了 a 对象的值,为什么 b 对象的值也发生了变化呢?
这是因为对象类型的数据是在堆中分配的。
内存示意如下:
let a = {name:'doubi', age:18} ,在栈中和堆中分配内存空间,对象是在堆中存储的,栈中只是存储的堆中的对象的内存地址,字符串不是直接存储的,也是存储字符串的指针。

let b = a; ,将 a 赋值给 b 时,实际是将 a 存储的地址复制给 b,这样 b 也存储的是对象的地址,指向的也是同一个对象。

当执行 a.name = 'niubi'; 时,修改的是堆中对象的信息:

所以此时修改 a ,通过 b 来访问,是一样的。
再举个例子:
let a = {name:'doubi', age:18}
let b = a;
b = {name:'shabi', age:20}
b.name = 'erbi';
console.log(a.name); // doubi
内存示意如下:
let a = {name:'doubi', age:18} ,栈和堆都分配内存空间,栈指向堆中的内存空间:

let b = a; ,a 存储的地址赋值给 b,a和b指向堆中相同的空间:

b = {name:'shabi', age:20} ,在堆中新建了一个对象,开辟了新的内存空间,将 b 指向新的内存空间。

b.name = 'erbi'; ,修改 b 指向的堆中的内存空间:

引用类型的数据是存储在堆中的,而栈中保存的是指向堆中数据的引用(即内存地址或指针)。堆中数据的大小不固定,且堆内存的分配和回收比栈慢。堆内存由垃圾回收机制(GC)管理。
虽然字符串是基本数据类型,它们通常存储在堆中,栈中存储的是指向堆中字符串的引用。
3 const修饰变量
前面说 const 用来定义常量,也就是使用 const 声明的变量是无法修改的。
const a = 123;
a = 456; // 错误,是无法修改a的值的。
但是需要注意,引用类型的变量是可以修改对象的值的,只是不能指向新的对象。
什么意思?举个栗子:
const a = {name:'Doubi', age: 13};
a = {}; // 错误,无法修改a的值,a的值是堆中的地址,所以修改a指向的堆中的对象
但是可以修改 a 指向的对象的值:
const a = { name: "Doubi", age: 13 };
a.name = "niubi"; // 这是可以的
console.log(a); //{name: 'niubi', age: 13}
七、函数
什么是函数呢?
先举个栗子:
假设给定两个整数,想获取其中的最大值,那么可以编写代码如下:
// 只是举个栗子,以下代码不是最简洁方式
let num1 = 24;
let num2 = 12;
let maxNum;
if (num1 > num2) {
maxNum = num1;
} else {
maxNum = num2;
}
console.log("较大的数为:" + maxNum); // 较大的数为:24
一个地方需要这个功能,我在一个地方写了这个功能的代码,100个、1000个地方需要这个功能,那就需要写1000遍。万一这个功能需要修改,修改1000个地方不得崩溃。
所以我们可以将这个功能封装为一个函数,想要使用到这个功能的时候,直接调用这个函数即可。
所以通过函数,提高代码的复用性,减少重复代码,提高开发效率。
我们前面也已经调用过一些函数,例如 console.log()、alert()、parseInt() 等,他们都实现了某些特定的功能,也就是功能进行了封装,我们只要调用就可以实现想要的功能,不用再自己编写。
在 JS 中,函数也是对象,所以函数具有对象所以的功能,而且函数还可以封装代码,提供想要的功能。
而现在我们主要学习如何自定义函数。
7.1 函数的定义
函数需要先定义,然后我们就可以调用函数了。
1 定义函数
定义函数的语法如下:
function 函数名(参数1, 参数2, ...) { // 参数可以省略
// 实现功能的代码
}
下面定义一个函数,函数实现的功能就是就是打印一行语句:
/**
* 定义一个函数
*/
function sayHello() {
alert('Hello, 欢迎来到For技术栈');
}
- 上面定义了一个函数,函数实现的功能比较简单,就是输出一句话。
function关键字用来定义函数,sayHello是函数的名字,是自定义的,建议按照标识符命名规则,采用首字母小写的命名方式;- 然后是
(),用来指定参数,这里没有参数,后面会讲,{}是函数体部分,可以编写函数要实现的功能。
2 函数的调用
函数已经定义好了,下面来调用函数:
/**
* 定义一个函数
*/
function sayHello() {
alert("Hello, 欢迎来到For技术栈");
}
// 调用函数
sayHello();
// 再次调用函数
sayHello();
- 调用函数使用
函数名();这样的方式来调用,函数可以调用多次,调用一次就是将函数体的代码执行一次。
执行效果如下:

3 函数的类型
查看一下函数的类型:
// 定义一个函数
function sayHello() {
alert("Hello, 欢迎来到For技术栈");
}
console.log(typeof sayHello); // function
- 使用 typeof 查看函数类型为
function,typeof操作符返回function并不是object,这只是为了区分函数和普通对象,函数只是对象类型的一种特殊形式。
4 匿名函数
上面定义函数的方式指定了函数名称,这种方式为具名函数,还有不指定函数名的函数,称为匿名函数。
举个栗子:
// 定义一个函数
let func = function() {
alert("Hello, 欢迎来到For技术栈");
}
// 调用函数
func();
- 上面使用
function(){...}定义了一个匿名函数,并赋值给 func 变量; - 在调用的时候,通过变量调用即可。
5 箭头函数
ES6 引入的箭头函数,是一种更为简洁的定义函数的方式。
举个栗子:
// 定义一个函数
let func = () => {
alert("Hello, 欢迎来到For技术栈");
}
// 调用函数
func();
- 上面通过
() => {}的方式定义函数,并将函数赋值给变量 func,所以箭头函数也是匿名函数。
如果箭头函数的函数体只有一条语句,还可以省略 {} 。
举个栗子:
// 定义一个箭头函数
let func = () => alert("Hello, 欢迎来到For技术栈");
// 调用函数
func();
7.2 函数的参数
上面定义的函数没有参数,在实际的使用中,函数一般都有参数,例如我们之前使用的 alert('Hello'); 括号中的就是参数,通过传递不同的参数,来让函数针对不同的数据进行处理。
1 函数参数
下面来定义一个函数,作用是计算任意两个数的和,所以函数需要接收两个参数,在函数中计算两个参数的和。
代码如下:
/**
* 定义两个数相加的函数
*/
function add(a, b) {
let result = a + b;
console.log("result:", result);
}
// 调用函数
add(1, 2);
- 首先在函数的括号中定义参数,参数个数不限,使用逗号分隔;函数的参数相当于在函数内部定义的局部变量,但是没有初始化。
- 在调用函数的时候,传递参数,会将值传递给函数的参数进行初始化。在上面的代码中,调用
add()函数,会将1复制给a,2复制给b。 - 函数的参数叫形参(形式参数),传递的参数叫实参(实际参数,实际执行的参数),不过在实际的开发中,一般说函数的参数就完了。
加深印象,再举个栗子:
function sayHello(name, age) {
console.log(`Hello, I'm ${name}, I'm ${age} years old`);
}
let name = "Doubi";
let year = 13;
// 调用函数
sayHello(name, year); // Hello, I'm Doubi, I'm 13 years old
// 再次调用函数
sayHello('Niubi', 10); // Hello, I'm Niubi, I'm 10 years old
- 上面定义了一个
sayHello()函数,接收两个参数,上面定义实参name和age和函数的形参名字是一样的,但两者没有任何关系,它们之间只是值的传递。
2 参数的个数
正常情况下,函数有几个参数,我们传递几个参数就可以了,一一对应,会将传递的值和函数的参数进行对应。
但是 JS 比较骚,传递的参数(实参)和函数的个数(形参)可以不相同。
举个栗子:
/**
* 定义两个数相加的函数
*/
function add(a, b) {
console.log('a =', a, ", b =", b)
let sum = a + b;
// console.log("sum:", sum);
}
// 调用函数
add(); // a = undefined , b = undefined
add(1); // a = 1 , b = undefined
add(1, 2, 3, 4); // a = 1 , b = 2
- 从上面的执行可以看出,如果实参个数多于形参个数,多的实参会被忽略;如果实参个数少于形参个数,形参的值为 undefined,没有初始化。
虽然 JS 传递的参数的参数和函数的参数个数可以不同,但是强烈建议你猥琐发育,别浪!
3 参数的类型
JS 是弱类型语言,定义的函数的参数也没有类型约束,所以传递什么类型的数据都是可以的。
举个栗子:
/**
* 定义两个数相加的函数
*/
function add(a, b) {
let sum = a + b;
console.log("sum:", sum);
}
// 调用函数
add(2, 3); // sum: 5
add('Hello', 'For技术栈'); // sum: HelloFor技术栈
add(true, 2); // sum: 3
add(null, 2); // sum: 2
add(2, 'abc'); // sum: 2abc
- 上面函数实现的是两个参数的相加,所以按照算数运算符号进行运行。
- 所以在实际的开发中,一定要注意参数的类型。
4 箭头函数的参数
箭头函数的参数,在有且仅有一个参数的时候,括号 () 可以省略。
举个栗子:
let func = (a) => {
console.log("a:" + a);
}
// 一个参数的时候,()可以省略,可以写为:
let func = a => {
console.log("a:" + a);
}
5 参数默认值
函数的参数可以指定默认值。
举个栗子:
function add(a=10, b=20, c=30) {
console.log('sum:' + (a + b + c));
}
add(1); // 没有传递b和c的值,b默认为20,c默认为30
add(1, 2); // 没有传递c的值,c的值默认为30
add(1, 2, 3);
- 参数指定默认值后,如果不传递参数,将使用默认值。
6 参数值传递
在 JavaScript 中调用函数传递参数是值传递,不是地址传递!
什么意思呢?举个栗子:
我们编写代码,交换 a 和 b 两个变量的值,代码如下:
let a = 3;
let b = 5;
// 交换a和b的值
let temp = a; // 需要一个中间变量
a = b;
b = temp;
console.log(a); // 5
console.log(b); // 3
交换两个变量这个功能,可能其他地方也会用到,于是写一个函数来实现:
/**
* 交换两个变量
*/
function swap(a, b) {
let temp = a;
a = b;
b = temp;
}
let a = 3;
let b = 5;
// 调用交换函数
swap(a, b);
console.log(a); // 3
console.log(b); // 5
运行代码,发现 a 和 b 的值并没有被交换,不科学啊,为什么呢。
首先执行:let a = 3; let b = 5; ,函数中的变量会在栈中开辟内存空间:

继续执行,调用 swap 函数,swap 函数的形参 (a, b) 和变量 a 和 b 根本就不是同一个变量,只是这里恰巧名称相同而已,只是将变量a和b的值赋值给了 swap 函数的形参。
然后执行 swap 函数中的代码,创建了 temp 变量,调换了函数中 a 和 b 的值,所以并没有改变变量a和b的值。
再看一段代码:
// 修改a的属性
function change(a) {
a.name = 'niubi';
}
let a = {name:'doubi', age:18}
change(a);
console.log(a.name); // niubi
为什么上面的函数又能修改a的属性值呢?
这是因为函数的参数是值传递,但是变量 a 是引用类型变量,存储的是地址值,所以传递的也是地址值。
调用方法后,会将 a 的值传递给函数参数a,那么函数参数 a 也指向堆中的对象:

然后修改参数 a 的值,也就修改了变量 a 的值:

所以这里也是值传递,只是传递的是引用的地址,所以指向的是堆中同一块内存地址。
7.3 函数的返回值
上面写的函数都是没有返回值的,计算两个数的和,我想拿这个和继续处理其他的逻辑,目前是不行的。
所以有时候,我们是需要函数将处理的结果返回给我们,例如 let num = parseInt(str); 函数接收一个字符串,返回一个数值。
1 返回值
函数如果要返回值,使用 return 关键字。
举个栗子:
function add(a, b) {
let sum = a + b;
return sum; // 使用 result 返回值
}
let result = add(2, 3); // 调用函数
console.log(result);
- 上面使用 return 将计算的结果返回;
- 在调用函数时,我们可以使用变量接收返回的值。
我们也可以简化上面的代码:
function add(a, b) {
return a + b; // 使用 result 返回值
}
console.log(add(2, 3)); // 直接输出调用结果
函数的返回值需要定义变量来接收,不过,如果你只是调用,不接收结果,也是没有问题的:
2 返回值的类型
任何类型的数据都可以作为函数的返回值,包括对象和函数。如果 return 不写返回值,那么返回 undefined,如果不写 return,返回值也是 undefined。
举个例子:
function add(a, b) {
let sum = a + b;
return; // 没有返回任何值
}
let result = add(2, 3);
console.log(result); // undefined
- 上面的代码中,函数直接return,没有返回值,那么函数的返回结果是 undefined。
同样,如果函数不写 return,调用函数,接收函数的返回结果,也是 undefined。
如下:
function add(a, b) {
let sum = a + b;
}
let result = add(2, 3);
console.log(result); // undefined
3 终止函数执行
return 表示终止函数的执行,当执行 return 后,会直接返回,表示函数执行完成。
例如,我们写一个除法的方法,检查一下除数是否是 0,如果是0,直接返回:
function divide(a, b) {
if (b == 0) {
return;
}
return a / b;
}
let result = divide(5, 0);
console.log(result); // undefined
- 在上面的代码中,先判断除数是否是0,如果是0,则直接将函数返回,函数将终止执行。
所以如果 return 语句后面有代码,则无法被执行。
举个栗子:
function add(a, b) {
return a + b; // 这里函数就返回了
console.log(a + b); // 前面已经return,这里无法被执行到
alert(a + b);
}
let result = add(2, 3);
console.log(result); // 5
- 函数执行到 return 就返回了,后面的代码无法被执行到。
4 箭头函数的返回值
如果箭头函数的函数体只有一句 return 语句,那么可以省略 {} 。
举个栗子:
let add = (a, b) => a + b;
console.log(add(2, 3)); // 5
但是这样有一个问题,如果返回的数据是对象类型,编译器没办法区分是函数的 {} 还是 对象的 {} 。
let obj = (a, b) => {name: name, age: age}; // 无法区分{}是函数的还是对象的。
需要使用小括号() ,括起来,如下:
let func = (name, age) => ({name: name, age: age});
let person = func('doubi', 18);
console.log(person.name); // doubi
7.4 函数的嵌套调用
函数的嵌套调用就是一个函数可以调用另外一个函数,另外一个函数还可以继续调用其他的函数,依此类推。
举个栗子:
下面定义了2个函数,funA() 和 funB(),并在main()函数中调用了funA() ,然后在funA()中调用了 funB()。
function funB() {
console.log("----b");
}
function funA() {
console.log("----a1");
funB();
console.log("----a2");
}
funA();
执行结果:
----a1 ----b ----a2
我们会发现在 funA() 中调用 funB() 后,funB() 执行完成,重新回到了 funA() 继续执行。
7.5 函数作为对象属性
对象的属性除了是基本数据类型和引用数据类型,还可以是函数。
当使用函数作为对象的属性的时候,一般称之为对象的方法,本质上函数和方法是一样的。
举个栗子:
let person = {};
person.name = 'doubi';
person.age = 13;
person.run = function(count) {
alert(`奔跑吧, 骚年! 跑个${count}米`);
}
person.run(100); // 奔跑吧, 骚年! 跑个100米
- 在上面的代码中,将一个函数赋值给对象的属性,然后可以通过对象来调用这个方法。
- 在前面通过
console.log()也是通过对象来调用方法。
7.6 作用域
什么是作用域?
作用域就是变量的可访问范围,简单来说就是在哪里可以访问到变量。
在 JS 中作用域分为全局作用域和局部作用域,局部作用域又可以分为块级作用域、函数作用域。
1 全局作用域
全局作用域中声明的变量,在代码的任何地方都可以访问。
举个栗子:
let a = 123;
{
let b = 456;
console.log(a); // 可以访问a
}
function test() {
let c = 789;
console.log(a); // 可以访问a
// console.log(b); // 无法访问b
}
test();
console.log(a); // 也可以访问
// console.log(b); // 无法访问b
// console.log(c); // 无法访问c
- 在上面的代码中,变量
a是全局作用域,直接在<script>标签中声明的代码就是在全局作用域中,可以在任何地方访问。 - 变量b 和变量c 只能在所在的代码块或函数中访问,它们是局部作用域。
- 全局作用域是在网页运行的时候创建,在网页关闭的时候销毁;而块级作用域只有在代码块运行的时候创建,运行完成就销毁;函数作用域会在函数每次被调用的时候创建,每次都是创建新的作用域,函数调用完成就销毁了。
注意:不使用 var/let/const 声明的变量,会默认变成全局变量(这是一个坑)。
举个栗子:
function test() {
a = 123; // 错误做法:没有声明
}
test();
console.log(a); // 输出 123(不推荐)
- 非常不推荐
2 块级作用域
ES6 引入了 let 和 const,它们拥有块级作用域。
块级作用域:在一对花括号 {} 中声明的变量,只在这对大括号中有效。
{
let x = 10;
const y = 20;
console.log(x, y); // 正常访问
}
console.log(x); // 报错:x is not defined
而 var 没有这个能力,会被提升到函数作用域或全局作用域。
if (true) {
var a = 123;
}
console.log(a); // 输出123,var不受块级作用域限制
- 上面的变量a 使用 var 声明,没有块级作用域,因为变量a 也没有在函数中,所以被提升到全局作用域。
3 函数作用域
函数内部声明的变量,只能在该函数内部访问。
function test() {
var a = 123;
console.log(a); // 正常访问
}
test();
console.log(a); // 报错:a is not defined
- 上面的变量a 只能在函数内部访问。
因为 var 声明的变量没有块级作用域,所以在函数中声明的变量会被提升为函数作用域(不是块级的)。
function test() {
if (true) {
var a = 5;
}
console.log(a); // 输出 5,不报错
}
test();
console.log(a); // 无法访问
- 虽然 var 没有块级作用域,但有函数作用域;上面的变量a只能在函数中访问。
4 作用域链
当访问一个变量时,JS 会在当前作用域查找变量,如果找不到,会去父一级的作用域查找,如果找到就使用,还找不到,继续向上查找,直到在全局作用域查找,如果还找不到,就报错了xx is not defined,这样就形成了一条链子,也就是作用域链。
let a = 1;
function outer() {
let a = 2;
function inner() {
let a = 3;
console.log(a); // 输出:3
}
inner();
}
outer();
- 上面代码中,查找变量 a 的顺序:
inner()→outer()→全局。所以作用域链就是查找变量的过程。
5 window对象
window 对象是 JavaScript 在浏览器环境中最核心的全局对象之一。它代表的是浏览器窗口,我们可以通过 window 对象对浏览器窗口进行各种操作(后面再讲),window 对象负责存储 JS 中的内置对象和函数,例如 String、Number 、console 、alert()函数等对象,并充当了 JavaScript 中的全局作用域对象。
window 对象中的属性和函数,可以直接调用,也可以通过 window. 调用。
举个栗子:
alert('Hello');
// 等价于
window.alert('Hello');
console.log('Hello');
// 等价于
window.console.log('Hello');
给 window 对象添加的属性,会自动成为全局变量。
举个栗子:
window.a = 123;
console.log(a); // 123, 全局变量
前面讲到 var 声明的变量不具有块作用域,在全局作用域中使用 var 声明的变量,会作为 window 对象的属性。
举个例子:
<script>
var abc = 123;
console.log(abc); // 123
// 等价于
console.log(window.abc); // 123
</script>
同样,在全局作用域中,使用 function 声明的函数,会作为 window 对象的方法。
举个例子:
function test() {
console.log('Hello');
}
test();
// 等价于
window.test();
看一下下面的情况:
let a = 123;
window.a = 456;
console.log(a); // 123
- 使用 let 声明的变量是不会作为 window 对象的属性的;
- 直接访问全局变量,会先访问 let 声明的变量;
7.7 提升
JavaScript 真的有一些乱七八糟的特性,真的没啥用,开发不用,但面试用。
1 变量提升
看一下下面的代码:
不使用 var 声明的变量和使用 var 声明的变量有什么区别?
a = 123;
console.log(a);
var a = 123;
console.log(a);
上面的代码有什么区别?用起来好像没区别。
改写一下:
// 先访问a
console.log(a); // 报错:a is not defined
a = 123;
- 上面的代码
console.log(a)报错,因为变量 a 没有定义,很好理解,你得先定义后使用;
再看一下 var 声明的变量:
// 先访问a
console.log(a); // undefined
var a = 123;
- 上面的代码不报错,
console.log(a)打印变量 a 的值为undefined。
为什么呢?
这是因为变量提升,使用 var 声明的变量,它会在所有的代码执行前被声明,注意是声明,不是值,所以值是 undefined,执行到赋值的代码的时候才被赋值。
可以理解成下面的代码:
var a;
// 先访问a
console.log(a); // undefined
a = 123;
这特性有啥意义,没啥意义,我们在开发中,变量要做到先声明,后使用!
变量在函数中中定义也会被提升的,看一下下面的代码:
var a = 1;
function test() {
console.log(a); // undefined
var a = 2;
console.log(a); // 2
}
test();
console.log(a); // 1
- 在函数内定义的变量 a ,被提升到函数执行之前被声明,所以函数内的第一句
console.log(a);执行时,函数内的变量 a 没有被赋值,所以是 undefined。 - var 声明的变量没有块作用域但是有函数作用域,所以函数内访问的变量 a 是函数内定义的变量a,不是操作的全局变量a;
- 函数中没有修改全局变量a;
2 函数提升
同样使用 function 定义的函数,也会被提升,也就是调用的代码可以写在函数定义前面。
举个栗子:
test(); // 可以写在函数前面
function test() {
console.log('Hello');
}
可以理解为下面的代码:
function test() {
console.log('Hello');
}
test(); // 可以写在函数前面
需要注意,下面这样是不行的:
func(); // 报错:func is not a function
var func = function () {
console.log("Hello");
};
- 上面报错,func 是变量,作为变量被提升了,值为 undefined,肯定不能作为函数来调用;
另外,使用 let 声明的变量也会被提升,但是在赋值之前是禁止被访问的,所以下面这样的代码也会报错:
// 先访问a
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 123;
- 但是报的错不一样,不是
a is not defined,而是初始化之前禁止访问。
为什么需要提升?
主要是为了预先分配内存,减少后期重新分配内存的次数,提高执行效率。
3 变量同名
使用 let 是不能定义两个相同名称的变量的,let 和 var 也不能定义相同名称的变量。
举个栗子:
let a = 1;
let a = 2; // 报错:Identifier 'a' has already been declared
下面的情况也报错:
let a = 1;
var a = 2; // 报错:Identifier 'a' has already been declared
但是下面的情况不报错:
var a = 1;
var a = 2;
console.log(a); // 2
- 后面变量的赋值会覆盖前面变量的赋值。
因为这里涉及到提升,提升后的代码类似:
var a;
a = 1;
a = 2;
console.log(a); // 2
所以看一下下面的代码,结构就知道了:
var a = 1;
var a;
console.log(a); // 1
- 第一句代码,已经提升了变量a,后面重新定义变量a 是没有效果的,因为没有重新赋值。
- 所以后续的声明不会覆盖之前的值,除非显式地重新赋值。
4 函数同名
后定义的函数会覆盖前面定义的函数。
举个栗子:
function test(a) {
console.log('Hello')
}
function test(a, b) {
console.log('World')
}
test(); // World
- 后定义的函数会覆盖前面定义的同名函数。
5 变量与函数同名
变量名和函数名可以相同,但需要注意提升和覆盖规则。
举个栗子:
console.log(foo); // 输出:foo() {}
var foo = "Hello";
function foo() {}
console.log(foo); // Hello
- 函数声明会比变量优先提升。
- 如果同名,函数会覆盖变量(除非变量被赋值)。
提升完,代码类似:
function foo() {}
console.log(foo);
var foo = "Hello";
console.log(foo);
如果在运行时对同名变量赋值,它会覆盖之前的函数。
function bar() {}
var bar = 1;
console.log(bar); // 输出:1
但是需要注意,上面都是使用 var 定义的变量,如果使用的 let 或 const,那么变量和函数是没法同名的。
let a = 1;
function a() {} // 报错:Identifier 'a' has already been declared
- 我们在开发的时候,要使用 let,不要使用 var。
7.8 立即执行函数
在介绍立即执行函数之前,先说一下问题。
我们在实际的开发中,尽量不要在全局作用域中写代码,为什么呢?
因为正常情况下可能多人开发项目,每个人将代码写到各自的 .js 文件中,但是可能在同一个页面中引入,那么这些在全局作用域中定义的变量和函数是会冲突的。
为了解决这个问题,我们可以创建局部作用域,在局部作用域中编写代码。
举个栗子:
{
let a = 1;
console.log(a);
}
{
let a = 2;
console.log(a);
}
- 上面的代码就创建了两个局部作用域,其中定义的函数和变量就不会冲突了。
但是上面使用的是 let,如果是 var 就没有块作用域了,那么怎么解决呢?
我们可以使用函数,因为 var 有函数作用域:
function func1() {
var a = 1;
console.log(a);
}
func1(); // 执行才可以
function func2() {
var a = 2;
console.log(a);
}
func2(); // 执行才可以
- 上面定义了两个函数,就有了两个函数作用域,两个函数中的变量或函数就不会冲突了。
但是现在仍然有问题,如果在多个文件中定义多个函数,多个人编写没办法保证它们不重名,另外函数需要执行才有效果,所以能否编写一个没有名字(这样就不会重名了)还立即执行的函数呢?
这就来了,我们可以定义只执行一次的匿名函数。
举个栗子:
(function() {
var a = 1;
console.log(a);
})();
- 上面的代码,首先创建一个匿名函数,正常是要赋值给一个变量的,这里没有赋值给变量,所以在外侧添加了
()。 - 后面的
()相当于调用函数了,和普通调用函数一样,所以上面的匿名函数会立即执行。
还可以这样写:
(function() {
var a = 1;
console.log(a);
}());
- 上面将调用函数的括号放到括号里面。
这样多个立即执行函数之间就有独立的作用域,不存在冲突了。
(function() {
var a = 1;
console.log(a);
}());
(function() {
var a = 2;
console.log(a);
}());
如果你的代码风格是不加分号 ; 的,那么代码会报错。
你可以在后面添加分号,也可以将分号写在前面,如下:
;(function () {
var a = 1;
console.log(a);
})()
;(function () {
var a = 2;
console.log(a);
})()
7.9 严格模式
JavaScript 有两种代码执行模式:正常模式(Sloppy Mode)和严格模式(Strict Mode)。
它们的主要区别在于严格模式对代码的约束更严格,能帮助开发者避免一些潜在错误,会让代码执行更快,提高代码执行效率。
在开发中能用严格模式,推荐使用严格模式。
1 正常模式
代码默认就是正常模式执行,该模式下允许一些不太严谨的语法和行为。
例如不声明变量:
// 正常模式示例
x = 10; // 未声明变量,自动成为全局变量
console.log(x); // 10
- 正常模式下,代码能不报错就不报错,但可能会导致一些潜在问题。
2 严格模式
严格模式通过 "use strict" 指令启用,我们可以在全局开启,也可以只在某个函数中开启。
全局开启严格模式
在 <script> 的标签开始的地方写,或者在整个脚本文件的开始处写。
"use strict";
// x = 10; // 未声明变量,直接使用会报错
let x = 10;
console.log(x); // 10
函数开启严格模式
也可以只针对某个函数开启严格模式:
function test() {
"use strict"; // 开启严格模式
// ...严格模式代码
}
test();
7.10 this
在函数执行的时候, JavaScript 解释器每次都会传递一个隐含的函数,就是 this 。
1 普通函数的this
举个栗子:
function test() {
console.log(this);
}
test();
执行如下:

- 可以看出,当调用函数的时候,
this其实指向的是window对象。 - 如果是严格模式,输出的是 undefined。
在看一下下面的代码:
function test() {
console.log(this);
}
let person = {
name: 'Doubi',
sayHello: test // 赋值为函数
}
person.sayHello(); // {name: 'Doubi', sayHello: ƒ}
- 此时是通过对象调用方法的形式来调用函数,此时
this指向的是调用方法的对象。
其实上面两种情况是一样的,因为直接调用函数的时候,也是 window.函数(); ,其实是通过 window 对象来调用的,所以 this 是指向调用函数的对象的。
这个有什么用呢?
在使用对象调用函数的时候,可以在函数中,访问到对象的属性。
举个栗子:
let person = {
name: 'Doubi',
sayHello: function() {
console.log(`Hello, I'am ${this.name}`);
}
}
person.sayHello(); // Hello, I'am Doubi
- 在上面的代码中,函数中
this指向的是对象,所以可以通过 this 来访问对象中的属性,此时属性值发生变化,函数是不用修改的。
所以对于普通函数而言,函数中中的 this 和它的调用方式是由关的。
2 箭头函数的this
箭头函数没有自己的 this,它的 this 是由外层作用域 this 决定的,所以箭头函数的 this 和它的调用方式无关。
举个例子:
let person = {
name: 'Doubi',
sayHello: () => {
console.log(this);
}
}
person.sayHello(); // window
- 上面sayHello 函数中中的 this 为什么指向 window,是因为对象的花括号
{}不构成作用域,所以箭头函数的 this 会继续向外查找,找到全局作用域,所以指向的是 window。
重新修改一下:
let person = {
name: 'Doubi',
sayHello: function() {
let test = () => {
console.log(this);
}
test();
}
}
person.sayHello(); // {name: 'Doubi', sayHello: ƒ}
- 在上面的代码中,test 箭头函数中的 this 指向的是外层作用域的 this,外层作用域是普通函数,此时外层函数中的 this 指向的是调用的对象,所以内部的箭头函数的 this 指向的也是调用的对象。
7.11 对象的简写
ES6 引入了对象简写的语法糖,可以简化对象字面量的定义。
1 属性简写
当属性名和变量名相同时,可以省略键值对的 value,直接写属性名。
举个栗子:
const name = 'Alice';
const age = 25;
// 传统写法
const person = {
name: name,
age: age
};
// 简写写法
const person = {
name, // 属性和变量名相同
age
};
console.log(person); // { name: 'Alice', age: 25 }
2 方法简写
对象中的方法可以省略 function 关键字。
举个栗子:
// 传统写法
const person = {
sayHello: function() {
console.log('Hello!');
},
sayGoodbye: function() {
console.log('Goodbye!');
}
};
// 简写写法
const person = {
sayHello() {
console.log("Hello!");
},
sayGoodbye() {
console.log("Goodbye!");
},
};
person.sayHello(); // "Hello!"
person.sayGoodbye(); // "Goodbye!"
更多推荐
所有评论(0)