JavaScript学习笔记

JavaScript学习笔记

小叶子

封面作者:雨里

内容并非完全线性, 如有不解可暂时跳过

为控制篇幅, 部分Web API已拆分为独立文章(推荐在学完 基础 部分后再学习)

⭐基础

JavaScript 是一种弱类型解释型脚本语言, 主要用于网页开发, 既可以面向对象编程, 也可以面向过程编程(在前端开发中, 一般都是面向对象编程)

面向对象的编程语言中, 每个对象都是一个某一主题的功能中心; 完成某个任务的过程就是调用各种对象的属性和方法(功能)进行处理; 具有灵活、易维护、易开发、封装性(可复用)、继承性多态性的特点

面向过程的编程语言, 如 CMATLAB, 强调解决问题的各个步骤, 每个步骤都被设计为一个函数, 然后依此调用这些函数, 即先干什么再干什么; 而面向对象的编程强调解决问题的对象, 即先找谁干什么, 再找谁干什么; 面向过程一般具有高性能的特点

运行和调试

JavaScript 程序需要在一定的环境, 如 浏览器Node.jsDenoBun 中运行, 在基础部分, 我们主要以 浏览器 为例; 有两种方式可以将 JavaScript 代码引入 HTML:

1
2
3
4
5
6
7
8
9
<!-- 内联形式: 通过 script 标签包裹 JavaScript 代码 -->
<script>
alert('这是内联形式引入的JavaScript代码');
</script>

<!-- 外部形式: 通过 script 的 src 属性引入独立的 .js 文件 -->
<script src="demo.js">
// 使用此方法后, script标签中的代码将不会被执行!
</script>
1
2
// demo.js
alert('这是外部形式引入的JavaScript代码');

JavaScript 中每节代码末尾的 ; 是可以省略的, 后面的代码都将以省略 ; 的形式书写

断点调试的方法

  1. 在浏览器中打开文件
  2. 通过 F12ctrl + shift + i 打开开发者工具
  3. Sources 中找到要调试的 JavaScript 文件
  4. 点击行号左侧添加断点
  5. 刷新页面
  6. 使用 F10F11 单步调试, 后者会进入函数内部

defer 属性

  • defer 属性用于延迟脚本的执行, 即脚本会在文档解析完毕后再执行, 即使脚本在页面中的位置靠前
  • 可用于避免代码内容获取不到页面元素、代码阻塞页面渲染等问题
  • 对于 module 类型的脚本, defer 属性是默认的
1
<script defer src="script.js"></script>

基本输入输出

方法描述
alert()在浏览器中弹出提示框, 显示指定内容
document.write()HTML 文件中添加指定内容
位置取决于 script 标签的位置
console.log()在浏览器的控制台中输出指定内容
一般用于调试程序, 应在正式上线前删除
res = prompt('提示内容')在浏览器中弹出提示框, 用户可以输入内容
输入的字符串将被赋值给 res
res = confirm('提示内容')在浏览器中弹出提示框, 用户可以点击确定或取消
确定则将 true 赋值给 res, 取消则将 false 赋值给 res

要通过 prompt 获取数值型数据, 可以用 num = +prompt('提示内容')num = Number(prompt('提示内容')) 进行类型转换 详见后文

注释

通过注释可以屏蔽代码被执行或者添加备注信息, JavaScript 支持三种形式注释语法:

  • 单行注释: 使用 // 注释单行代码, VScode 中快捷键为 ctrl + /
  • 多行注释: 使用 /* */ 注释多行代码, VScode 中快捷键为 shift + alt + a
  • 文档注释: 使用 /** */ 注释函数或类, 用来声明函数或类的作用、参数、返回值等信息
1
2
3
4
5
6
7
8
9
// 这是一个单行注释

/* 这是一个
多行注释 */

/*
这也是一个
多行注释
*/

文档注释

详见JSDoc部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 这是一个加法函数
* 用于计算两个数的和
* @param {number} a 加数
* @param {number} b 加数
* @returns {number} 两个数的和
* @example
* console.log(plus(1, 2)) // 1 加 2, 输出 3
*/
function plus(a, b) {
return a + b
}
// 书写文档注释后, 可以通过鼠标悬停在函数名上查看注释

// 对对象参数进行文档注释
/**
* @param {Object} obj 参数对象
* @param {string} obj.name 姓名
* @param {number} obj.age 年龄
* @returns {string} 返回姓名和年龄
*/
function getName(obj) {
return obj.name + obj.age
}
语句描述
@param {type} name description参数及其类型和描述
@returns {type} description返回值及其类型和描述
@example示例
@author name <contact>(可选)作者及联系方式
@license license许可证

{} 中可以是 *stringbooleanobjectfunction 等数据类型, 也可以是 DateArrayRegExp 等对象, 还可以是 'GET' | 'POST'1 | 0 等特定值

变量

计算机存储数据的容器

声明

  • let 变量名 声明可变变量
  • const 变量名 声明不可变变量, 即常量(不可变 只表示变量名指向的内容不可变, 详见数据类型
  • 早期用 var 变量名 声明变量, 现在不推荐使用
  • 变量名可以: 包含字母、数字、下划线、$ 在较新的浏览器中可以包含中文
  • 变量名不能: 以数字开头、是 JavaScript 内置的关键字, 如 let, 或保留字, 如 int
  • 变量名应该: 有一定意义、用驼峰命名法 userName、sumScoreMath
  • 变量名区分大小写

赋值

  • 在声明变量后赋值, 如 let name; name = '小叶子'
  • 在声明变量的同时赋值, 如 let name = '小叶子'
  • 常量必须在声明的同时赋值, 如 const PI = 3.14, 且不允许重新赋值
  • 能用 const 就不要用 let

函数内部声明的变量, 称为局部变量, 只能在函数内部使用; 详见作用域

数据

JavaScript 中, 声明变量时无需指定数据类型, 系统会根据变量的值自动判断数据类型, 这种特性称为弱类型语言, 可以通过 typeof 检测数据类型, 如 console.log(typeof 变量名)console.log(typeof(变量名))

基本数据类型

也称为包装类型

类型说明
数值型 numberJavaScript 中的数值类型包含整数和浮点数
字符串型 string通过单引号 ''、双引号 ""、反引号 `` 包裹的数据都叫字符串
布尔型 boolean表示真或假的数据, 有两个固定的值 truefalse
未定义型 undefined表示声明了变量但未赋值, 如 let name = ''string 型, 而 let nameundefined
空值型 null表示声明了变量并赋值, 但值为空, 如 let name = null
  • 因历史原因, typeof null 的结果是 object 而非 null
  • 转义符 \ 可以将表意字符转换成普通字符, 如 \' 表示单引号
  • 需要在字符串内使用引号时, 应用不同的引号, 或者使用转义符
  • 除上述的基本数据类型外, 还有 SymbolBigInt 两种类型, 会在后面介绍
  • 由于小数(浮点数)在计算机中是以二进制存储的, 所以在计算时会有精度问题, 如 0.1 + 0.2 结果为 0.30000000000000004; 如果需要精确计算, 可以转换为整数再计算
  • 比较大的数值可以使用分隔符 _ 分隔, 如 let num = 1_000_000

NaN, Not a Number

  • 在数字计算失败时, 显示的结果是 NaN
  • NaN 和任何值都不相等, 包括它自己
  • 任何对 NaN 的运算, 结果都是 NaN
  • NaN 与任何值比较, 结果都是 false

复杂数据类型

也称为引用类型

类型说明
对象型 object表示一组数据的集合, 类似于 C 语言中的结构体, 但功能更强大
数组型 array表示一组数据的集合, 长度不固定, 也属于对象类型数据
函数型 function表示一段可执行的代码, 如 function getNumber() {一些代码}
  • 栈内存: 由系统自动分配, 存储基本数据类型的值和复杂数据类型的地址
  • 堆内存: 由程序员分配和释放, 存储复杂数据类型的值
  • 基本数据类型的变量存储的是数据的, 即变量名指向数据的值
  • 复杂数据类型的变量存储的是数据的地址, 即变量名指向数据的地址
  • 两种数据类型的变量名都指向栈内存, 其中基本数据类型指向数据的值, 复杂数据类型指向数据的地址
  • 复杂数据类型的数据的值存储在堆内存, 通过存储在栈内存中的地址来进行访问
  • const 声明的变量, 只是不允许修改变量名指向的栈内存中的内容, 但是可以修改堆内存中的内容

JavaScript 中其实没有真正意义上的栈和堆, 这里借用了其他语言中的概念

上述特性示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 声明一个对象
let obj1 = { name: '小叶子', age: 18 }
// obj1 指向栈内存中的地址, 如 0x001
// 该地址指向堆内存中的数据 { xxx }

// 将 obj1 赋值给 obj2
let obj2 = obj1
// 此操作仅仅是将 obj1 指向的地址赋值给了 obj2
// 即 obj2 也指向栈内存中的地址 0x001

// 修改 obj2 的值
obj2.age = 12
// 此时 obj2 指向的地址中的数据 { xxx } 被修改

// 查看 obj1 的值
console.log(obj1.age) // 12
// 由于 obj1 和 obj2 指向同一个地址
// 所以 obj1 的数据也被修改了

// --------------------------------------

// 声明一个常量
const obj3 = { name: '小叶子', age: 18 }
// obj3 指向栈内存中的地址, 如 0x002
// 由于常量的特性, 不允许修改 obj3 指向的地址

obj3 = { name: '小叶子', age: 12 } // 报错

// 但是可以修改堆内存中的数据
obj3.age = 12
console.log(obj3.age) // 12

模板字符串

  • 模板字符串用于方便地拼接字符串(而非使用多个加号, 如 console.log('我叫' + name + ', 今年' + age + '岁了')
  • 模板字符串必须使用反引号 `` 包裹, 字符串中的变量使用 ${变量名} 表示
  • ''"" 时, 一对引号必须位于同一行, 而模板字符串 `` 可以换行
  • alertconsole.log 中, 换行会如实显示
  • HTML 中(即 document.write 中)无论多少换行都会显示为一个空格, 应该用 <br> 换行
  • ${} 中可以进行简单的运算, 如 ${age + years}${name.toUpperCase()}
1
2
3
4
5
6
7
const name = '小叶子'
const age = 18
const years = 3
// 传统方法
console.log('我叫' + name + ', 今年' + age + '岁了')
// 模板字符串
console.log(`我叫${name}, 今年${age}岁了, ${years}年后我就${age + years}岁了`)

类型转换

隐式转换

某些运算符被执行时, 系统内部自动将数据类型进行转换, 这种转换称为隐式转换

  • 通过 + 可以将字符串转换成数值类型, 如 +'1' 结果为 1, 此时 + 表示 正号(这个机制有时很有用, 如通过 prompt 获取数字型数据: let age = +prompt('请输入您的年龄')
  • 当字符串和数字进行 + 运算时, 数字会转换成字符串, 如 '1' + 1 结果为 '11'
  • 当字符串和数字进行 -*/% 运算时, 字符串会转换成数值类型, 如 '1' - 1 结果为 0, 其中 '' 会转换成 0
  • null 进行数字运算时, 会转换成 0
  • undefined 进行数字运算时, 会转换成 NaN
  • 通过 ! 可以将数据转换成布尔类型, 如 !0 结果为 true, !1 结果为 false

显式转换

隐式转换规律不清晰, 为了避免因其带来的问题, 通常需要对数据进行显式转换

方法描述
Number()将数据转换成数值类型, 当转换失败时结果为 NaN
parseInt()将数据只保留整数, 如 parseInt('12.8px') 结果为 12
parseFloat()将数据只保留浮点数, 如 parseFloat('12.8px') 结果为 12.8
String()将数据转换成字符串类型
变量.toString(进制)将变量转换成字符串类型, 进制为可选参数
num.toString(2) 将数字转换成二进制字符串
Boolean()将数据转换成布尔类型
只有 0NaN''nullundefined 会转换成 false, 其他都会转换成 true

parseIntparseFloat 只支持转换以数字开头的字符串, 如 '12px' 可以转换, 而 'px12' 不可以转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const num = 1 // 数值
const str = '2' // 字符串

console.log(num + str) // 结果为 12
// 数值 num 转换成了字符串, 相当于 '1'
// 然后 + 将两个字符串拼接到了一起

console.log(num - str) // 结果为 -1
// 字符串 num2 转换成了数值, 相当于 2
// 然后数值 1 减去 数值 2

console.log(num + Number(str)) // 结果为 3
// 通过 Number() 将字符串 num2 转换成了数值 2
// 然后数值 1 加上 数值 2
parseInt 实现秒数转时分秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>类型转换</title>
</head>
<body>
<script>
let sec = +prompt('请输入秒数: ')
function secToTime(sec) {
if (isNaN(sec) || sec < 0) {
return '请输入正确的秒数'
}
let s = parseInt(sec % 60)
let m = parseInt(sec / 60 % 60)
let h = parseInt(sec / 60 / 60 % 24)
s < 10 ? s = '0' + s : s
m < 10 ? m = '0' + m : m
h < 10 ? h = '0' + h : h
return `${h}${m}${s}秒`
}
alert(secToTime(sec))
</script>
</body>
</html>

进制

JavaScript 中的数值型数据不止可以是十进制, 还可以是二进制、八进制、十六进制, 分别用 0b0o0x 开头表示

1
2
3
4
5
6
7
8
9
10
// 十进制表示 324
const num10 = 324
// 二进制表示 324
const num2 = 0b101000100
// 八进制表示 324
const num8 = 0o504
// 十六进制表示 324
const num16 = 0x144
// 打印时都是十进制
console.log(num10, num2, num8, num16) // 324 324 324 324

二进制运算符

下面的运算符会将操作数 (十进制) 转换成 32 位整数 (由 01 组成), 然后执行操作, 最后返回结果 (十进制)

运算符说明
&: 按位与1010 & 1100 结果为 1000
|: 按位或1010 | 1100 结果为 1110
~: 按位非~1010 结果为 0101
^: 按位异或1010 ^ 1100 结果为 0110
异或门: 两个输入相同时输出为 0, 不同时输出为 1
<<: 左移1010 << 1 结果为 10100
左移一位相当于乘以 2
>>: 右移1010 >> 1 结果为 0101
右移一位相当于除以 2
>>>: 无符号右移1010 >>> 1 结果为 0101
右移一位, 最高位补 0
&=: 按位与赋值a &= b 相当于 a = a & b
|=: 按位或赋值a |= b 相当于 a = a | b
^=: 按位异或赋值a ^= b 相当于 a = a ^ b
<<=: 左移赋值a <<= b 相当于 a = a << b
>>=: 右移赋值a >>= b 相当于 a = a >> b
>>>=: 无符号右移赋值a >>>= b 相当于 a = a >>> b
1
2
3
4
5
6
7
8
9
const a = 10 // 1010
const b = 12 // 1100
console.log(a & b) // 8 即 1000
console.log(a | b) // 14 即 1110
console.log(~a) // -11 即 0101
console.log(a ^ b) // 6 即 0110
console.log(a << 1) // 20 即 10100
console.log(a >> 1) // 5 即 0101
console.log(a >>> 1) // 5 即 0101

数组

Array, 用于存放一组数据的集合, 长度不固定, 属于对象类型数据

1
2
3
4
5
const arr = [] 
// 声明一个空数组
const arr = [1, '小叶子', true]
// 用 '[]' 包裹数据, 数据之间用 ',' 分隔
// 内部各元素的数据类型可以不同

数组索引

数组的每一个元素都有一个唯一的索引 也叫下标, 通过索引可以访问或修改数组中的元素 也叫数组单元, 索引从 0 开始

  • 通过索引访问数组中的元素, 如 arr[0] 获取数组中的第一个元素
  • 通过索引修改数组中的元素, 如 arr[0] = 1 将数组中的第一个元素修改为 1
  • 通过索引添加数组中的元素, 如 arr[6] = 1 将数组中的第五个元素添加为 1, 此时第四个元素不存在, 为 undefined
  • 通过索引删除数组中的元素, 如 delete arr[0] 删除数组中的第一个元素, 删除后该元素变为 undefined, 数组长度不变
索引的相关属性和方法描述
arr.length获取数组的长度, 即数组中元素的个数
arr.indexOf('xxx')获取数组中 'xxx' 这个特定元素的索引; 若有重复, 则返回最小索引; 若数组中不存在该元素, 则返回 -1
arr.lastIndexOf('xxx')同上, 但有重复时返回最大索引
arr.findIndex()查找元素, 返回符合测试条件的第一个数组元素索引值, 如果没有符合条件的则返回 -1
arr.slice(0, 2)截取数组中索引为 01 的元素, 不包含 2, 返回新数组; 不传参数时, 返回整个数组
findIndex() 的用法
1
2
3
4
// 查找数组中大于 3 的第一个元素的索引
let arr = [1, 3, 2, 5, 4]
let ans = arr.findIndex(ele => ele > 3)
console.log(ans) // 3

数组修改

修改的相关属性和方法描述
arr.push('啦啦啦')向数组末尾添加元素 '啦啦啦', 并返回新数组的长度
arr.pop()删除数组末尾的元素, 并返回被删除的元素
arr.unshift('啦啦啦')向数组开头添加元素 '啦啦啦', 并返回新数组的长度
arr.shift()删除数组开头的元素, 并返回被删除的元素
arr.reverse()反转数组
arr.sort()对数组进行排序, 默认按照字符编码升序排序
arr.splice()从指定位置删除和(或)添加元素
arr.fill(xxx, 1)xxx 填充索引 1 开始的所有元素, 包括 1
sort() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [1, 3, 2, 5, 4]
let ans = []

ans = arr.sort((a, b) => {
// 这是一个比较函数
// 返回值的正负会决定 a 和 b 的相对顺序
// 若返回值为正, 则 a 在 b 后面
// 若返回值为负, 则 a 在 b 前面
// 若返回值为 0, 则 a 和 b 的相对顺序不变
return a - b
})

console.log(ans) // [1, 2, 3, 4, 5]

=> 称为箭头函数, 详见箭头函数

splice() 的用法
  • arr.splice(0, 2): 从索引 0 开始, 删除 2 个元素, 即 01
  • arr.splice(0, 0, '啦啦啦'): 从索引 0 开始, 添加 '啦啦啦' 元素, 原来的元素依次后移
  • arr.splice(0, 1, '啦啦啦'): 将索引 0 的元素替换'啦啦啦'
  • arr.splice(0, 2, '啦啦啦'): 从索引 0 开始, 删除 2 个元素, 然后添加 '啦啦啦' 元素

数组方法

方法描述
arr.concat(x, y, ...)arr 末尾依此连接数组 xy, 并返回新数组
arr.map()对数组中的每个元素进行处理, 并返回一个新数组
arr.reduce()对数组中的每个元素进行累加, 并返回一个值
arr.join()将数组转换成字符串, 可指定分隔符, 返回这个字符串
arr.forEach()对数组中的每个元素进行调用, 并返回 undefined
arr.filter()对数组中的每个元素进行筛选, 并返回一个新数组
arr.every()检测是否数组中每个元素都满足条件, 并返回 truefalse
arr.some()检测是否数组中有元素满足条件, 并返回 truefalse
arr.find()检测并返回数组中符合条件的第一个元素, 若没有则返回 undefined
Array.from(xxx)将类数组或可迭代对象转换成数组, 并返回这个数组
arr.includes(xxx)检测数组中是否包含 xxx, 并返回 truefalse
arr.at(index)获取数组中指定索引的元素, 若索引为负数, 则从末尾开始计算
arr.toReversed()ES2023 新增, 不修改原数组, 返回一个反转后的新数组
arr.toSorted()ES2023 新增, 不修改原数组, 返回一个排序后的新数组
arr.toSpliced()ES2023 新增, 不修改原数组, 返回一个截取后的新数组
map()join() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const arr = ['red', 'blue', 'pink']

// map
const newArr = arr.map(function (ele, index) {
// 第一个参数表示数组元素
// 第二个参数表示索引号, 可选
return '<td>' + ele + '</td>'
// 如果是对象数组, 可用 ele.xxx 获取对象的属性
})
console.log(newArr) // ['<td>red</td>', '<td>blue</td>', '<td>pink</td>']

// join
// 默认逗号分割
console.log(newArr.join()) // <td>red</td>,<td>blue</td>,<td>pink</td>
// 空字符表示没有分隔符
console.log(newArr.join('')) // <td>red</td><td>blue</td><td>pink</td>
// 指定分隔符
console.log(newArr.join('<br>')) // <td>red</td><br><td>blue</td><br><td>pink</td>
reduce() 方法
1
2
3
4
5
6
7
8
9
10
const arr = [1, 2, 3, 4, 5]
const sum = arr.reduce(function (pre, cur, index, arr) {
// pre 表示上一次调用回调函数的返回值
// cur 表示当前元素
// index 表示索引号, 可选
// arr 表示数组本身, 可选
return pre + cur
}, 0) // 0 表示初始值, 即第一次运行时 pre 的值
// 若不指定初始值, 初始值为数组的第一个元素, 且从数组的第二个元素开始循环
console.log(sum) // 15
forEach() 方法
1
2
3
4
5
6
7
8
9
const arr = ['red', 'blue', 'pink']
const newArr = arr.forEach(function (ele, index) {
// 第一个参数表示数组元素
// 第二个参数表示索引号, 可选
console.log(ele)
console.log(index)
// 总是返回 undefined
})
console.log(newArr) // undefined

forEach() 方法无法中断循环, 如需中断循环, 应使用 for 循环

filter() 方法
1
2
3
4
5
6
7
8
9
const arr = ['red', 'blue', 'pink']
const newArr = arr.filter(function (ele, index) {
// 第一个参数表示数组元素
// 第二个参数表示索引号, 可选
return ele.length > 3
// 返回值为 true 时, 将元素添加到新数组中
// 返回值为 false 时, 将元素过滤掉
})
console.log(newArr) // ['blue', 'pink']
every()some()find() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const arr = [1, 2, 3, 4, 5]

// every
let ans = arr.every(function(ele, index) {
// ele 表示数组元素
// index 表示索引号, 可选
// 返回值为 true 时, 继续循环, 直到所有元素都满足条件, 返回 true
// 返回值为 false 时, 中断循环并返回 false
return ele > 2
})
console.log(ans) // false

// some
let ans = arr.some(function(ele, index) {
// 返回值为 true 时, 中断循环并返回 true
// 返回值为 false 时, 继续循环, 直到所有元素都不满足条件, 返回 false
return ele > 2
})
console.log(ans) // true

// find
let ans = arr.find(function(ele, index) {
// 返回值为 true 时, 中断循环并返回当前元素
// 返回值为 false 时, 继续循环, 直到所有元素都不满足条件, 返回 undefined
return ele > 2
})
console.log(ans) // 3
from() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 类数组
let obj = document.querySelectorAll('div')
Array.from(obj).forEach(ele => console.log(ele)) // 类数组没有 forEach 方法

// 字符串
let str = 'hello'
Array.from(str).forEach(ele => console.log(ele)) // h e l l o

// 子元素
element.addEventListener('click', e => {
if (Array.from(element.children).indexOf(e.target) === 0) {
console.log('点击了第一个子元素')
}
})
at() 方法

ECMAScript 2022 中引入了 Array.prototype.atString.prototype.at 方法, 用于获取数组和字符串中的值

1
2
3
4
5
6
7
8
9
const arr = [1, 2, 3, 4, 5]
// 获取下标为 2 的值
console.log(arr.at(2)) // 3
// 获取倒数第 2 个值
console.log(arr.at(-2)) // 4
// 获取倒数第 1 个值
console.log(arr.at(-1)) // 5
// 获取超出范围的值
console.log(arr.at(5)) // undefined

Array 表示数组构造函数, arr 表示数组, 不能混用; 除了数组修改这一节, 其他的实例方法请默认不会修改原实例

运算符

运算符分类说明
+算术运算符加法运算符 若包含字符串, 则表示拼接
-算术运算符减法运算符
*算术运算符乘法运算符
/算术运算符除法运算符
%算术运算符取模 余数 运算符, 常用于判断能否被整除
**算术运算符幂(指数)运算符, 相当于 ^
=赋值运算符将右侧的值赋值给左侧的变量
+=赋值运算符加法赋值, num += 1 等同于 num = num + 1
-=赋值运算符减法赋值, num -= 1 等同于 num = num - 1
*=赋值运算符乘法赋值, num *= 1 等同于 num = num * 1
/=赋值运算符除法赋值, num /= 1 等同于 num = num / 1
%=赋值运算符取模赋值, num %= 1 等同于 num = num % 1
++自增运算符变量的值加 1, num++ 等同于 num = num + 1
自减运算符变量的值减 1, num-- 等同于 num = num - 1
>比较运算符表示是否大于, 1 > 2 结果为 false
<比较运算符表示是否小于, 1 < 2 结果为 true
>=比较运算符表示是否大于等于, 1 >= 2 结果为 false
<=比较运算符表示是否小于等于, 1 <= 2 结果为 true
==比较运算符表示是否等于, 1 == '1' 结果为 true
===比较运算符表示是否严格等于, 1 === '1' 结果为 false
!=比较运算符表示是否不等于, 1 != '1' 结果为 false
!==比较运算符表示是否严格不等于, 1 !== '1' 结果为 true
&&逻辑运算符逻辑与, 一假则假, true && false 结果为 false
||逻辑运算符逻辑或, 一真则真, true || false 结果为 true
!逻辑运算符逻辑非, 取反, 若 a = true!afalse
  • 推荐使用 ===!== 而非 ==!=
  • num++++num 的区别在于前者先使用后加, 后者先加后使用 同C语言
  • 用比较运算符比较字符串时, 会依此比较字符的 ASCII 码, 如 'a' < 'b''aa' < 'ab''bba' > 'bb' 的结果都为 true

优先级

优先级运算符说明
1小括号()
2其他一元运算符++--!**
3算术运算符 */%, +-
4大小比较运算符><>=<=
5相等比较运算符==!====!==
6逻辑运算符 &&, ||
7赋值运算符=+=-=*=/=%=
8逗号运算符,

使用 ** 时, 不能将一元运算符 +/-/~/!/typeof 放在底数之前, 例如, -2 ** 2 是无效的, 必须写成 - (2 ** 2)(-2) ** 2

逻辑赋值

运算符说明示例
&&=逻辑与赋值a &&= b 等同于 a = a && b
||=逻辑或赋值a ||= b 等同于 a = a || b
??=空值合并赋值a ??= b 等同于 a = a !== null ? a : b

操作符

操作符说明
in用于检测对象中是否有某个属性
const name = 'name' in obj ? '有' : '无'
new用于创建对象实例
const obj = new Object()
typeof用于检测数据类型
const type = typeof obj // 'object'
instanceof用于检测对象是否是某个类的实例
const isArr = arr instanceof Array // true
delete用于删除对象的属性
delete obj.name, delete arr[0]
void用于返回 undefined
const result = void 0 // undefined
...用于展开数组或对象
const arr = [1, 2, 3]; const newArr = [...arr]
??空值合并运算符, 用于判断左侧是否为 nullundefined, 若是则返回右侧值
const name = obj.name ?? '无名'
相比于 ||, ?? 不会将 0''false 等值判定为 false
?.可选链运算符, 用于判断左侧是否为 nullundefined, 若是则返回 undefined
const name = obj?.name
objnullundefined, 则 nameundefined
1
2
3
4
5
6
7
8
// 用 void 标识立即执行函数
void function() {
console.log('立即执行函数')
}()
// 与 +function(){}() 同理, 但语义更明确

// 用 void 强制箭头函数返回 undefined
button.onclick = () => void console.log('点击了按钮')

本部分内容为后文内容总结和补充, 可先略过; 完整详见MDN

逻辑中断

逻辑运算符 &&|| 有一个特殊的功能, 即逻辑中断

  • && 左侧为假时, 右侧的表达式不会被执行, 返回左侧值; 为真时, 返回右侧值
  • || 左侧为真时, 右侧的表达式不会被执行, 返回左侧值; 为假时, 返回右侧值
  • 注意: &&|| 的返回值不是布尔类型, if 的判断条件中, 实际上将返回值隐式转换为了布尔类型
  • 但是: &&|| 在决定返回值时, 会将数据隐式转换成布尔类型, 然后再进行判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// || 可用于设置默认值
(function test(a, b) {
a = a || 1
b = b || 2
console.log(a, b)
})() // 结果是 1 2

// && 用法示例
function test(a = false, b = 0) {
let s = parseInt(b % 60)
let m = parseInt(b / 60 % 60)
let h = parseInt(b / 60 / 60 % 24)
a && (s < 10 ? s = '0' + s : s)
a && (m < 10 ? m = '0' + m : m)
a && (h < 10 ? h = '0' + h : h)
return `${h}${m}${s}秒`
}

// || 用法示例
const dpr = window.devicePixelRatio || 1
// 如果无法获取到 window.devicePixelRatio, 则使用 1

// 演示易产生困惑的地方
console.log(1 && 2) // 2
console.log(1 || 2) // 1
console.log(false && 2) // false
console.log(false || 2) // 2
console.log(0 && 2) // 0
console.log(0 || 2) // 2
console.log(1 < 2 && 2) // 2
console.log(1 < 2 || 2) // true(比较运算的返回值为布尔类型)

语句和表达式

  • 表达式: 由变量、运算符组成的表示一个的式子, 如 1 + 1a > ba && bfunc()
  • 语句: 控制某些行为的一段代码, 不一定有返回值, 如 alert()if (a > b) {一些代码}break
  • 分支语句: 根据条件执行不同的代码, 如 ifswitch
  • 循环语句: 重复执行某些代码, 如 whilefor

if

1
2
3
4
5
6
7
8
9
10
11
12
13
if (条件表达式) {
// 条件为真时执行的代码
} else {
// 条件为假时执行的代码
}

// 可以嵌套
const score = +prompt('请输入您的成绩: ')
if (score >= 90) alert(`你的成绩是${score}, 成绩优秀`)
else if (score >= 80) alert(`你的成绩是${score}, 成绩良好`)
else if (score >= 60) alert(`你的成绩是${score}, 成绩及格`)
else alert(`你的成绩是${score}, 成绩不及格`)
// 只有一行代码时, 可以省略大括号

条件表达式中的结果会被自动转换成布尔类型, 相当于 Boolean(条件表达式)

switch

1
2
3
4
5
6
7
8
9
10
11
switch (表达式) {
case1:
// 表达式的值为值1时执行的代码
break
case2:
// 表达式的值为值2时执行的代码
break
default:
// 表达式的值为其他时执行的代码
break
}
  • break 用于退出 switch, 若没有 break, 则会依此执行后续所有 case
  • 分支较少时, 一般使用 if else 而非 switch
switch 实现简单计算器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 用户输入
const num1 = +prompt('请输入第一个数字: ')
const operator = prompt('请输入运算符: ')
const num2 = +prompt('请输入第二个数字: ')
// 2. 判断输出
switch (operator) {
case '+':
alert(`${num1} + ${num2} = ${num1 + num2}`)
break
case '-':
alert(`${num1} - ${num2} = ${num1 - num2}`)
break
case '*':
alert(`${num1} * ${num2} = ${num1 * num2}`)
break
case '/':
alert(`${num1} / ${num2} = ${num1 / num2}`)
break
default:
alert('您输入的运算符有误')
break
}

三元表达式

一些简单的双分支可以用三元运算符代替 if else, 语法为 条件表达式 ? 为真时执行的代码 : 为假时执行的代码, 一般用于赋值

1
2
3
4
5
6
7
8
9
10
11
// 用于赋值
let score = +prompt('请输入您的成绩: ')
let grade = score >= 60 ? '及格' : '不及格'

// 用于执行语句
score = +prompt('请输入您的成绩: ')
score >= 60 ? alert('及格') : alert('不及格')

// 在模板字符串中使用
score = +prompt('请输入您的成绩: ')
alert(`您的成绩是${score}, 成绩${score >= 60 ? '及格' : '不及格'}`)
用三元运算符实现数字补零
1
2
3
4
// 1. 用户输入
let num = +prompt('请输入一个数字: ')
// 2. 判断输出
alert(num < 10 ? `你输入的数字是0${num}` : `你输入的数字是${num}`)

while

1
2
3
4
while (条件表达式) {
// 循环体
// 即条件为真时, 重复执行的代码
}
  • 条件表达式的性质同 if 语句
  • 使用 break 退出循环
  • 使用 continue 结束本次循环, 即跳过循环体中 continue 之后的代码, 继续下一次循环
while 循环实现打印偶数
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 初始化变量
const num = +prompt('请输入一个数字: ')
console.log(`小于${num}的偶数有: `)
let i = 0
// 2. 循环判断
while (i <= num) {
// 3. 打印偶数
if (num % 2 === 0) {
console.log(num)
}
// 4. 变量自增
i++
}

do while

先执行代码后判断条件, 即至少执行一次

1
2
3
do {
// 循环体
} while (条件表达式)

实际开发中 do while 循环使用较少

for

1
2
3
for (起始值; 条件表达式; 变化量) {
// 循环体
}
  • 起始值: 声明循环变量, 如 let i = 0
  • 条件表达式: 同 while 循环
  • 变化量: 循环体执行完毕后, 循环变量的变化, 如 i++
  • 同样可以使用 breakcontinue
for 循环实现打印偶数
1
2
3
4
5
6
7
8
9
10
// 1. 初始化变量
const num = +prompt('请输入一个数字: ')
console.log(`小于${num}的偶数有: `)
// 2. 循环判断
for (let i = 0; i <= num; i++) {
// 3. 打印偶数
if (num % 2 === 0) {
console.log(num)
}
}
for 循环实现遍历数组
1
2
3
4
5
6
// 1. 初始化数组
const arr = ['定风波', '苏轼', '莫听传林打叶声', '何妨吟啸且徐行', '竹杖芒鞋轻胜马', '谁怕', '一蓑烟雨任平生', '料峭春风吹酒醒', '微冷', '山头斜照却相迎', '回首向来萧瑟处', '归去', '也无风雨也无晴']
// 2. 遍历数组
for (let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
for 循环计算数组统计量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义数组
const arr = [1, 2, 3, 4, 5]
// 求数组的均值
let sum = 0
for (let i = 0; i < arr.length; i++) {
sum += arr[i]
}
let avg = sum / arr.length
console.log(avg)
// 求数组的最大值
let max = arr[0]
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i]
}
}
console.log(max)
// 求最小值同理

相比于 while 循环, for 循环更加简洁, 且循环变量仅限于循环体内使用, 所以更加常用

无限循环

while(true)for(;;) 实现无限循环, 需要通过 break 退出循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// while 无限循环
while (true) {
let ans = confirm('您是否同意用户协议?')
if (ans) {
alert('谢谢')
break
}
}
// for 无限循环
for (;;) {
let ans = confirm('您是否同意用户协议?')
if (ans) {
alert('谢谢')
break
}
}

循环嵌套

在循环体中再嵌套循环, 注意: 循环变量不能相同

1
2
3
4
5
6
// 九九乘法表
for (let i = 1; i <= 9; i++) {
for (let j = 1; j <= i; j++) {
console.log(`${j} * ${i} = ${i * j}`)
}
}

函数

函数是执行特定任务的一个代码块, 用于将常用的代码封装起来, 从而方便重复使用, 前面用过的 alert() 等就是 JavaScript 内置的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明函数
function 函数名(形式参数1 = 默认值1, 形式参数2 = 默认值2, ...) {
// 参数非必须, 可以不写默认值
// 只有在调用函数时没有传入实际参数时, 才会使用默认值
// 如果不写默认值, 各参数的默认值为 undefined
函数体 // 即要执行的代码
return 返回值
// return 非必须, 返回值可以是数据、变量、表达式
// 只能有一个返回值, 可以用数组或对象返回多个数据
// 如果不写 return, 则返回值为 undefined
}

// 调用函数
函数名(实际参数1, 实际参数2, ...)

// 调用函数, 并将返回值赋值给变量 ans
const ans = 函数名(实际参数1, 实际参数2, ...)
  • 函数名: 函数的名称, 命名规则同变量名, 建议用动词开头, 提示函数功能
  • 函数体: 函数的功能代码, 运行到 return 时会结束函数, 并返回返回值
  • 形式参数 形参: 在声明函数时使用的变量, 只在函数内生效, 在函数体内只能引用形式参数
  • 实际参数 实参: 在调用函数时使用的变量或表达式, 实际参数在函数内部被赋值给形式参数
  • 返回值: 函数执行完毕后对外输出 即赋给"函数名()" 的数据或变量
用函数实现打印偶数
1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明函数
function printEven(num) {
console.log(`小于${num}的偶数有: `)
for (let i = 0; i <= num; i++) {
if (i % 2 === 0) {
console.log(i)
}
}
}
// 声明变量
const input = +prompt('请输入一个数字: ')
// 调用函数
printEven(input)
用函数实现冒泡排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明函数
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
let temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
// 每次循环, 将前 arr.length - i 个元素中最大的元素放到最后
return arr
}
// 声明变量
const input = [3, 1, 4, 2, 5]
// 调用函数
const ans = bubbleSort(input)

函数的注意事项

  • 如果声明相同的函数名, 则后面的函数会覆盖前面的函数, 且在严格模式下会报错
  • 形参和实参的个数可以不同
  • 形参多于实参时, 多余的形参值为默认值
  • 实参多于形参时, 多余的实参会被忽略
  • 函数可以调用外部声明的变量, 但函数内部声明的变量不能被外部调用, 详见作用域
  • 函数会优先调用内部声明的变量, 如果没有才会调用外部声明的变量

回调函数

回头再调用的函数, 将 A 函数作为参数传递给 B 函数, 这个 A 函数就叫做回调函数 callback, A 函数会在 B 函数内部被调用; 多用匿名函数作为回调函数; 使用回调函数 A 时只需写函数名, 加上括号表示调用函数 A 的返回值, 即函数 A 会立即执行, 并以返回值作为参数传递给 B 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 以添加事件监听为例
// <button id="btn">按钮</button>
// 声明回调函数
function fn() {
console.log('我是一个回调函数...')
return 1
}
// 声明函数
const btn = document.querySelector('#btn')
btn.addEventListener('click', fn) // 传入函数
btn.addEventListener('click', fn()) // 传入 1, 事件触发时会报错
btn.addEventListener('click', function () {
fn()
}) // 通过匿名回调函数传入函数
btn.addEventListener('click', () => {
fn()
}) // 通过箭头回调函数传入函数
1
2
3
4
// 调用定时器, 匿名函数做为参数
setInterval(function () {
console.log('我是一个匿名回调函数...')
}, 1000)

函数表达式

将函数赋值给一个变量, 并通过变量调用匿名函数; 与具名函数的区别在于: 在书写 JavaScript 代码时, 可以先使用具名函数再声明, 但函数表达式必须先声明后使用

1
2
3
4
5
// 声明函数
let fn = function () {}
// 调用函数
fn()
// 必须先声明再调用
  • 这个区别的实质是函数声明会被提升, 而函数表达式不会被提升, 后续会介绍
  • 前面介绍的函数具有函数名, 叫做具名函数; 而有些函数没有函数名, 叫做匿名函数
  • 匿名函数不能直接调用, 必须通过变量调用 (即函数表达式)、作为回调函数传递给其他函数、立即执行 (即立即执行函数) 等方式调用

立即执行函数

通过自执行调用匿名函数, 可以避免函数名和内部变量名污染全局变量, 必须加分号 可在前可在后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法一
(function(形参){函数体})(实参); // 这个方法逻辑上好理解一点
// 方法二
(function(形参){函数体}(实参));// 多个立即执行函数间, 必须用分号隔开
// 方法三
!function(形参){函数体}(实参);
+function(形参){函数体}(实参);
~function(形参){函数体}(实参);

// 如果要重复使用, 也可以包含变量名
(function fn(形参){函数体})(实参);
// 等同于
function fn(形参){函数体}
fn(实参)

定时函数

函数名功能
setTimeout(函数, ms)延时函数; 在指定的毫秒数后执行函数, 只执行一次, 返回定时器的ID
setInterval(函数, ms)间歇函数; 每隔指定的毫秒数执行函数, 会一直执行, 返回定时器的ID
clearTimeout(定时器ID)取消 setTimeoutsetInterval 的执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 间歇执行函数
let timer = setInterval(function() {
console.log(typeof timer) // number
}, 1000)

// 间歇执行函数
function timer() {
console.log('Hello World')
}
let sayHello = setInterval(timer, 1000)
// 第一次执行前也有 1000ms 的间隔

// 取消间歇函数
setTimeout(clearInterval(timer), 5000)
setTimeout(clearInterval(sayHello), 5000)
setInterval 实现倒计时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明变量
let time = 10

// 声明函数
function countDown() {
console.log(time)
time--
if (time < 0) {
clearInterval(timer) // 清除间歇函数
time = 10
timer = setInterval(countDown, 1000)
// 重新执行间歇函数
}
}

// 间歇执行函数
let timer = setInterval(countDown, 1000)

对象

面向对象编程中的基本实体, 可以包含一系列数据和逻辑, 是一种数据类型

  • 属性: 对象中数据的存在形式, 相当于依附于对象的变量
  • 方法: 对象中代码的存在形式, 相当于依附于对象的函数
  • 属性和方法也可以在对象声明后动态添加或修改
  • 属性和方法的名称统称为, 值称为, 是应唯一的 否则后面的会覆盖前面的
  • 属性和方法名如果打破了变量的命名规则, 如使用特殊符号 -空格 等或以数字开头, 此时该名称必须用引号包裹, 如 'user-name': 'xiaoyezi'; 引用时只能用 xiaoyezi['user-name'], 不能用 xiaoyezi.user-name
  • JavaScript 中内置了一些对象, 称为内置对象, 这些对象包含许多属性和方法, 可以直接使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 声明空对象
const emptyObj = {}
// 声明对象
const obj = {
属性: 值,
方法: function(形参, ...) {
// 方法体
return 返回值
}
// 或者:
方法(形参, ...) {
// 方法体
return 返回值
}
}
// 从已有变量创建
const obj = { uname, age }
// 相当于 { uname: uname, age: age }

// 访问属性
// 访问不存在的属性会返回 undefined
obj.属性
obj['属性']
// 调用方法
// 调用不存在的方法会报错
obj.方法(实参1, 实参2, ...)
obj['方法'](实参1, 实参2, ...)

// 动态添加属性或方法
obj.新属性 = 值
obj['新属性'] = 值
obj.新方法 = function(形参, ...) {
// 方法体
return 返回值
}
obj['新方法'] = function(形参 ...) {
// 方法体
return 返回值
}

// 删除属性或方法
delete obj.属性
delete obj['属性']
delete obj.方法
delete obj['方法']
对象创建和使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 声明对象
const xiaoyezi = {
uname: '小叶子', // 建议不要使用 name, 因为 name 是 window 对象的属性
gender: '男',
printName: function(do = true) {
if(do) {
console.log(this.uname)
}
},
getGender: function() {
return this.gender
}
}

// 访问属性
let name = xiaoyezi.uname // 小叶子
console.log(xiaoyezi.gender) // 男
// 调用方法
xiaoyezi.printName() // 小叶子
let gender = xiaoyezi.getGender() // 男
// 动态添加
xiaoyezi.age = 18
xiaoyezi.printAge = function() {
console.log(this.age)
}

遍历对象

对象 非数组 内的属性和方法是无序的, 不能用 for 循环遍历, 可以用 for in 循环遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 声明对象
let xiaoyezi = {
uname: '小叶子',
gender: '男',
printName: function(do = true) {
if(do) {
console.log(this.uname)
}
},
getGender: function() {
return this.gender
}
}

// 遍历对象
for (let key in xiaoyezi) {
console.log(key, xiaoyezi[key])
// 这里不能用 xiaoyezi.key, 因为 key 是变量
// xiaoyezi[key] 相当于 xiaoyezi['xxxxx']
key === 'gender' ? break : null
// 同一般 for 循环, 可以用 break 和 continue
}
// key 的名称没有要求, 也可以用 k、i、j 等
// key 相当与循环变量, 会被依此赋值为对象的键
// key 的数据类型和键一样, 是字符串类型

不建议用 for in 遍历数组, 因为数组的键是数字, 会被隐式转换成字符串

环境对象

函数中的特殊变量 this 指向的是环境对象, 即函数所在的环境, 如果函数是在全局环境中声明的, 则 this 指向 window 对象; 通常 this 指向调用它的对象

对象描述
window浏览器窗口对象, 是 JavaScript 中内置的对象, 称为全局对象, 我们定义在全局作用域的 var 变量和函数实际上都是 window 对象的属性和方法; 详见WebAPI
document文档对象, 是 JavaScript 中内置的对象, 称为文档对象模型, 是 DOM 的入口, 详见下一章
1
2
3
4
5
6
7
8
9
10
11
// 声明函数
function sayHi() { console.log(this) }
// 声明对象
let user = { sayHi: sayHi }
let person = { sayHi: sayHi }

// 调用
sayHi() // window
window.sayHi() // 上面 sayHi() 的实质
user.sayHi() // user
person.sayHi() // person

console

方法描述
console.log()打印信息
console.info()打印信息
console.warn()打印警告
console.error()打印错误
console.table()打印表格
console.time()计时开始
console.timeEnd()计时结束
console.clear()清空控制台
console.dir()打印对象的详细信息

Math

属性描述
Math.PI圆周率
Math.E自然对数的底数
Math.LN22 的自然对数
Math.LN1010 的自然对数
Math.LOG2E以 2 为底的 e 的对数
Math.LOG10E以 10 为底的 e 的对数
Math.SQRT1_21/2 的平方根
Math.SQRT22 的平方根
方法描述
Math.abs(x)返回 x 的绝对值
Math.ceil(x)向上取整: 返回大于等于 x 的最小整数
Math.floor(x)parseInt(), 向下取整: 返回小于等于 x 的最大整数
Math.round(x)返回 x 的四舍五入值, 注意: Math.round(-1.5)-1
Math.max(x1, x2, ...)返回 x1、x2、… 中的最大值
Math.min(x1, x2, ...)返回 x1、x2、… 中的最小值
Math.pow(x, y)返回 x 的 y 次幂, 即 xy
Math.sqrt(x)返回 x 的平方根
Math.random()返回 [0, 1) 之间的随机数

详见 MDN

Math.random() 使用示例

  • 生成 [0, 100) 之间的随机整数: Math.floor(Math.random() * 100)
  • 生成 [0, 100] 之间的随机整数: Math.floor(Math.random() * 101)
  • 生成 [N, M] 之间的随机数: Math.random() * (M - N + 1) + N
  • 随机抽取数组中的元素: arr[Math.floor(Math.random() * arr.length)]

将数字保留 x 位小数: num.toFixed(x), 返回字符串类型

Date

DateJavaScript 中内置的对象, 称为日期对象, 这个对象包含许多日期属性和方法; 使用它之前需要先用 new 关键字创建一个 Date 对象实例

  • 实例化: 通过 new 关键字创建一个对象实例, 即创建一个对象
  • 任何变量都可以用 new 创建, 详见包装类型构造函数
  • 时间戳: 从 1970-01-01 00:00:00 至今的毫秒数
  • 日期字符串: yyyy-mm-dd hh:mm:ss 格式的字符串, 时分秒可省略
操作描述
const date = new Date()创建一个 Date 对象实例, 值为创建时的本地时间
const date = new Date(时间戳)创建一个 Date 对象实例, 值为指定时间戳
const date = new Date('日期字符串')创建一个 Date 对象实例, 值为指定日期
方法描述
date.getFullYear()返回年份, 如 2020
date.getMonth()返回月份, 从 0 开始, 0 表示 1
date.getDate()返回日期, 从 1 开始, 如 1 表示 1
date.getDay()返回星期几, 从 0 开始, 0 表示星期天
date.getHours()返回小时, 从 0 开始, 如 0 表示 0
date.getMinutes()返回分钟, 从 0 开始, 如 0 表示 0
date.getSeconds()返回秒数, 从 0 开始, 如 0 表示 0
date.getMilliseconds()返回毫秒数, 从 0 开始, 如 0 表示 0 毫秒
date.getTime()返回时间戳, 如 1590000000000
date.toLocaleString()返回本地时间, 如 2020/5/21 03:20:00
date.toLocalDateString()返回本地日期, 如 2020/5/21
date.toLocalTimeString()返回本地时间, 如 03:20:00

也可以写成 new Date().getFullYear() 等, 效果相同, 但是每次都会创建多余的对象实例, 不推荐

获取时间戳

1
2
3
4
5
6
7
8
9
// 上述方法
const date = new Date()
let timestamp = date.getTime()

// 直接调用 Date.now() 方法
timestamp = Date.now() // 只能获取当前时间戳

// 通过 + 运算符将实例转换为数字
timestamp = +new Date()

时间戳转换为时间

1
2
3
4
5
6
7
8
// 获取时间戳
const time = Date.now() // 毫秒

// 转换为时间
const d = parseInt(time / 1000 / 60 / 60 / 24) // 天
const h = parseInt(time / 1000 / 60 / 60 % 24) // 时
const m = parseInt(time / 1000 / 60 % 60) // 分
const s = parseInt(time / 1000 % 60) // 秒

Object

对象的构造函数, 以下是它的静态属性和方法

属性或方法描述
Object.keys(对象)返回对象的所有键, 返回值是数组
Object.values(对象)返回对象的所有值, 返回值是数组
Object.entries(对象)返回对象的所有键值对, 返回值是二维数组
[[key, value], [key, value], ...]
Object.assign(目标对象, 源对象1, 源对象2, ...)将源对象的所有可枚举属性复制到目标对象
返回目标对象(同时也会修改目标对象)
Object.freeze(对象)冻结对象, 使对象的属性和方法不可修改、删除或添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 声明对象
let obj = {
name: 'xiaoyezi',
age: 18
}
// 获取对象的所有键
let keys = Object.keys(obj) // ['name', 'age']
// 获取对象的所有值
let values = Object.values(obj) // ['xiaoyezi', 18]
// 获取对象的所有键值对
let entries = Object.entries(obj) // [['name', 'xiaoyezi'], ['age', 18]]
// 复制对象
const copy = Object.assign({}, obj)
// 给对象添加属性
Object.assign(copy, {gender: '男'})

Array 对象的静态和实例属性和方法见数组

包装类型

JavaScript 中的基本数据类型 NumberStringBoolean 等, 在底层都是用对象”包装”出来的, 具有对象的特征(有静态方法和静态属性), 称为包装类型; 一般用字面量创建包装类型, 但也可以通过 new 关键字实例化包装类型, 详见构造函数

String

属性或方法描述
String(x)x 转换为字符串类型
str.length返回字符串的长度
str.trim()去除字符串两端的空格, 返回新字符串
str.toUpperCase()将字符串转换成大写, 返回新字符串
str.toLowerCase()将字符串转换成小写, 返回新字符串
str.split('分隔符')将字符串按照分隔符分割成数组, 返回数组
str.substring(start, end)
str.slice(start, end)
按照索引截取字符串, 返回新字符串
end 可选, 返回值不包含 end 索引的字符
str.startsWith('xxx', posititon)判断字符串是否以 xxx 开头, 返回布尔值
position 可选, 表示从指定索引位置检测 xxx
str.endsWith('xxx')判断字符串是否以 xxx 结尾, 返回布尔值
str.includes('xxx', posititon)判断字符串是否包含 xxx, 返回布尔值
position 可选, 表示只从指定索引及其后方检测 xxx
str.indexOf('xxx', posititon)返回 xxx 在字符串中首次出现的索引, 没有则返回 -1
position 可选, 表示只从指定索引及其后方检测 xxx
str.match('xxx')查找并返回字符串中首次出现的 xxx, 没有则返回 null
支持正则表达式
str.replace('xxx', 'xx')将字符串中首次出现的 xxx 替换xx, 返回新字符串
支持正则表达式
str.padStart(targetLength[, padString])padString 填充 str 的前面
直到 str 的长度达到 targetLength
返回填充后的字符串
str.padEnd(targetLength[, padString])padString 填充 str 的后面
直到 str 的长度达到 targetLength
返回填充后的字符串
  • padString 默认为空格, targetLength 如果小于 str 的长度, 则返回 str 本身
  • substrsubstring 相同, 但已弃用; 字符串中的索引和数组类似, 从 0 开始; 中文字符只占一个索引和一个长度
padStart 方法
1
2
3
4
5
6
7
8
9
10
11
const arr = [1, 2, 3, 4, 5]
// 给数字补 0
for (const value of arr) console.log(value.toString().padStart(3, '0'))
// 001 002 003 004 005

// 如果用老办法
for (const value of arr) {
let str = value.toString()
while (str.length < 3) str = '0' + str
console.log(str)
}
split 方法
1
2
3
4
5
6
7
// 声明字符串
let str = 'x; y; z'
// 分割字符串
let arr = str.split('') // ['x', ';', ' ', 'y', ';', ' ', 'z']
let arr = str.split(' ') // ['x;', 'y;', 'z']
let arr = str.split(';') // ['x', ' y', ' z']
// 分隔符本身不会被包含在数组中

Number

属性或方法描述
num.toFixed(x)将数字保留 x 位小数, 返回字符串类型
num.toPrecision(x)将数字转换为指定长度的字符串, 返回字符串类型
num.toString(x)将数字转换为指定进制的字符串, 返回字符串类型
num.toExponential(x)将数字转换为科学计数法的字符串, 返回字符串类型

JSON

JavaScript Object Notation, JSON, 是一种轻量级的数据交换格式, 书写简单、一目了然, 常用于前后端数据交互; 它可以作为一个对象字符串.json 文件存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"title": "Blog", // 键和字符串必须用双引号包裹
"year": 2024, // 数值必须用十进制
"active": true, // 支持布尔值
"others": null, // 支持 null
"tags": [], // 支持数组和空数组
"category": {}, // 支持对象和空对象
"members": [
{
"name": "xiaoyezi",
"mail": "[email protected]"
},
{
"name": "xiaoyezi",
"mail": ""
}
] // 最后一个元素后面不要加逗号
// 不支持函数、正则表达式、日期对象、undefined、NaN
}
方法描述
JSON.stringify(对象, 处理方法, 缩进)将对象转换为 JSON 字符串; 缩进(空格)默认 0, 推荐 2
JSON.parse(JSON字符串)JSON 字符串转换为对象
  • 对象内的不支持的数据类型会被忽略
  • 数组内的不支持的数据类型会被转换为 null
  • 正则对象会被转换为空对象
  • JSON.stringify() 会忽略对象的不可遍历属性

处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 指定只转换对象的部分属性
JSON.stringify(对象, ['属性1', '属性2', ...])

// 对属性值进行处理
JSON.stringify(对象, function(key, value) {
// key 是属性名, value 是属性值
// 返回值为处理后的属性值
// 如果返回 undefined, 则表示忽略该属性
// 如果返回函数、正则表达式、日期对象, 则返回值转换为字符串
// 如果返回 NaN、Infinity、-Infinity, 则返回值转换为 null
// 如果返回其他值, 则表示将该值转换为字符串
})

// 只适用于一般对象, 不适用于数组
// 如果对象内存在 toJSON() 方法, 则优先使用该方法

导入 .json 文件

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
type: "get",
url: "xxx.json",
dataType: "json",
success: function(data) {
// data 就是 json 对象
},
error: function() {
alert("请求失败")
}
})

⭐进阶

事件循环

JavaScript 是一门单线程语言, 即一次只能执行一条语句, 后面的语句必须等前面的语句执行完毕后才能执行, 这种执行机制称为同步执行; 但有时我们需要执行一些耗时的操作, 如 AJAX 请求、定时器等, 这时就需要异步执行, 即不用等待前面的语句执行完毕就可以执行后面的语句

  • 异步任务: 调用后会消耗一定时间, 如 setTimeoutsetIntervalfetch 请求, 但不会阻塞后续代码的执行, 并在将来任务完成后执行回调函数
  • 同步任务都在执行引擎的主线程上执行, 形成一个执行栈 Execution Context Stack
  • 异步代码会被放入宿主环境(浏览器、Node.js 等)中进行计时或等待
  • 异步任务完成后, 宿主环境会将回调函数放入任务队列
  • 执行引擎会先执行执行栈中的同步任务
  • 同步任务执行完毕后会去任务队列中查看是否有异步任务, 如果有则将其放入执行栈中执行
  • 以上过程称为事件循环 Event Loop
  • 网页的多任务处理(计时器计时等)是由浏览器(宿主环境)完成的, 它负责向任务队列里添加任务

详见代码执行机制

正则表达式

Regular Expression, 简称 RegExp, 是一种多种语言使用的用来匹配字符串的规则, 常用于表单验证、爬虫、文本编辑器等; 正则表达式属于对象

属性或方法描述
reg.test(字符串)返回布尔值, 判断字符串是否匹配正则表达式
reg.exec(字符串)返回数组, 值依次为匹配到的字符串、索引、原字符串
字符串.match(reg)返回数组, 值依次为匹配到的字符串、索引、原字符串
字符串.replace(reg, 替换字符串)返回字符串, 将匹配到的字符串替换为指定字符串
字符串.search(reg)返回第一个匹配到的字符串的索引, 没有匹配到则返回 -1
字符串.split(reg)返回数组, 将字符串按匹配到的字符串分割成数组
1
2
3
4
5
6
7
// 创建正则对象
const reg = /xiaoyezi/
const reg = new RegExp('xiaoyezi')
// 调用方法
console.log(reg.test('xiaoyezi')) // true
console.log(reg.test('xiaoyezixxx')) // true
console.log(reg.test('yezi')) // false

语法规则

  • 普通字符: 匹配自身, 如 xiaoyezibc123
  • 元字符: 具有特殊含义的字符, 如 .*+?$^|[](){}\
  • 空白字符: 空格、制表符、换行符等, 如 \n\t\r\f\v\s
  • 非空白字符: 除空白字符以外的任意字符
元字符描述元字符描述
.匹配除换行符以外的任意字符*匹配前面的字符 0 次或多次
^xxx匹配字符串的开始+匹配前面的字符 1 次或多次
xxx$匹配字符串的结尾?匹配前面的字符 0 次或 1
^xxx$精确匹配{n}匹配前面的字符 n
[abc]匹配方括号中的任意一个字符{n,}匹配前面的字符 n 次或多次
[^abc]匹配除方括号中的任意一个字符{n,m}匹配前面的字符 n 次到 m
[a-z]匹配任意小写字母x|y匹配 xy
[A-Z]匹配任意大写字母\b匹配单词的结尾, 单词按空格分隔
[0-9]\d匹配任意数字\B匹配非单词的结尾
[^0-9]\D匹配除数字以外的任意字符\s匹配任意空白字符
[a-zA-Z0-9_]\w匹配任意字母数字下划线\S匹配任意非空白字符
[^a-zA-Z0-9_]\W匹配除字母数字下划线以外的任意字符\特殊字符通过转义字符匹配特殊字符
\n匹配换行符\t匹配制表符
\r匹配回车符\f匹配换页符
\v匹配垂直制表符(pattern)详见这里

可以在这里 测试正则表达式, 在这里 查看正则表达式的常用规则

  • 优先级: \ > [] / () > * + ? {} > ^ $ \x > |
  • 贪婪匹配: 默认情况下, 正则表达式会尽可能多的匹配字符, 如 /\d+/ 会匹配 123, 而不是 1
  • 非贪婪匹配: 在贪婪匹配的基础上, 加上 ?, 则会尽可能少的匹配字符, 如 /\d+?/ 会匹配 1, 而不是 123
  • []限定符只匹配一个字符, 应配合 {}数量符使用; 如应该用 ^\w+$ 检验一个字符串是否为纯字母数字下划线, 而非用 ^\w$
  • 在书写和阅读时, 可以把限定符和数量符的组合看作一个小块, 然后拼接成放在 ^$ 之间的完整块

修饰符

在正则表达式的末尾添加修饰符, 可以改变正则表达式的匹配方式, 如 /abc/im 中, im 就是修饰符

修饰符描述
i忽略大小写
m多行匹配; 即 ^$ 匹配每一行 以\n分界 的开头和结尾
s使 . 匹配包括换行符在内的所有字符
g全局匹配; 即匹配所有符合条件的结果, 而不是第一个; 常用于 replace() 方法

作用域

某个关键字、变量名、函数名的能够被访问到的范围; 一些变量只能在特定的范围内使用, 避免了命名冲突和逻辑混乱

  • 全局作用域: 作用于一个 .js 文件或一个 <script> 标签最外层的代码环境
  • 局部作用域: 分为函数作用域和块级作用域
  • 函数作用域: 函数内部的代码环境, 内部声明的变量只能在函数内部使用, 包括 letconstvar
  • 块级作用域: 作用于 {} 内部的代码环境, 如 iffor, 内部声明的 letconst 变量无法在外部使用; var 没有块级作用域
  • 处于全局作用域的变量叫全局变量, 处于局部作用域的变量叫局部变量
  • 作用域链: 程序会优先使用最近的局部变量, 若不存在则逐级向上查找, 直到全局作用域
  • 如果一个变量没有声明, 直接赋值, 会自动变成全局变量, 包括函数内部
  • 为了更好的性能和避免命名冲突, 应该尽量避免使用全局变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(function func() {
var a = 1
let b = 2
})();
for (let c = 3; c < 5; c++) { console.log(c) }
for (var d = 4; d < 6; d++) { console.log(d) }
(function global() {
e = 5
})();
// 注意下方结果
console.log(a) // 报错
console.log(b) // 报错
console.log(c) // 报错
console.log(d) // 能够访问
console.log(e) // 能够访问

垃圾回收机制

Garbage Collection, 简称 GC, 是 JavaScript 中的自动管理内存的机制; 当内存不再使用时, 就需要将其释放, 否则会造成内存泄漏

  • 生命周期: 分配内存 → 使用内存 → 释放内存
  • 分配内存: 当声明变量、函数、对象时, 系统会自动分配内存
  • 使用内存: 读写变量、调用函数、访问对象属性等
  • 释放内存: 当变量、函数、对象不再使用时, 垃圾回收器会自动释放内存
  • 全局变量的生命周期是永久的, 直到页面关闭
  • 局部变量的生命周期是临时的, 使用完毕后, 局部变量就会被释放
  • 内存泄漏: 分配的内存未释放或无法释放, 可能导致程序占用的内存越来越多

垃圾回收算法

  • 引用计数: 当变量被引用时, 引用计数器加 1, 当变量不再被引用时, 引用计数器减 1, 当引用计数器为 0 时, 垃圾回收器会回收该变量; 但是这种算法无法解决循环引用的问题, 已经被淘汰
  • 标记清除: 定期扫描, 将无法从全局对象或其下级对象访问到的变量标记为 待回收, 并稍后回收它们; 现代浏览器都使用这种算法
  • 栈内存: 存储基本数据类型的值和引用类型的地址, 由系统自动分配和释放
  • 堆内存: 存储引用类型的值, 由程序员分配, 由程序员或垃圾回收器释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 引用计数
let arr = [1, 2, 3]
// 此时堆内存中的 [1, 2, 3] 的引用计数为 1
// 因为栈内存中 arr 对应的地址指向了它
let num = arr[0]
// 此时堆内存中的 [1, 2, 3] 的引用计数为 2
arr = null
num = 0
// 此时堆内存中的 [1, 2, 3] 的引用计数为 0
// 被垃圾回收器回收
(function func() {
let o1 = {}
let o2 = {}
o1.name = o2
o2.name = o1
// 此时即使函数运行完毕, o1 和 o2 也不会被回收
})();

// 标记清除
let arr = [1, 2, 3]
// 此时堆内存中的 [1, 2, 3] 可以被全局对象下的 arr 访问到
let num = { number: arr[0] }
arr = null
// 此时堆内存中的 [1, 2, 3] 仍然可以被全局对象下的 num 访问到
num = null
// 此时堆内存中的 [1, 2, 3] 无法被全局对象下的任何变量访问到
// 被垃圾回收器回收
(function func() {
let o1 = {}
let o2 = {}
o1.name = o2
o2.name = o1
})();
// 运行结束后, o1 和 o2 无法被全局对象访问到
// 被垃圾回收器回收

闭包

Closure, 是指有权访问另一个函数作用域 即其外层函数 中的变量的函数, 这个函数和与其相关的变量构成闭包

  • 作用: 封闭数据, 实现数据为某个函数私有, 同时又可以让外部访问, 且不会被自动释放
  • 原理: 函数执行完毕后, 由于其内部的函数被外部函数表达式引用, 从而不会被垃圾回收器回收
  • 问题: 闭包可能会导致内存泄漏, 应适时手动释放闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function func() {
// --------闭包--------
let num = 1
function inner() {
num++
return num
}
// --------------------
return inner
}
const numInFunc = func() // 通过变量(函数表达式)调用 inner 函数
console.log(numInFunc()) // 2
console.log(numInFunc()) // 3
console.log(num) // 报错, num 为 func 函数的局部变量, 应通过闭包访问
// num 此时无法在外部被赋值修改, 实现了数据的私有化

提升

变量提升

Hoisting, 是指在代码执行前, 会先将 var 变量的声明提升到作用域的最前面, 但不会提升变量的赋值

1
2
3
console.log(a) // undefined
var a = 1
console.log(a) // 1

以上代码等价于

1
2
3
4
var a
console.log(a) // undefined
a = 1
console.log(a) // 1

letconst 声明的变量不会发生变量提升, 会直接报错

函数提升

在代码执行前, 会先将函数的声明提升到作用域的最前面, 但不会提升函数的调用

1
2
3
4
5
6
7
8
func() // 1
function func() {
console.log(1)
}
exp() // 报错
var exp = function() {
console.log(2)
}

以上代码等价于

1
2
3
4
5
6
7
8
9
function func() {
console.log(1)
}
var exp
func() // 1
exp() // 报错
exp = function() {
console.log(2)
}

函数表达式不会发生函数提升, 包括 var 声明的函数表达式, 如上述示例

解构赋值

一种快速为变量赋值的简洁语法, 本质上仍然是为变量赋值; 分为数组解构对象解构

数组解构

1
2
3
4
5
6
7
8
9
10
// 声明数组
const arr = [1, 2, 3]
// 解构赋值
const [a, b, c, d] = arr
console.log(a, b, c, d) // 1 2 3 undefined
// 相当于
const a = arr[0] // 1
const b = arr[1] // 2
const c = arr[2] // 3
const d = arr[3] // undefined
  • 变量的数量大于单元值数量时, 多余的变量将被赋值为 undefined
  • 变量的数量小于单元值数量时, 可以通过 ... 获取剩余单元值, 但只能置于最末位
  • 允许初始化变量的默认值, 且只有单元值为 undefined 时默认值才会生效
  • 不需要的单元值可以用 , , 省略
1
2
3
4
5
6
7
8
9
10
11
12
// 声明数组
const arr = [1, 2, 3, 4]
// 获取剩余单元值
const [a, b, ...c] = arr
console.log(a, b, c) // 1 2 [3, 4]
// 初始化默认值
const [a, b, c = 6, d = 7, e = 8] = arr
console.log(a, b, c, d, e) // 1 2 3 4 8
// 省略单元值
const [a, , , d] = arr
// 交换变量值
;[a, b] = [b, a] // 注意要加分号

直接使用 [a, b, ...] 数组值进行赋值或调用数组方法时, 应在前面加分号, 避免错误解析

对象解构

1
2
3
4
5
6
7
8
9
10
11
12
// 声明对象
const obj = {
name: 'xiaoyezi',
age: 18
}
// 解构赋值
const {name: myName, age, gender} = obj
console.log(myName, age, gender) // xiaoyezi 18 undefined
// 相当于
const myName = obj.name // xiaoyezi
const age = obj.age // 18
const gender = obj.gender // undefined
  • 对象中找不到与变量名一致的属性时, 变量值为 undefined
  • 允许初始化变量的默认值, 属性不存在或单元值为 undefined 时默认值才会生效
1
2
3
4
5
6
7
8
// 声明对象
const obj = {
name: 'xiaoyezi',
age: 18
}
// 初始化默认值
const {name = 'leaf', age = 12, gender = 'male'} = obj
console.log(name, age, gender) // xiaoyezi 18 male

将对象解构作为形参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明对象
const msg = {
code: 200,
data: {
name: 'xiaoyezi',
age: 18
}
}
// 将对象解构作为形参
function func({data: content}) {
// 相当于 const content = msg.data
// 也相当于 const {data: content} = msg
console.log(content) // { name: 'xiaoyezi', age: 18 }
}

多维解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 声明数组
const arr = [1, 2, [3, 4]]
// 解构赋值
const [a, b, [c, d]] = arr
const [a, b, e] = arr
// 不可以 const [a, b, e[c, d]] = arr

// 声明对象
const obj = {
name: 'xiaoyezi',
age: 18,
habits: {
eat: 'strawberry',
sleep: 'bed'
}
}
// 解构赋值
const {name, age, habits: {eat, sleep}} = obj
const {name, age, habits} = obj
// 只有下面这种写法可以将 habits 对象的值赋给变量 habits

// 嵌套使用
const mix = [
1,
2,
3,
{
name: 'xiaoyezi',
age: 18,
habits: {
eat: 'strawberry',
sleep: 'bed'
}
}
]
// 解构赋值
const [a, b, c, {name, age, habits: {eat, sleep}}] = mix

函数进阶

动态参数

arguments 是一个类数组对象, 包含了函数的所有参数, 可以通过索引访问参数, 也可以通过 length 属性获取参数的个数

1
2
3
4
5
6
7
8
// 声明函数
function func() {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i])
}
}
// 调用函数
func(1, true, 'xiaoyezi') // 1 true 'xiaoyezi'

剩余参数

语法符号 ... 用于获取函数的剩余参数, 所有多余的实参会存储到 ... 后的形参中, 返回一个真数组

1
2
3
4
5
6
7
8
// 声明函数
function func(x, y, ...z) {
for (let i = 0; i < z.length; i++) {
console.log(z[i])
}
}
// 调用函数
func(1, 2, 3, 4, 5) // 3 4 5

展开运算符

在函数外使用时, ... 用于展开数组、类数组对象、对象, 将其展开成多个参数(以逗号分隔), 不会影响原数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const arr = [1, 2, 3]
console.log(...arr) // 1 2 3
// 相当于 console.log(1, 2, 3)

// 求数组的最大值
Math.max(...arr) // 3
// 相当于 Math.max(1, 2, 3)

// 合并数组
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const arr3 = [...arr1, ...arr2] // [1, 2, 3, 4, 5, 6]

// 合并对象
const obj1 = { name: 'xiaoyezi', age: 18 }
const obj2 = { ...obj1, gender: 'male' }
const obj3 = { ...obj2 } // 浅拷贝

箭头函数

ES6 新增的一种函数声明方式, 使用 => 定义函数, 可以看作是匿名函数的简写

  • 属于函数表达式, 因此不存在函数提升
  • 只有一个参数时, 可以省略圆括号 ()
  • 函数体只有一行代码时, 可以省略花括号 {}, 并自动做为返回值被返回
  • 不能使用 arguments, 可以使用 ...
  • 不能用作构造函数
  • 不能作为对象的方法, 但能作为方法内的匿名函数, 此时箭头函数的 this 与方法的 this 指向相同
  • 箭头函数没有自己的 this, 详见MDN , 且 this 值在函数声明时便固定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 基本语法
const func = () => {
console.log('Hello World!')
}

// 只有一个参数的简写
const func = x => {
return x + 1
}

// 只有一行代码的简写
const func = x => x + 1

// 返回对象
const func = x => ({ name: x }) // 要把对象包裹在圆括号中, 否则会被解析为函数体
console.log(func('xiaoyezi')) // { name: 'xiaoyezi' }

// 阻止默认事件
document.addEventListener('click', e => e.preventDefault())

// 箭头函数作为方法内的匿名函数
const obj = {
count: 10,
doSomethingLater() {
// 该方法语法将 this 与 obj 上下文绑定。
setTimeout(() => {
// 由于箭头函数没有自己的绑定,
// 而 setTimeout(作为函数调用)本身也不创建绑定,
// 因此使用了外部方法的 obj 上下文。
this.count++
console.log(this.count)
}, 1000)
},
};

类基础

本节内容了解即可, 实际开发中会用 class 关键字定义类, 后面会详细讲解

构造函数

构造函数是一种特殊的函数, 用于创建对象和功能封装, 构造函数的名称一般以大写字母开头, 以便区分, 如 ObjectArrayDate; 如果一个函数使用 new 关键字调用, 那么这个函数就是构造函数

  • 使用 new 关键字调用函数的行为被称为实例化
  • new 关键字会创建一个新对象, 并将构造函数的 this 指向该对象, 最后返回该对象
  • 实例对象: 通过构造函数创建的对象, 即 new 关键字创建的对象
  • 实例成员: 实例对象的属性和方法, 即声明构造函数时, 函数体中用 this 添加的属性和方法
  • 不同的实例对象互不影响
  • 不使用 new 调用时, 构造函数的行为和普通函数一样
  • 实例化构造函数时没有参数时可以省略 ()
  • 构造函数内部的 return 无效, 返回值为新创建的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 声明构造函数
function Person(name, age) {
// 构造函数内部的 this 指向新创建的对象
this.myName = name
this.age = age
this.author = 'xiaoyezi'
this.sayHello = function() {
console.log(`Hello, I'm ${this.myName}`)
}
this.setNewName = function(newName) {
this.myName = newName
}
return 1 // 对构造函数无效
}
// 使用 new 关键字调用构造函数
let xiaoyezi = new Person('小叶子', 18)
let cat = new Person('小猫', 2)
let dog = new Person('小狗', 3)
// 不用 new 关键字直接调用, 视作普通函数
let res = Person('小叶子', 18)
console.log(res) // 1

静态成员

JavaScript 中, 底层函数本质上也是对象类型, 因此允许直接为函数动态添加属性或方法; 构造函数本身的属性和方法被称为静态成员

  • 静态成员方法中的 this 指向构造函数本身
  • 实例对象无法访问静态成员
  • 构造函数对象无法访问实例成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Person(name, age) {
// 实例属性
this.myName = name
this.age = age
this.author = 'xiaoyezi'
// 实例方法
this.sayHello = function() {
console.log(`Hello, I'm ${this.myName}`)
}
this.setNewName = function(newName) {
this.myName = newName
}
}

// 静态属性
Person.leg = 2
// 静态方法
Person.walk = function () {
// this 指向 Person
console.log(this.legs) // 2
console.log(this.author) // undefined
}

// 实例化
let xiaoyezi = new Person('小叶子', 18)
console.log(xiaoyezi.author) // xiaoyezi
console.log(xiaoyezi.legs) // undefined

各种数据类型的构造函数

用字面量声明各种数据类型时, 实际上是调用了构造函数, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对象
let obj = new Object({
name: 'xiaoyezi',
age: 18
})
// 数组
let arr = new Array('xiaoyezi', 18)
// 正则
let reg = new RegExp('\\d+')
// 字符串
let str = new String('xiaoyezi')
// 数字
let num = new Number(18)
// 布尔
let bool = new Boolean(true)

原型对象

Prototype, 是 JavaScript 中的一个重要概念, 每个(构造)函数都有一个 prototype 属性, 指向一个对象, 这个对象就是原型对象

  • 实例对象不能访问构造函数的静态成员, 但可以直接访问构造函数的原型对象的属性和方法
  • 实例成员用 ins.xx 访问原型对象时, 原型对象中的 this 都指向实例对象
  • 实例对象的 __proto__ 属性, 指向它的构造函数的原型对象, 即 ins.__proto__ === Func.prototype
  • 实例成员用 ins.__proto__.xx 访问原型对象时, 原型对象中的 this 都指向原型对象本身
  • 实例成员和原型对象成员重名时, 实例成员优先级高
  • 对象实例化不会多次创建原型上函数, 从而能节约内存
  • 构造函数对象访问原型对象时, 要使用 prototype 属性, 且原型对象的 this 指向原型对象本身
  • 基于以上性质, 将实例方法写在原型对象上, 即不改变使用方式, 又能节约内存, 还能让构造函数对象访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 声明构造函数
function Person(name, age) {
this.myName = name
this.age = age
}

// 为原型对象添加属性和方法
Person.prototype.sayHello = function() {console.log(`Hello, ${this.myName}`)}
Person.prototype.myName = 'xiaoyezi'

// 实例化
let xiaoyezi = new Person('小叶子', 18)

// 实例对象访问原型对象的方法, this 指向实例对象
// 添加同名实例方法, 其优先级高于原型对象的方法
xiaoyezi.sayHello() // Hello, 小叶子
xiaoyezi.sayHello = function() {console.log(`Hi, ${this.myName}`)}
xiaoyezi.sayHello() // Hi, 小叶子

Person.myName = 'leaf' // 静态属性
xiaoyezi.myName = 'leaf' // 实例属性
// 直接访问原型对象的方法, this 指向原型对象本身
Person.prototype.sayHello() // Hello, xiaoyezi
xiaoyezi.__proto__.sayHello() // Hello, xiaoyezi
概念区分
类型添加方法访问方法this 指向
构造函数 → 静态成员Func.xx = xxFunc.xx构造函数本身
实例对象 → 实例成员构造函数中 this.xx = xxins.xx实例对象
构造函数 → 原型对象Func.prototype.xx = xxins.xx
Func.prototype.xx
ins.__proto__.xx
实例对象
原型对象本身
原型对象本身
  • 实例对象和原型对象都无法访问静态成员
  • 上述的 this 可以简单概括为 this 指向调用者
  • 再次注意, 不要用箭头函数作为构造函数及其方法
constructor 属性

每个原型对象都有一个 constructor 属性, 指向它的构造函数, 即 Func.prototype.constructor === Func

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 声明构造函数
function Person(name, age) {
this.myName = name
this.age = age
}
Person.myName = 'leaf'

// 实例化
let xiaoyezi = new Person('小叶子', 18)

// 访问 constructor 属性
console.log(xiaoyezi.constructor === Person) // true

// 访问静态属性
console.log(xiaoyezi.constructor.myName) // leaf
console.log(xiaoyezi.myName) // 小叶子

// 对原型对象重新赋值时, 要记得重新赋值 constructor 属性
Person.prototype = {
constructor: Person,
cat: function() {console.log('I am a cat')}
}

原型继承

Prototype Inheritance, 是指一个对象继承另一个对象的属性和方法, JavaScript 中的继承是通过原型链实现的

原型链

每个对象都有一个 __proto__ 属性, 称为对象原型, 指向它的构造函数的原型对象, 构造函数的原型对象也有一个 __proto__ 属性, 指向它的构造函数的原型对象, 以此类推, 形成了一个原型链

  • [[Prototype]]__proto__ 相同, 但前者无法直接用 . 访问, 后者不是标准属性
  • 访问属性和方法时, 会先在实例对象中查找, 再逐级向上查找, 直到找到为止
  • 之前提到的大部分数组、字符串、数字等的方法, 都是通过原型链实现的, 如 Array.prototype.map()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 声明构造函数
function Person(name, age) {
this.myName = name
this.age = age
}

// 实例化
let xiaoyezi = new Person('小叶子', 18)

// 原型对象
console.log(xiaoyezi.__proto__ === Person.prototype) // true
// console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(xiaoyezi.__proto__.__proto__ === Object.prototype) // true
// console.log(Object.prototype.__proto__ === null) // true
console.log(xiaoyezi.__proto__.__proto__.__proto__ === null) // true
给所有数组添加新方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 给数组的构造函数的原型对象添加方法
Array.prototype.sum = function() {
return this.reduce((pre, cur) => pre + cur, 0)
}

// 声明数组
let arr = [1, 2, 3, 4, 5]
// 实质是 let arr = new Array(1, 2, 3, 4, 5)

// 调用方法
console.log(arr.sum()) // 15

// 给 Object 构造函数添加方法
Object.prototype.print = function() {
console.log(this)
}

// 也可以调用
arr.print() // [1, 2, 3, 4, 5]
用原型对象赋予公共方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 公共方法
function Animal(src) {
this.miao = function() {
console.log('miao~')
}
this.woof = function() {
console.log('woof~')
}
this.constructor = src
}

// 声明构造函数
function Cat(name, age) {
this.myName = name
this.age = age
}
function Dog(name, age) {
this.myName = name
this.age = age
}

// 赋予公共方法
Cat.prototype = new Animal(Cat)
Dog.prototype = new Animal(Dog)

// 调用
Cat.miao() // miao~
Dog.miao() // miao~

child.prototype = new Parent(), child 继承了 Parent 的属性和方法; 记得重新赋值 constructor 属性

不同内容的弹出对话框
1
2
3
<button id="btn1">问好</button>
<button id="btn2">提示</button>
<button id="btn3">睡觉</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 声明构造函数
function Box(title, content) {
this.title = title || '标题' // 对话框标题
this.content = content || '内容' // 对话框内容
}
Box.prototype.open = function() { // 再再再次提醒, 方法不要用箭头函数
// 判断是否已经存在对话框, 存在则删除
const current = document.querySelector('[data-id="notice"]')
current && current.remove()
// 创建对话框
let notice = document.createElement('div')
notice.dataset.id = 'notice'
// 添加内容
notice.innerHTML = `
<h2>${this.title}</h2>
<p>${this.content}</p>
<button>关闭</button>
`
// 添加样式
notice.style = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 120px;
padding: 20px;
border: 1px solid #000;
background-color: #fff;
`
// 添加到页面
document.body.appendChild(notice)
// 添加关闭对话框事件
notice.querySelector('button').onclick = () => document.body.removeChild(notice)
}


// 实例化后立即调用
document.querySelector('#btn1').onclick = () => new Box('问好', '你好').open()
document.querySelector('#btn2').onclick = function() { new Box('提示', '这是一个提示').open() }

// 先实例化再调用
const box = new Box('睡觉', '晚安')
// 不用匿名函数包裹的话, this 不指向 box, 需要用 bind
// 如果不用 bind, 标题和内容都会是 undefined
document.querySelector('#btn3').onclick = box.open.bind(box)

Function 实例的 bind(thisTarget, arg1, arg2, ...) 方法创建一个新函数, 当调用该新函数时, 它会调用原始函数并将其 this 关键字设置为给定的值, 其第二个参数开始为新函数的实参

instanceof 运算符

instanceof 运算符用于检测”实例对象的原型链上”是否含有”某个构造函数的原型对象”; 语法为 ins instanceof Func, 返回布尔值

1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明构造函数
function Person(name, age) {
this.myName = name
this.age = age
}

// 实例化
let xiaoyezi = new Person('小叶子', 18)

// 检测原型链
console.log(xiaoyezi instanceof Person) // true
console.log(xiaoyezi instanceof Object) // true
console.log(Person.prototype instanceof Object) // true

getter / setter

gettersetter 是对象的一种属性(用属性的方式获取, 用函数的方式定义), 用于获取设置对象的属性值

  • getter 用于获取对象的属性值, 可以在获取属性值时执行一些逻辑, 使用 get 关键字定义
  • setter 用于设置对象的属性值, 可以在设置属性值时执行一些逻辑, 使用 set 关键字定义
  • gettersetter 不能与属性名相同, 但它们两者可以同名
  • 在定义时, getter 没有形参, setter 有一个形参, 即用户赋值操作赋的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义
const obj = {
logs: ['log1', 'log2', 'log3'],
get latestLog() {
return this.logs[this.logs.length - 1]
},
set latestLog(log) {
this.logs.push(log)
}
}
// 调用
console.log(obj.latestLog) // log3
obj.latestLog = 'log4'
console.log(obj.latestLog) // log4

删除和添加

gettersetter 可以通过 delete 关键字删除、通过 Object.defineProperty 方法添加

1
2
3
4
5
6
7
8
9
10
11
12
13
// 删除
delete obj.latestLog
console.log(obj.latestLog) // undefined
// 添加
Object.defineProperty(obj, 'latestLog', {
get() {
return this.logs[this.logs.length - 1]
},
set(log) {
this.logs.push(log)
}
})
console.log(obj.latestLog) // log4

this 指向

this 是一个关键字, 用于指向当前执行上下文的对象; 不同的场合, this 可能有意想不到的指向

默认指向

场合this 指向
在全局作用域声明的普通函数window, 严格模式下为 undefined
对象中用普通函数声明的方法对象本身
window 下的各种方法, 如定时器window
普通函数声明的事件处理函数触发事件的元素
对象方法内的箭头函数同对象方法内的 this, 即对象本身
箭头函数声明的事件处理函数window
箭头函数声明的 prototype 方法window
  • 普通函数的 this 可以理解为谁调用就指向谁
  • 箭头函数的 this 指向定义时的上下文, 不会改变, 不受调用者影响
  • 事实上箭头函数中并不存在 this, 其访问的 this 是其所在作用域的 this 变量
  • 箭头函数不能用作对象的方法和构造函数
  • 内层函数(一般是箭头函数)不存在 this 时, 会向上级作用域查找, 直到找到为止

指定指向

方法作用
func.call(thisTarget, arg1, arg2, ...)调用函数, 指定 this 指向 thisTarget, 并传入参数
func.apply(thisTarget, arr)调用函数, 指定 this 指向 thisTarget, 并传入数组 [arg1, arg2, ...]
func.bind(thisTarget)创建新函数, 指定新函数的 this 指向 thisTarget

注意: 上述方法仅适用于普通函数, 箭头函数的 this 无法改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 声明函数
const arrowSayHi = () => `Hello, I'm ${this.myName}. I'm ${this.age} years old.`
const normalSayHi = function() { return `Hello, I'm ${this.myName}. I'm ${this.age} years old.` }

// 声明对象
const me = { myName: 'xiaoyezi', age: 18 }
const cat = { myName: '小猫', age: 2 }
const dog = { myName: '小狗', age: 3 }
window.myName = 'leaf'
window.age = 12

// 调用函数
console.log(arrowSayHi()) // Hello, I'm leaf. I'm 12 years old.
console.log(arrowSayHi.call(me)) // Hello, I'm leaf. I'm 12 years old.
console.log(arrowSayHi.apply(cat)) // Hello, I'm leaf. I'm 12 years old.
console.log(arrowSayHi.bind(dog)()) // Hello, I'm leaf. I'm 12 years old.

console.log(normalSayHi()) // Hello, I'm leaf. I'm 12 years old.
console.log(normalSayHi.call(me)) // Hello, I'm xiaoyezi. I'm 18 years old.
console.log(normalSayHi.apply(cat)) // Hello, I'm 小猫. I'm 2 years old.
console.log(normalSayHi.bind(dog)()) // Hello, I'm 小狗. I'm 3 years old.
事件监听中的定时器使用 bind()示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// <button id="btn">五秒内只可点击一次</button>

// 获取元素
const btn = document.querySelector('#btn')

// 添加事件监听
btn.addEventListener('click', function() {
// 禁用按钮
this.disabled = true // this 指向调用者 btn
// 五秒后解禁
setTimeout(function() {
this.disabled = false // 回调函数内的 this 原本指向调用者 window
}.bind(this), 5000) // bind() 中的 this 指向事件监听的调用者 btn
// 通过 bind() 方法将回调函数内的 this 指向事件监听的调用者 btn

// 也可以用箭头函数, 箭头函数的 this 指向定义时的上下文, 不会改变
setTimeout(() => this.disabled = false, 6000)
})

深拷贝和浅拷贝

  • 深拷贝和浅拷贝是针对引用类型的数据, 因为简单类型的数据赋值时, 直接将数据的值赋给了新的变量
  • 直接赋值: 只将对象的地址赋值给了新的变量, 两者指向同一块内存地址, 修改其中一个会影响另一个
  • 浅拷贝: 拷贝了对象的第一层属性, 但如果属性值是引用类型, 拷贝的是地址, 修改其中一个会影响另一个
  • 深拷贝: 拷贝了对象的所有属性, 包括引用类型, 两者完全互不影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 声明数组和对象
const arr = [1, 2, [3, 4, [5, 6]]]
const obj = {name: 'xiaoyezi', age: 18, habits: {eat: 'strawberry', sleep: 'bed'}}

// 浅拷贝
const newArr = arr.slice()
const newArr = arr.concat()
const newArr = [...arr]
const newObj = Object.assign({}, obj)
const newObj = {...obj}

// 利用 JSON 深拷贝
const newArr = JSON.parse(JSON.stringify(arr))

// 利用 lodash 库深拷贝
// <script src="
// https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// "></script>
const newArr = _.cloneDeep(arr)
const newObj = _.cloneDeep(obj)

// 利用递归深拷贝
function deepClone(obj) {
// 如果不是引用类型, 直接返回
if (typeof obj !== 'object' || obj === null) return obj
// 判断是数组还是对象, 创建对应的空数组或对象
let result = Array.isArray(obj) ? [] : {}
// 遍历对象
for (let key in obj) {
// 递归调用
result[key] = deepClone(obj[key])
// 若属性值不是引用类型, 直接赋值
// 若属性值是引用类型, 递归调用, 直到属性值不是引用类型
}
return result
}
const newArr = deepClone(arr)
const newObj = deepClone(obj)
利用递归和 setTimeout 模拟 setInterval
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function mySetInterval(fn, time) {
// 声明一个闭包变量
let timer = null
// 声明执行函数
function interval() {
fn()
// 递归调用自身
timer = setTimeout(interval, time)
}
// 执行执行函数
interval()
// 返回一个对象, 用于清除定时器
return {
clear: function() {
clearTimeout(timer)
}
}
}

递归函数: 函数内部调用自身的函数, 称为递归函数, 递归函数必须有一个结束条件, 否则会陷入死循环

异常处理

throw

  • throw 语句用于抛出一个用户自定义的异常, 后面可以跟任何值
  • 配合 Error 构造函数使用, 可以创建一个新的错误对象, 展示更详细的错误信息
  • throw 语句会立即终止函数的执行, 后面的代码不会执行
1
2
3
4
5
6
7
8
9
10
11
12
// 声明函数
function divide(a, b) {
if (b === 0) {
throw '除数不能为 0'
} else if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('参数类型错误')
}
return a / b
}

// 调用函数
divide(1, 0) // Uncaught 除数不能为 0

try catch finally

  • trycatchfinally 语句用于处理异常
  • try 语句用于测试代码块, 如果有异常则跳转到 catch 语句
  • catch 语句用于处理异常, 可以获取异常信息
  • finally 语句用于无论是否有异常都会执行的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明函数
function func(x) {
try {
// ...
}
catch (e) { // 如果有异常, 执行 catch 语句, 其中形参是异常对象
console.log(e.message)
return '出现异常' // 不写 return 的话, 程序会继续执行
}
finally { // 可以省略
console.log('测试完成') // 无论是否有异常, 都会执行
}
return xxx // 如果无异常, 或 catch 和 finally 中无 return 语句, 执行该语句
}

debugger

  • debugger 语句用于在代码中设置断点, 调试代码
  • 在调试器打开的情况下, 代码执行到 debugger 语句时会自动暂停, 可以查看变量的值、执行栈等信息
  • 在调试器未打开的情况下, debugger 语句不会产生任何效果
1
2
3
4
5
6
7
8
9
10
11
// 声明函数
function func() {
for (let i = 0; i < x; i++) {
if (i === 3) {
debugger // 设置断点
}
}
}

// 调用函数
func() // 如果调试器打开, 会在 i === 3 时暂停

Error 对象

Error 对象是 JavaScript 中的一个内置对象, 表示某些特定的错误信息; 创建一个 Error 对象时, 可以省略 new 关键字

构造函数描述
Error([message[, options]])创建一个新的 Error 对象
RangeError([message[, options]])表示数值或参数超出其有效范围
ReferenceError([message[, options]])表示非法或不能识别的引用值
SyntaxError([message[, options]])表示解析代码时发生的语法错误
TypeError([message[, options]])表示变量或参数不是有效类型
URIError([message[, options]])表示 encodeURI()decodeURI() 函数的参数无效
实例属性描述
error.message错误消息, 即参数中的 message
error.name错误名称, 即构造函数的名称
error.cause错误原因, 通常是另一个错误
对于用户创建的错误, 该属性为 options.cause
  • options.cause 一般用于将捕获到的错误重新包装, 并附上一些信息
  • 如果不传入 options 参数, 则 Error 对象没有 cause 属性
  • 如果第二个参数不是对象, 也不会被添加到 cause 属性中 (用于对一些非标准的写法进行容错处理)

防抖和节流

防抖 / Debounce

单位时间内, 如果频繁触发同一事件, 只执行最后一次; 即触发后等待一段时间再执行, 如果在这段时间内再次触发, 则重新计时

  • 搜索框输入时, 只有停止打字后才会触发搜索
  • 用户输入密码时, 只有停止输入后才会验证密码
  • 窗口大小改变时, 只有停止改变后才会触发重新计算布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 鼠标在盒子上移动时, 使盒子内的数字加一
// 通过防抖, 实现只有停止移动 500ms 后才会触发
// <div id="box"></div>

// 获取元素
const box = document.querySelector('#box')

// 事件处理函数
function addOne() {
let i = 0
box.innerHTML = i++
}

// 通过 lodash 库实现
box.addEventListener('mousemove', _.debounce(addOne, 500))

// 手动实现
function debounce(fn, delay) {
// 声明一个闭包变量
let timer
// 由于 debounce() 是作为事件处理函数, 所以应返回一个函数
return function() {
// 由于形成了闭包, 所以只有这个返回的函数能访问到 timer
// 清除定时器
timer && clearTimeout(timer)
// 重新设置定时器
timer = setTimeout(fn, delay)
}
}
box.addEventListener('mousemove', debounce(addOne, 500))

节流 / Throttle

单位时间内, 只触发一次事件; 即触发后立即执行, 然后在规定时间内不再触发

  • 多次点赞时, 每隔一段时间才真正发送请求
  • 上述防抖的案例, 改为节流则是每隔一段时间才会触发
  • 针对高频事件, 如 mousemovescrollresize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 将上方的例子改为触发一次后, 等待 500ms 才能再次触发

// 通过 lodash 库实现
box.addEventListener('mousemove', _.throttle(addOne, 500))

// 手动实现
function throttle(fn, delay) {
// 声明一个闭包变量
let timer
// 返回事件处理函数
return function() {
// 如果定时器不存在, 立即执行
if (!timer) {
fn()
// 下方不用 clearTimeout() 是因为无法在定时器内部清除定时器
timer = setTimeout(() => timer = 0, delay)
}
}
}
通过节流实现视频从上次播放位置开始
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video</title>
<style>
video {
width: 100%;
height: 100%;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
</head>
<body>
<video src="https://www.runoob.com/try/demo_source/movie.mp4" controls></video>
<script>
// 获取元素
const video = document.querySelector('video')
// 事件处理函数
function saveTime() {
localStorage.setItem('videoTime', video.currentTime)
}
// 通过节流, 正在播放时每隔 1s 保存一次播放位置
video.addEventListener('timeupdate', _.throttle(saveTime, 1000))
// 获取上次播放位置, 并设置
video.currentTime = localStorage.getItem('videoTime') || 0
</script>
</body>
</html>

模块化

通过 exportimport 关键字, 可以将代码分割成多个模块, 使代码更加清晰、易于维护

export

  • export ... 关键字用于在一个独立的 .js 文件中, 导出变量、函数、类等供外部使用
  • export default ... 关键字用于声明导出模块的默认成员, 一个模块只能有一个默认成员
  • as 关键字用于重命名导出的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
// 导出变量
export default const name = 'xiaoyezi'
export const age = 18

// 导出函数
export function sayHi() {
console.log(`Hello, I'm ${name}. I'm ${age} years old.`)
}

// 也可以前面不写 export, 在后面批量导出
// 此时还可以用 as 关键字重命名
export default name // 或 export { name as default }
export { age as myAge, sayHi }

import

  • 必须在 <script> 标签中声明 type="module" 才能使用 import
  • 需要引入的模块无需在 HTML 文件中引入, 只需在 JavaScript 文件中使用 import 关键字即可
  • import ... from ... 关键字用于导入模块的变量、函数、类等
  • import 关键字后面的花括号 {} 用于导入模块的指定成员, 不加时导入模块的默认成员
  • as 关键字用于重命名导入的成员
  • * as xxx 用于导入模块的所有成员, xxx 是一个对象, 包含了模块的所有成员
1
<script type="module" src="module.js"></script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 导入模块的默认成员
import xxx from './module.js'
// 不加花括号, 则直接导入模块的默认成员
// 导入的名称可以自定义
// 如果还要导入其他成员:
import xxx, { age, sayHi } from './module.js'

// 导入模块的指定成员
import { name as myName, age as myAge, sayHi} from './module.js'

// 导入模块的所有成员
import * as module from './module.js'
// module.age、module.sayHi
// 默认成员只能用 module.default

⭐网络

JavaScript 中可以通过 FetchXMLHttpRequest 发送网络请求的, FetchXMLHttpRequest 的替代品, 使用更加简单

HTTP

HTTPHyperText Transfer Protocol 的缩写, 即超文本传输协议, 是当前互联网上应用最为广泛的一种网络传输协议, 最新版本为 HTTP/3

版本年份特点
HTTP/0.91991只有一个命令 GET, 并且只能请求 HTML 格式的文档
HTTP/1.01996增加了很多命令, 如 POSTHEADPUTDELETE
HTTP/1.11999增加持久连接、管道机制、增加缓存处理、增加 Host
HTTP/22015所有数据以二进制传输, 多路复用, 头部压缩, 服务器推送等
HTTP/32018基于 QUIC 协议, 解决 TCP 的队头阻塞问题

由于 HTTP/3QUIC 协议是基于 UDP 的, 所以在国内不太受待见(虽然速度快, 但成本高且较难审查), 经常被限速甚至封锁(点名批评北师大校园网)

URL

URLUniform Resource Locator 的缩写, 指的是统一资源定位符, 用于定位互联网上的资源, 包括文件、目录、程序等; 即我们通常所说的网址

http://www.example.com:8080/path/to/file?query=string#hash 为例, URL 由以下几部分组成

内容说明示例
协议资源的访问协议http://https://ftp://
主机名资源所在的域名IPwww.example.com127.0.0.1
端口资源所在的端口号, 不写时使用默认端口
默认 http 端口为 80, https 端口为 443
8080443
路径资源所在的路径/path/to/file/
查询字符串资源的查询参数, 用 ? 开头, 多个参数用 & 连接query=string&name=leaf
哈希值资源的锚点, 用 # 开头#hash

IP 地址

IPInternet Protocol 的缩写, 指的是互联网协议, 用于唯一标识互联网上的设备; IP 地址分为 IPv4IPv6 两种, 前者是 32 位的二进制数, 后者是 128 位的二进制数

  • IPv4 地址由 48 位的二进制数组成, 每个数组用 0-255 的十进制数表示, 如 192.168.0.1
  • IPv6 地址由 816 位的二进制数组成, 每个数组用 0-ffff 的十六进制数表示, 如 2001:0db8:85a3:0000:0000:8a2e:0370:7334
  • IPv4 已经用尽, 现在一般是一个 IPv4 地址对应对应多个实际使用的用户(NAT 技术)
  • NAT 技术就是让多个私有地址共用一个公网地址, 通过端口号区分不同的用户
  • 192.168.x.x172.16.0.0-172.31.255.25510.0.0.0-10.255.255.255 是私有地址, 只能在局域网内使用(家庭宽带一般是第一个、校园网一般是第二个)
  • 127.0.0.1-127.255.255.254 是本地回环地址, 用于访问本机; 而 localhost 就是指向 127.0.0.1 的域名
  • 每一个域名都对应一个 IP 地址, 而 DNS 服务器就是用来记录这个对应关系的

端口

一个主机往往不只有一个服务, 因此为了区分不同的服务, 为它们分配了不同的端口号, 范围是 0-65535

  • 0-1023系统保留端口, 一般用于系统服务
  • 1024-49151注册端口, 一般用于用户进程
  • 49152-65535动态和/或私有端口, 一般用于客户端程序
  • 80HTTP 协议的默认端口
  • 443HTTPS 协议的默认端口
  • 21FTP 协议的默认端口
  • 22SSH 协议的默认端口

请求

请求方法

访问 URL 时, 我们的请求方法决定了服务器对资源的操作; 直接输入网址访问某个网站时, 浏览器默认使用的是 GET 方法

请求方法说明
GET请求指定的页面信息, 并返回实体主体, 数据一般附在 URL 后面
POST向指定资源提交数据进行处理请求, 数据一般包含在请求体中
PUT从客户端向服务器传送数据, 用其取代指定文档的内容, 数据包含在请求体中
DELETE请求服务器删除指定的页面
HEAD类似于 GET 请求, 只不过返回的响应中没有具体的内容, 用于获取报头
PATCH对资源进行部分修改
  • 实际开发中, GETPOST 常常也被用来删除和修改资源
  • GET 请求大小有限制, 一般不超过 2KB, POST 请求大小没有限制

请求报文

发送请求时, 我们需要按照 HTTPHTTPS 协议的规范, 向服务器发送一个请求报文, 包括请求行、请求头和请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /path/to/file?query=string#hash HTTP/1.1     ----- 请求行
Host: www.example.com ----- 请求头
Connection: keep-alive
Accept: text/html
User-Agent: Mozilla/5.0
Content-Type: application/json ----- 这句表明请求体的数据类型
Content-Length: 13
Origin: http://127.0.0.1:5500
Referer: http://127.0.0.1:5500/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9 ----- 请求头结束
----- 空行
{"name": "xiaoyezi","age": 18} ----- 请求体

调试时, 可以在浏览器的开发者工具中的 Network 选项卡中查看请求报文和响应报文

响应

响应报文

服务器接收到请求报文后, 会返回一个响应报文, 包括状态行、响应头和响应体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP/1.1 400 Bad Request                        ----- 状态行
Server: nginx ----- 响应头
Date: Fri, 01 Jan 2021 00:00:00 GMT
Content-Type: application/json ----- 这句表明响应体的数据类型
Content-Length: 13
Connection: keep-alive
Vary: Origin
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-download-options: noopen
x-readtime: 16 ----- 响应头结束
----- 空行
{"error": "Bad Request"} ----- 响应体

HTTP 状态码

HTTP 协议的响应报文中, 状态行的第二个字段是状态码, 用于表示服务器对请求的处理结果

状态码说明状态码说明
1xx信息性状态码2xx成功状态码
3xx重定向状态码4xx客户端错误状态码
5xx服务器错误状态码100继续
200成功301永久重定向
302临时重定向304未修改
400错误请求401未授权
403禁止404未找到
500服务器错误503服务不可用

接口

APIApplication Programming Interface 的缩写, 指的是应用程序接口, 用于不同软件系统之间的通信; 在网页开发中, 通常指的是接口文档, 用于说明如何与服务器进行通信; 可以在这里 获取一些练习用的接口文档及示例代码

  • encodeURI() 用于将中文等特殊字符转换为 URL 编码, 如 小叶子 转换后为 %E5%B0%8F%E5%8F%B6%E5%AD%90
  • decodeURI() 用于将 URL 编码转换为中文等特殊字符, 如 %E5%B0%8F%E5%8F%B6%E5%AD%90 转换后为 小叶子
  • URLSearchParams 对象会自动处理 URL 查询参数, 因此通过 URLSearchParamsURL 对象访问查询参数时, 不需要手动处理编码和解码
  • 但在使用 fetch 发送请求时, 需要手动处理编码

Token

Token 是一种身份验证的方式, 常用于登录和权限验证; Token 通常是一个长字符串, 由服务器生成并返回给客户端, 包含了用户的一些信息, 如用户名、权限等; Token 认证失败时, 服务器会返回 401 状态码

XMLHttpRequest

XMLHttpRequest 对象, 简称 XHR, 用于在后台与服务器交换数据, 可以在不重新加载页面的情况下更新页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest()

// 设置请求方法和请求地址
xhr.open('GET', 'https://xxx.xxx/xxx')

// 监听响应结果
xhr.addEventListener('loadend', () => { // 加载完成事件
// 如果请求成功
if (xhr.status === 200) {
// 获取响应结果
console.log(xhr.response)
} else {
// 如果请求失败
console.log('请求失败')
}
})

// 发送请求
xhr.send()
属性或方法说明
readyState请求的状态, 0 未初始化, 1 已打开, 2 已发送, 3 已接收, 4 完成
status响应的状态码, 如 200 成功, 404 未找到, 500 服务器错误
response响应的数据
open(method, url, async, user, password)初始化请求, 后三个参数可省略
send(body)发送请求, body 为请求体, 仅在 POSTPUT 中使用
setRequestHeader(name, value)设置请求头
getResponseHeader(name)获取响应头
getAllResponseHeaders()获取所有响应头
abort()取消请求
addEventListener(event, callback)添加事件监听

事件

事件说明
loadstart请求开始
progress请求过程中
abort请求被取消
error请求失败
load请求成功
loadend请求完成
timeout请求超时
readystatechange请求状态改变
发送 GET 请求获取省份列表
1
2
3
4
5
6
7
8
9
10
11
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest()

// 设置请求方法和请求地址
xhr.open('GET', 'http://hmajax.itheima.net/api/province')

// 监听响应结果
xhr.addEventListener('load', () => console.log(JSON.parse(xhr.response)))

// 发送请求
xhr.send()
发送 POST 请求登陆账号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<style>
input {
display: block;
margin: 10px 0;
}
</style>
</head>
<body>
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button id="login">登陆</button>
<script>
// 获取元素
const username = document.querySelector('#username')
const password = document.querySelector('#password')
const login = document.querySelector('#login')

// 添加事件监听
login.onclick = function() {
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest()
// 设置请求方法和请求地址
xhr.open('POST', 'http://hmajax.itheima.net/api/login')
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json')
// 监听响应结果
xhr.addEventListener('loadend', () => {
// 如果请求成功
if (xhr.status === 200) {
// 提示登陆成功
alert(JSON.parse(xhr.response).message)
// 禁用输入框和按钮
username.disabled = true
password.disabled = true
login.disabled = true
} else {
// 如果请求失败
alert(JSON.parse(xhr.response).message)
// 清空输入框
username.value = ''
password.value = ''
}
})
// 发送请求
xhr.send(JSON.stringify({
username: username.value,
password: password.value
}))
}
</script>
</body>
</html>

URL 对象

URL 对象用于处理 URL 地址; 实例化时可以传入一个 URL 地址, 也可以传入一个基地址和一个相对地址, 如 new URL(https://${req.headers.origin}${req.url}) (Node.js 中)

属性或方法说明
new URL()实例化 URL 对象
url.hashURL 的哈希值, 如 #hash
url.hostURL 的主机名和端口号 (如果有), 如 www.example.com:8080
url.hostnameURL 的主机名, 如 www.example.com
url.href
url.toString()
url.toJSON()
URL 的完整地址
url.origin只读, URL 的协议、主机名和端口号, 如 http://www.example.com:8080
url.pathnameURL 的路径, 如 /path/to/file
url.portURL 的端口号, 如 8080 (字符串)
url.protocolURL 的协议, 如 https:
url.searchURL 的查询参数, 如 ?query=string
url.searchParams只读, URLSearchParams 对象
URL.createObjectURL(blob)创建一个 Blob 对象的 URL (用于图片、音频等文件)
URL.revokeObjectURL(url)释放一个 Blob 对象的 URL

URL(xxx).searchParamsURLSearchParams(xxx) 等效, 但传入的参数不同

URLSearchParams 对象

URLSearchParams 对象用于处理 URL 查询参数, 可以用于设置查询参数; 实例化时, 可以传入一个对象, 也可以传入一个查询字符串不是传入完整 URL

属性或方法说明
get(‘xxx’)获取指定名称的查询参数
set(‘xxx’, ‘xxx’)设置指定名称的查询参数
append(‘xxx’, ‘xxx’)添加指定名称的查询参数
delete(‘xxx’)删除指定名称的查询参数
toString()返回查询参数的字符串形式, 如 name=xiaoyezi&age=18
forEach()遍历查询参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取查询参数
const getParams = new URLSearchParams(location.search)
console.log(getParams.get('name')) // xiaoyezi
getParams.delete('name')

// 设置查询参数
const setParams = new URLSearchParams({
name: 'xiaoyezi',
age: 18
})
setParams.append('gender', 'male')
// 发送请求
const xhr = new XMLHttpRequest()
xhr.open('GET', `https://xxx.xxx/xxx?${setParams.toString()}`)
xhr.send()

AJAX

本段内容了解即可, 建议直接学习 fetch

AJAXAsynchronous JavaScript and XML 的缩写, 指的是通过 JavaScript 发送异步请求, 获取数据并更新页面, 而不用刷新整个页面; 本质上还是通过 XMLHttpRequest 对象发送请求

axios

建议直接学习 fetch

axios 是一个基于 PromiseHTTP 客户端, 可以实现 AJAX 请求, 可用于浏览器和 Node.js, 支持 Promise API, 可以拦截请求和响应, 转换请求和响应数据, 取消请求, 自动转换 JSON 数据, 客户端支持防止 CSRF

promise 对象: 用于表示一个异步操作的最终完成或失败, 以及其结果值; 最显著特点是具有 then() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 引入 axios 库
// <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

// 发送请求
axios({
method: 'get', // 请求方法, 如 get、post, 不写默认为 get
url: 'https://xxx.xxx/xxx', // 请求地址
params: { // 查询参数, 仅在 get 中使用
name: 'xiaoyezi',
age: 18
},
data: { // 请求体, 仅在 post 中使用
name: 'xiaoyezi',
age: 18
}
}).then(res => {
// 请求成功后, 结果会被传入 then() 方法
console.log(res)
// 其中的 data 属性是请求得到的数据
console.log(res.data)
}).catch(err => {
// 请求失败后, 错误会被传入 catch() 方法
console.log(err)
// 其中的 response.data 是请求得到的错误信息
console.log(err.response.data)
})
发送 GET 请求获取省份列表
1
2
3
4
5
6
7
8
9
// 发送请求
axios({
method: 'get',
url: 'http://hmajax.itheima.net/api/province'
}).then(res => {
res.data.list.forEach(item => console.log(item.name))
}).catch(err => {
console.log(err)
})
发送 GET 请求获取城市列表
1
2
3
4
5
6
7
8
9
10
11
12
// 发送请求
axios({
method: 'get',
url: 'http://hmajax.itheima.net/api/city',
params: {
pname: '广东省'
}
}).then(res => {
res.data.list.forEach(item => console.log(item.name))
}).catch(err => {
console.log(err)
})
发送 POST 请求登陆账号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<style>
input {
display: block;
margin: 10px 0;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button id="login">登陆</button>
<script>
// 获取元素
const username = document.querySelector('#username')
const password = document.querySelector('#password')
const login = document.querySelector('#login')

// 添加事件监听
login.onclick = function() {
// 判断输入是否合法
if (!username.value || !password.value) {
alert('用户名和密码不能为空')
return
} else if (username.value.length < 6) {
alert('用户名长度不能小于 6 位')
return
} else if (password.value.length < 6) {
alert('密码长度不能小于 6 位')
return
} else {
// 发送请求
axios({
method: 'post',
url: 'http://hmajax.itheima.net/api/login',
data: {
username: username.value,
password: password.value
}
}).then(res => {
// 如果成功, 提示登陆成功
alert(res.data.message)
// 禁用输入框和按钮
username.disabled = true
password.disabled = true
login.disabled = true
}).catch(err => {
// 如果失败, 根据错误类型提示错误信息
if (err.response.status === 0) { // 网络错误
alert('网络错误')
} else {
alert(err.response.data.message) // 其他错误
}
// 清空输入框
username.value = ''
password.value = ''
})
}
}
</script>
</body>
</html>
发送 PUT 请求修改图书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取表单数据
const form = document.querySelector('form')
const data = serialize(form, {hash: true, empty: true})

// 发送请求
axios({
method: 'put',
url: `http://hmajax.itheima.net/api/book/${data.id}`,
data: {
name: data.name,
author: data.author,
publisher: data.publisher
}
}).then(res => {
// 修改成功后, 重新渲染
render()
}).catch(err => {
// 修改失败后, 提示错误信息
alert(err.response.data.message)
})
发送 DELETE 请求删除图书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
document.querySelector('.list').onclick = e => {
// 判断是否点击了删除按钮
if (e.target.tagName === 'BUTTON' && e.target.classList.contains('delete')) {
// 获取图书的 id
const id = e.target.dataset.id
// 发送请求
axios({
method: 'delete',
url: `http://hmajax.itheima.net/api/book/${id}`
}).then(res => {
// 删除成功后, 重新渲染
render()
}).catch(err => {
// 删除失败后, 提示错误信息
alert(err.response.data.message)
})
}
}

form-seriaize

form-seriaize 插件可以快速获取表单数据, 而无需手动遍历表单元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 引入 form-seriaize 插件
// <script src="xxx"></script>

// 示例代码
// <form>
// <input type="text" name="name" value="xiaoyezi">
// <input type="password" name="password" value="123456">
// </form>

// 获取表单
const form = document.querySelector('form')

// 获取表单数据
const data = serialize(form, {hash: true, empty: true})
// hash: true 表示返回一个对象
// hash: false 表示返回一个查询字符串(不含问号)
// empty: 是否获取空值

// 返回一个对象, 键为表单元素的 name 属性, 值为表单元素的 value 属性
console.log(data) // {name: "xiao", password: "123456"}

FormData

FormData 对象用于将表单数据发送到服务器, 可以用于上传文件, 也可以用于发送 XMLHttpRequest 对象; 使用时, 请求头中的 Content-Type 应为 multipart/form-dataaxios 会自动设置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取表单
const input = document.querySelector('input[type="file"]')

// 添加事件监听
input.addEventListener('change', e => {
// 创建 FormData 对象
const formData = new FormData()
// 添加文件
formData.append('img', e.target.files[0]) // files 属性包含了所有的文件, 是一个类数组对象
// 发送请求
axios({
method: 'post',
url: 'http://hmajax.itheima.net/api/uploadimg',
data: formData
}).then(res => {
// 上传成功后, 打开图片
window.open(res.data.url)
}).catch(err => {
// 上传失败后, 提示错误信息
alert(err.response.data.message)
})
})

Promise

Promise 对象用于表示一个异步操作的最终完成或失败, 以及其结果值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 Promise 对象
const promise = new Promise((resolve, reject) => {
// 异步操作
// 两个参数分别是成功时的回调和失败时的回调
// 实例化后, 内部的代码会立即开始执行
if (/* 异步操作成功 */) {
resolve(xxx) // 调用 resolve() 方法, 结束异步操作
// resolve() 方法的参数会传入 then() 方法
} else {
reject(xxx) // 调用 reject() 方法, 结束异步操作
// reject() 方法的参数会传入 catch() 方法
// 通常传入一个 Error 对象
}
}).then(res => {
// 如果成功
console.log(res)
}).catch(err => {
// 如果失败
console.log(err)
})
  • 一个 Promise 必然出于以下三种状态之一
    • 待定 Pending: 既不是成功也不是失败状态, 表示异步操作尚未完成, 是 Promise 对象的初始状态
    • 已兑现 Fulfilled: 表示异步操作成功完成, 通过 resolve() 方法将状态从 Pending 改为 Fulfilled
    • 已拒绝 Rejected: 表示异步操作失败, 通过 reject() 方法将状态从 Pending 改为 Rejected
  • 兑现或拒绝后, 状态就不会再改变, 即已敲定 Settled
  • then() 方法接收两个参数, 第一个是成功时的回调, 第二个是失败时的回调 (通常不写, 而是使用 catch() 方法)
  • then() 方法返回一个新的 Promise 对象, 可以进行链式调用
  • catch() 方法用于捕获错误, 与 then() 方法的第二个参数等效
  • finally() 方法用于无论成功或失败都会执行的回调, 通常用于清理工作, 放在链式调用的最后
通过 Promise 对象获取省份列表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建 Promise 对象
const promise = new Promise((resolve, reject) => {
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest()
// 设置请求方法和请求地址
xhr.open('GET', 'http://hmajax.itheima.net/api/province')
// 监听响应结果
xhr.addEventListener('load', () => {
// 如果请求成功
if (xhr.status === 200) {
// 获取响应结果
resolve(JSON.parse(xhr.response))
} else {
// 如果请求失败
reject(new Error(xhr.response))
}
})
// 发送请求
xhr.send()
})

// 定义成功和失败的回调
promise.then(res => {
res.list.forEach(item => console.log(item))
}).catch(err => {
alert(err.message)
})
封装一个简易的 axios 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 只支持 get 和 post 请求
// 只支持 json 数据格式
function axios(config = {
method: 'get',
url: '',
params: {},
data: {}
}) {
// 返回一个 Promise 对象
return new Promise((resolve, reject) => {
// 创建 XMLHttpRequest 对象
const xhr = new XMLHttpRequest()
// 判断请求方法, 处理 URL
config.method.toLowerCase() === 'get' && /?/.test(config.url) ? config.url += '?' + new URLSearchParams(config.params) : null
// 设置请求方法和请求地址
xhr.open(config.method, config.url)
// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json')
// 监听响应结果
xhr.addEventListener('loadend', () => {
// 如果请求成功
if (xhr.status >= 200 && xhr.status < 300) {
// 获取响应结果
resolve(JSON.parse(xhr.response))
} else {
// 如果请求失败
reject(new Error(xhr.response))
}
})
// 判断请求方法, 发送请求
config.method.toLowerCase() === 'get' ? xhr.send() : xhr.send(JSON.stringify(config.data))
})
}

// 发送请求
axios({
method: 'get',
url: 'http://hmajax.itheima.net/api/province'
}).then(res => {
res.list.forEach(item => console.log(item))
}).catch(err => {
alert(err.message)
})

调试时, 可以在浏览器的开发者工具中的 NetworkOnline 选项卡中模拟低网速和断网, 观察代码的执行情况

链式调用

回调函数地狱

回调函数地狱

回调地狱指的是多个回调函数嵌套调用, 导致代码难以阅读和维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取第一个省份, 并获取省份下的第一个城市, 再获取城市下的第一个区县
axios({ url: 'http://hmajax.itheima.net/api/province' }).then(res => {
document.querySelector('.province').innerHTML = res.list[0].name
axios({ url: `http://hmajax.itheima.net/api/city?pname=${res.list[0].name}` }).then(res => {
document.querySelector('.city').innerHTML = res.list[0].name
axios({ url: `http://hmajax.itheima.net/api/area?cname=${res.list[0].name}` }).then(res => {
document.querySelector('.area').innerHTML = res.list[0].name
}).catch(err => {
alert(err.message)
})
}).catch(err => {
alert(err.message)
})
}).catch(err => {
alert(err.message)
})

通过 Promise 对象的链式调用, 可以解决回调地狱的问题, 使代码更加清晰和易读

1
2
3
4
5
6
7
8
9
10
11
12
// 使用 Promise 对象的链式调用
axios({ url: 'http://hmajax.itheima.net/api/province' }).then(res => {
document.querySelector('.province').innerHTML = res.list[0].name
return axios({ url: `http://hmajax.itheima.net/api/city?pname=${res.list[0].name}` })
}).then(res => {
document.querySelector('.city').innerHTML = res.list[0].name
return axios({ url: `http://hmajax.itheima.net/api/area?cname=${res.list[0].name}` })
}).then(res => {
document.querySelector('.area').innerHTML = res.list[0].name
}).catch(err => {
alert(err.message) // 只需一个 catch() 方法, 捕获所有错误
})

如需共享某个数据, 可以在 axios 外声明一个变量, 或利用闭包

静态方法

  • Promise.all() 方法接收一个 Promise 对象数组, 返回一个新的 Promise 对象, 只有当所有 Promise 对象都成功时, 才会成功, 返回结果为一个数组, 顺序与传入的数组一致
  • Promise.allSettled() 类似于 Promise.all(), 当所有 Promise 对象都敲定时, 返回结果为一个数组, 包含每个 Promise 对象的结果
  • Promise.any() 方法接收一个 Promise 对象数组, 返回一个新的 Promise 对象, 只要有一个 Promise 对象成功, 就会成功, 并返回第一个成功的结果; 如果所有 Promise 对象都失败, 则返回错误数组
  • Promise.race() 类似于 Promise.all(), 但只要有一个 Promise 对象敲定就会返回结果, 无论成功或失败
  • Promise.withResolvers() 见下面的示例 (Node.js 暂不支持, Deno 1.38 后支持)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 比较差的写法
async function getPrice() {
const choice = await promptForDishChoice()
const prices = await fetchPrices()
return prices[choice]
}

// 更好的写法
async function getPrice() {
const [choice, prices] = await Promise.all([
// 两个任务并行执行
promptForDishChoice(),
fetchPrices(),
])
return prices[choice]
}

// 不用 Promise.withResolvers()
let res, rej
const promise = new Promise((resolve, reject) => {
res = resolve
rej = reject
})
// 使用 Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers()

async await

asyncawaitES2017 中新增的关键字, 用于简化 Promise 对象的使用和链式调用

关键字说明
async声明一个函数为异步函数, 返回一个 Promise 对象
await等待 Promise 对象的状态改变; 若成功, 返回 Promise 对象的结果, 函数继续执行
若失败, 抛出错误, 函数终止执行; 只能在 async 函数中使用

ECMAScript 2022 中引入了 顶层 await, 用于在模块的顶层使用 await 关键字; 但不支持 CommonJS 规范的 Node.js; 如果要使用顶层 await, 需要在 package.json<script>) 中设置 type 字段为 module, 并用 import 模块化规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用 async 和 await
async function getData() {
try {
// 等待返回结果
const res = await axios({ url: 'http://hmajax.itheima.net/api/province' })
document.querySelector('.province').innerHTML = res.list[0].name
// 也可以写在一行
document.querySelector('.city').innerHTML = (await axios({ url: `http://hmajax.itheima.net/api/city?pname=${res1.list[0].name}` })).list[0].name
// 也可以写 then 和 catch
document.querySelector('.area').innerHTML = await axios({ url: `http://hmajax.itheima.net/api/area?cname=${res2.list[0].name}` }).then(res => res.list[0].name).catch(err => err.message)
} catch (err) {
alert(err.message)
}
}
// 调用函数
getData()
.then(() => console.log('执行完成'))
.catch(() => console.log('执行失败'))

记得把 await 和异步操作用 () 包裹起来, 否则会报错

错误捕获

一个系统或用户用 throw 语句抛出的错误, 只会被最近的 catch 语句捕获; 如果没有 catch 语句, 错误会一直向上冒泡, 直到被浏览器捕获

1
2
3
4
5
6
7
8
9
10
11
// 链式调用
(async () => {
try {
await fetch('url')
.catch(() => fetch('url'))
.catch(() => fetch('url'))
.then(res => console.log(res))
} catch (err) {
console.log('三次请求全部失败')
}
})()

宏任务和微任务

ES6 中新增了 Promise 对象, 使得 JavaScript 也具有了发起和管理异步操作的能力; 从而引入了宏任务微任务的概念

  • 宏任务: 由浏览器环境执行的异步任务, 如 setTimeoutsetIntervalI/OUI 渲染等
  • 微任务: 由 JavaScript 引擎执行的异步任务, 如 Promiseprocess.nextTickMutationObserver
  • 宏任务队列: 存放宏任务的队列
  • 微任务队列: 存放微任务的队列, 优先级高于宏任务队列
任务类别
setTimeout / setInterval宏任务
<script> 脚本执行事件宏任务
AJAX 请求宏任务
用户交互事件宏任务
promise.then() / promise.catch()微任务
  • Promise 对象本身为同步任务
  • asyncawait 本质上是 Promise 的语法糖, 它们的执行顺序和 Promise 是一样的

fetch

fetchJavaScript 中的一个全局函数, 用于发送网络请求, 替代了 XMLHttpRequest 对象和 AJAX 技术, 返回一个 Promise 对象

1
fetch('url'[, req]).then(res => {}).catch(err => {})
相关对象说明
Responsefetch 成功后返回的响应对象, 用于获取响应结果
Request用于设置请求的相关信息
Headers用于设置请求头和获取响应头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fetch('url', {
method: 'get', // 请求方法, 默认为 get
headers: { // Headers 对象
'Content-Type': 'application/json'
},
body: JSON.stringify({ // 请求体
name: 'xiaoyezi',
age: 18
})
}).then(res => {
return res.json() // 返回一个 Promise 对象
}).then(data => {
console.log(data) // 打印响应结果
}).catch(err => {
console.log(err) // 打印错误信息
})
发送 GET 请求获取省份列表
1
2
3
4
5
6
7
8
9
10
11
// 发送 GET 请求
fetch('http://hmajax.itheima.net/api/province').then(res => {
// 通过 Response 对象的 json() 方法获取响应结果, 返回一个 Promise 对象
return res.json()
}).then(data => {
// 获取响应结果
data.list.forEach(item => console.log(item))
}).catch(err => {
// 请求失败后, 返回一个错误对象
alert(err.message)
})
发送 POST 请求登陆账号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button id="login">登陆</button>
<script>
// 获取元素
const username = document.querySelector('#username')
const password = document.querySelector('#password')
const login = document.querySelector('#login')

// 添加事件监听
login.onclick = function() {
// 发送 POST 请求
fetch('http://hmajax.itheima.net/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username.value,
password: password.value
})
}).then(res => {
return res.json()
}).then(data => {
// 如果成功, 提示登陆成功
alert(data.message)
// 禁用输入框和按钮
username.disabled = true
password.disabled = true
login.disabled = true
}).catch(err => {
// 如果失败, 根据错误类型提示错误信息
if (err.status === 0) { // 网络错误
alert('网络错误')
} else {
alert(err.message) // 其他错误
}
// 清空输入框
username.value = ''
password.value = ''
})
}
</script>
asyncawait 实现上述功能

获取省份列表

1
2
3
4
5
6
7
8
9
10
11
12
13
async function getProvince() {
try {
// 发送 GET 请求, 并将 Response 对象赋值给 res
const res = await fetch('http://hmajax.itheima.net/api/province')
// 通过 Response 对象的 json() 方法获取响应结果, 并赋值给 data
const data = await res.json()
// 打印响应结果
data.list.forEach(item => console.log(item))
} catch (err) {
alert(err.message)
}
}
getProvince()

登陆账号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<input type="text" id="username" placeholder="用户名">
<input type="password" id="password" placeholder="密码">
<button id="login">登陆</button>
<script>
// 获取元素
const username = document.querySelector('#username')
const password = document.querySelector('#password')
const login = document.querySelector('#login')

// 添加事件监听
login.onclick = async function() {
try {
// 发送 POST 请求, 并将 Response 对象赋值给 res
const res = await fetch('http://hmajax.itheima.net/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username.value,
password: password.value
})
})
// 通过 Response 对象的 json() 方法获取响应结果, 并赋值给 data
const data = await res.json()
// 如果成功, 提示登陆成功
alert(data.message)
// 禁用输入框和按钮
username.disabled = true
password.disabled = true
login.disabled = true
} catch (err) {
// 如果失败, 根据错误类型提示错误信息
if (err.status === 0) { // 网络错误
alert('网络错误')
} else {
alert(err.message) // 其他错误
}
// 清空输入框
username.value = ''
password.value = ''
}
}
</script>

Response 对象

属性或方法说明
new Response([body, options])创建一个新的 Response 对象
bodyBlobFormDatastringReadableStream
optionsstatusstatusTextheaders 对象等属性
res.headers响应的头部信息, Headers 对象
res.ok响应是否成功, 布尔值
res.status响应的状态码, 如 200404
res.statusText响应的状态文本, 如 OKNot Found
res.type响应的类型, 如 basiccors
res.url响应的 URL
res.body响应体 getter(可读流对象)
res.clone()克隆响应对象(用于多次处理响应体)
res.arrayBuffer()promise, 返回响应体的 ArrayBuffer 对象
res.blob()promise, 返回响应体的 Blob 对象
res.formData()promise, 返回响应体的 FormData 对象
res.json()promise, 返回响应体的 JSON 对象
res.text()promise, 返回响应体的文本

属性都是只读的, 且响应体只能被读取一次, 再次读取会报 TypeError: body stream is locked 错误

Request 对象

属性或方法说明
new Request('url')创建一个新的 Request 对象, 一般不直接使用
req.body请求体(可读流对象)
req.headers请求头部信息, Headers 对象
req.method请求方法, 如 GETPOST
req.mode请求模式, 如 corsno-corssame-origin
req.referrer请求 referrer (没有时, 为 '')
req.url请求的 URL (node 中可能不是完整 url, 详见相关内容)
req.arrayBuffer()promise, 返回请求体的 ArrayBuffer 对象
req.blob()promise, 返回请求体的 Blob 对象
req.formData()promise, 返回请求体的 FormData 对象
req.json()promise, 返回请求体的 JSON 对象
req.text()promise, 返回请求体的文本
req.clone()克隆请求对象(用于多次处理请求体)

属性都是只读的, 请求体只能被读取一次, 再次读取会报 TypeError: body stream is locked 错误

Headers 对象

属性或方法说明
headers.append(key, value)添加一个头部信息(如果已存在, 则添加到末尾)
headers.delete(key)删除一个头部信息
headers.entries()返回一个迭代器, 包含所有头部信息的键值对
headers.get(key)获取一个头部信息
headers.has(key)判断是否存在某个头部信息
headers.keys()返回一个迭代器, 包含所有头部信息的键
headers.set(key, value)设置(更新或添加)一个头部信息
headers.values()返回一个迭代器, 包含所有头部信息的值

⭐高级

ES6 中引入了 class 关键字, 用于定义类, 可以便捷地替代原本的构造函数原型对象原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义类
class Person {
// 构造函数
constructor(name, age) {
this.name = name
this.age = age
}
// 实例方法
say() {
console.log(this.name, this.age)
// 实例方法中 this 指向实例
}
// 静态方法
static makePerson(name, age) {
return new Person(name, age)
// 静态方法中 this 指向类本身
}
// 实例属性
message = 'hello' // 可以在构造函数外定义并赋初值
// 静态属性
static message = 'world'
}
// 调用
const personA = new Person('xiaoyezi', 18)
const personB = Person.makePerson('leaf', 18)
personA.say() // xiaoyezi 18
personB.say() // leaf 18
如果用老方法实现上面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 构造函数
function Person(name, age) {
this.name = name
this.age = age
}
// 实例方法
Person.prototype.say = function () {
console.log(this.name, this.age)
}
// 静态方法
Person.makePerson = function (name, age) {
return new Person(name, age)
}
// 调用
const personA = new Person('xiaoyezi', 18)
const personB = Person.makePerson('leaf', 18)
personA.say() // xiaoyezi 18
personB.say() // leaf 18

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 定义父类
// ...
// 定义子类
// extends 关键字表示继承某父类
class Student extends Person {
constructor(name, age, grade) {
// super 关键字指向父类
// 调用父类的构造函数
super(name, age)
this.grade = grade
}
// 重写父类的实例方法
say() {
// 由于 class 中只用 static 来区分静态方法和实例方法
// 所以 super.xxx() 既可以调用父类的静态方法, 也可以调用父类的实例方法
super.say()
console.log(`And the grade is ${this.grade}`)
}
// 静态方法
static makeStudent(name, age, grade) {
const person = super.makePerson(name, age)
return new Student(person.name, person.age, grade)
// 实际上 return new this(name, age, grade) 即可, 这里是为了演示
}
}
// 调用
const studentA = new Student('xiaoyezi', 18, 3)
const studentB = Student.makeStudent('leaf', 18, 3)
studentA.say() // xiaoyezi 18 And the grade is 3
studentB.say() // leaf 18 And the grade is 3
如果用老方法实现上面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义父类
// ...
// 定义子类
function Student(name, age, grade) {
// 调用父类的构造函数
Person.call(this, name, age)
this.grade = grade
}
// 继承父类的原型对象
Student.prototype = Object.create(Person.prototype)
// 重写父类的实例方法
Student.prototype.say = function () {
// 调用父类的实例方法
Person.prototype.say.call(this)
console.log(`And the grade is ${this.grade}`)
}
// 静态方法
Student.makeStudent = function (name, age, grade) {
const person = Person.makePerson(name, age)
return new Student(person.name, person.age, grade)
}
// 调用
const studentA = new Student('xiaoyezi', 18, 3)
const studentB = Student.makeStudent('leaf', 18, 3)
studentA.say() // xiaoyezi 18 And the grade is 3
studentB.say() // leaf 18 And the grade is 3

私有字段

ECMAScript 2022 中引入了 # 关键字, 用于定义类的私有属性和方法, 只能在类的内部访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义类
class Person {
// 实例属性 - 私有字段
#name
// 构造函数
constructor(name) {
this.#name = name
}
// 实例方法 - 访问私有字段
getName() {
return this.#name
}
setName(name) {
this.#name = name
}
}
// 调用
const person = new Person('xiaoyezi')
console.log(person.getName()) // xiaoyezi
console.log(person.name) // undefined
console.log(person.#name) // 报错
person.name = 'leaf' // name 和 #name 是两个不同的属性
console.log(person.getName()) // xiaoyezi
person.setName('leaf')
console.log(person.getName()) // leaf

静态属性和静态方法也可以设置为私有字段

in 操作符

in 操作符用于检测对象是否具有某个属性 (包括私有字段), 返回布尔值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Person {
constructor(name) {
this.#name = name
}
static check(obj) {
return #name in obj
}
}

// 相当于
class Person {
constructor(name) {
this.#name = name
}
static check(obj) {
try {
obj.#name
return true
} catch (err) {
if (err instanceof TypeError) {
return false
}
throw err
}
}
}

二进制数据

  • Blob: Binary Large Object, 二进制大对象, 表示一个不可变、原始数据的类文件对象
  • File: 继承自 Blob, 表示一个文件对象(<input type="file">
  • FileReader: 用于读取 BlobFile 对象的内容
  • ArrayBuffer: 表示一个通用的、固定长度的二进制数据缓冲区, 不能直接操作
  • TypedArray: ArrayBuffer 的视图, 用于操作 ArrayBuffer 对象
  • DataView: ArrayBuffer 的视图, 用于操作 ArrayBuffer 对象; 相比于 TypedArray, DataView 可以自定义字节序

乱七八糟的转换可以参考这里

TypedArray

  • Int8Array: 8 位有符号整数
  • Uint8Array: 8 位无符号整数
  • Uint8ClampedArray: 8 位无符号整数, 溢出时会被截断
  • Int16Array: 16 位有符号整数
  • Uint16Array: 16 位无符号整数
  • Int32Array: 32 位有符号整数
  • Uint32Array: 32 位无符号整数
  • Float32Array: 32 位浮点数
  • Float64Array: 64 位浮点数

Blob

BlobBinary Large Object 的缩写, 表示一个不可变、原始数据的类文件对象, 用于存储二进制数据

属性或方法作用
new Blob(array[, options])创建一个 Blob 对象
array: 可迭代对象, 如数组、字符串、TypedArray
option.type: MIME 类型
blob.size返回 Blob 对象的字节长度, 只读
blob.type返回 Blob 对象的 MIME 类型, 只读
blob.arrayBuffer()promise, 返回 Blob 对象的 ArrayBuffer 对象
blob.slice([start[, end[, type]]])返回一个新的 Blob 对象, 包含原 Blob 对象的部分数据
不传参相当于复制、不包含 endtypeMIME 类型
blob.stream()返回一个 ReadableStream 对象
blob.text()promise, 返回 Blob 对象的文本
URL.createObjectURL(blob)创建一个 URL, 用于访问 Blob 对象
URL.revokeObjectURL(url)释放一个 URL

ObjectURL 的生命周期是在 document 被卸载时结束, 但强烈建议在不需要时手动释放

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
// 编码为 URL
const encodedText = encodeURI(text)
// 发送请求
const res = await fetch(`${server}/?prompt=${encodedText}`)
// 响应头为 'content-type': 'image/png'
const blob = await res.blob()
// 创建一个 URL 对象
const imgUrl = URL.createObjectURL(blob)
// 创建一个图片元素
const img = document.createElement('img')
// 设置图片的 src
img.src = imgUrl

FileReader

FileReader 对象用于读取 BlobFile 对象的内容

方法作用
new FileReader()创建一个 FileReader 对象
reader.readAsArrayBuffer(blob/file)promise, 返回 ArrayBuffer 对象
reader.readAsBinaryString(blob/file)promise, 返回二进制字符串
reader.readAsDataURL(blob/file)promise, 返回 data: 格式的 URL (base64 编码)

使用前确认所在环境是否支持 FileReader 对象

🚧ArrayBuffer

🚧Atomics

Uint8Array

Uint8ArrayTypedArray 中的一种, 用于操作 ArrayBuffer 对象, 表示一个 8 位无符号整数的数组

属性或方法作用
new Uint8Array(length)创建一个 Uint8Array 数组, 长度为 length, 用 0 填充
new Uint8Array(typedArray/object/buffer)创建一个 Uint8Array 数组
Uint8Array.from(arrayLike)从类数组对象或可迭代对象中创建一个 Uint8Array 数组
包括字符串、数组、TypedArray
Uint8Array.of(...items)从参数中创建一个 Uint8Array 数组
u8a.buffer返回 Uint8Array 对象的 ArrayBuffer 对象, 只读
u8a.byteLength返回 Uint8Array 对象的字节长度, 只读
u8a.length返回 Uint8Array 对象的长度, 只读
u8a.toString()返回 Uint8Array 对象的字符串表示

由于 Uint8ArrayTypedArray 的一种, 所有也可以用许多数组的方法

其他数据类型

Set

SetES6 中新增的一种数据结构, 用于存储唯一的值, 类似于数组, 但值是唯一的

方法作用
new Set([arr])创建一个 Set 集合, 重复值会被去除
set.add(value)Set 集合中添加一个值, 重复值会被忽略
set.delete(value)删除 Set 集合中的一个值
set.has(value)判断 Set 集合中是否有某个值
set.clear()清空 Set 集合
set.size返回 Set 集合的大小
set.keys()
set.values()
返回一个迭代器, 包含 Set 集合的值
set.entries()返回一个迭代器, 包含 [value, value]
set.forEach(callback)遍历 Set 集合, 回调函数接受三个参数: valuekey (同 value)、set (当前 Set 集合)
1
2
3
4
5
6
7
8
9
10
11
// 创建
const set = new Set([1, 2, 3, 4, 5, 1, 2, 3])
console.log(set) // Set(5) { 1, 2, 3, 4, 5 }
// 添加
set.add(6)
// 删除
set.delete(6)
// 判断
console.log(set.has(5)) // true
// 清空
set.clear()

可以使用 Array.from 将迭代器转换为数组, 如 Array.from(new Set(arr)) 可以去除数组中的重复值, 并返回一个新数组

Map

MapES6 中新增的一种数据结构, 用于存储键值对, 类似于对象, 键是唯一的; 但 Map键可以是任意类型, 而对象的键只能是字符串或 Symbol 类型

方法作用
new Map()创建一个 Map 集合
map.set(key, value)Map 集合中添加一个键值对
map.get(key)获取 Map 集合中的一个值
map.delete(key)删除 Map 集合中的一个键值对
map.has(key)判断 Map 集合中是否有某个键
map.clear()清空 Map 集合
map.size返回 Map 集合的大小
map.keys()返回一个迭代器, 包含 Map 集合的键
map.values()返回一个迭代器, 包含 Map 集合的值
map.entries()返回一个迭代器, 包含 [key, value]
map.forEach(callback)遍历 Map 集合, 回调函数接受三个参数: valuekeymap (当前 Map 集合)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建
const map = new Map([
['name', 'xiaoyezi'],
['age', 18],
[
{ key: 'key' },
{ value: 'value' }
]
])
console.log(map) // Map(3) { 'name' => 'xiaoyezi', 'age' => 18, { key: 'key' } => { value: 'value' } }
// 添加
map.set('height', 180)
// 获取
console.log(map.get('name')) // xiaoyezi
// 删除
map.delete('height')
// 判断
console.log(map.has('age')) // true
// 清空
map.clear()

Symbol

SymbolES6 中新增的一种基本数据类型, 用于表示独一无二的值, 可以用于对象的属性名

  • Symbol 函数接受一个字符串作为参数, 用于描述 Symbol 的名称, 但是 Symbol 的名称只是一个描述, 不会影响 Symbol 的唯一性
  • Symbol 的值是唯一的, 即使描述相同, 值也不同
  • Symbol 的值可以作为对象的属性名, 用于解决对象属性名冲突的问题
  • Symbol 的值不会被 for...inObject.keysJSON.stringify 等遍历方法遍历到, 但可以使用 Object.getOwnPropertySymbols 方法获取
  • Symbol 的值可以作为对象的私有属性, 用于隐藏属性(见下方示例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建
const symbolA = Symbol('description')
const symbolB = Symbol('description')
console.log(symbolA) // Symbol(description)
console.log(symbolB) // Symbol(description)
console.log(symbolA === symbolB) // false
// 作为属性名
const obj = {
[symbolA]: 'valueOfA',
[symbolB]: 'valueOfB'
}
// symbol 属性名不能用 . 运算符访问
console.log(obj[symbolA]) // valueOfA
console.log(obj[symbolB]) // valueOfB
// 遍历时不会遍历到 symbol 属性
for (const key in obj) console.log(key) // 无输出
console.log(Object.keys(obj)) // []
console.log(Object.values(obj)) // []
console.log(JSON.stringify(obj)) // {}
// 获取 symbol 属性
console.log(Object.getOwnPropertySymbols(obj)) // [ Symbol(description), Symbol(description) ]

内置 Symbol 值

ES6 中提供了一些内置的 Symbol 值, 用于表示对象的内部方法, 如 .toString

  • 如果直接以字符串定义这些属性, 可能会与其他属性冲突, 也可能会被覆盖
  • Symbol.iterator: 用于表示对象的迭代器方法
  • Symbol.hasInstance: 用于表示对象的 instanceof 方法
  • Symbol.toPrimitive: 用于表示对象的转换方法
  • Symbol.toStringTag: 用于表示对象的 toString 方法
  • Symbol.isConcatSpreadable: 用于表示对象的 concat 方法
  • Symbol.species: 用于表示对象的构造函数
1
2
3
4
const obj = {}
console.log(obj.toString()) // [object Object]
obj[Symbol.toStringTag] = 'xxx'
console.log(obj.toString()) // [object xxx]

BigInt

BigInt 是一种内置对象, 它提供了一种方法来表示大于 2^53 - 1 的整数, 这原本是 Javascript 中可以用 Number 表示的最大数字; BigInt 可以表示任意大的整数

  • BigInt 不能用于 Math 对象的方法
  • BigInt 不能和 Number 直接运算, 需要先转换为同一类型(但要小心 BigInt 转换为 Number 时可能会丢失精度)
  • BigInt 不支持单目 + 运算符(即正号)
  • 运算结果的小数部分会被舍弃
  • BigInt('1') == 1true, BigInt('1') === 1false
  • BigInt('2') > 1true
  • Boolean(BigInt('0'))false(和 Number 一样)
1
2
3
4
5
6
7
8
// 用构造函数创建
const bigIntA = BigInt('1234567890123456789012345678901234567890')
// 用字面量创建
const bigIntB = 1234567890123456789012345678901234567890n
// 支持其他进制
const bigIntC = BigInt('0x1fffffffffffffffffffffffffffff')
// 类型为 bigint
console.log(typeof bigIntA) // bigint

存入 JSON

BigInt 类型的值不能直接存入 JSON(会出现 TypeError 错误), 需要先转换为字符串

1
2
3
4
5
6
7
8
9
// 声明对象
const obj = { bigInt: 1234567890123456789012345678901234567890n }
// 存入 JSON
const json = JSON.stringify(obj) // 报错
// 转换为字符串
const json = JSON.stringify({ bigInt: obj.bigInt.toString() })

// 也可以实现 toJSON 方法, 在转换为 JSON 时会自动调用
BigInt.prototype.toJSON = function() return this.toString()

遍历

方法适用对象示例
for数组for (let i = 0; i < arr.length; i++)
for...of数组、Set、Map、字符串等可迭代对象for (const value of arr)
可以使用 breakcontinue
for...in对象for (const key in obj)
forEach数组、Set、Maparr.forEach(value => null)
  • 上面的遍历方法无有效的返回值, 而其他遍历数组的方法(如 mapfilterreduce 等)都有有效的返回值, 详见数组方法
  • SetMap 集合都可以使用 for...of 循环遍历, 也可以使用 forEach 方法遍历
  • 通过解构赋值可以在遍历 Map 集合时获取键和值
1
2
3
4
5
6
7
8
9
10
// Set
for (const value of set) {
console.log(value)
}
set.forEach(value => console.log(value))
// Map
for (const [key, value] of map) { // 解构赋值
console.log(key, value)
}
map.forEach((value, key) => console.log(key, value))

可迭代对象

for...of 适用于实现了迭代器 [Symbol.iterator]()(这个常量见下文)方法的对象, 即可迭代对象, 包括数组、SetMap、字符串等

调用 [Symbol.iterator]().next() 方法会返回一个对象, 形如 { value: xxx, done: false }, value 为当前值, done 为是否遍历结束; 再次调用 next() 方法会返回下一个值, 直到 donetrue; 这就是 for...of 的原理

只要实现了 [Symbol.iterator]() 方法, 任何对象都可以使用 for...of 遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 为对象添加迭代器
// 由于一般对象的原型对象中没有 [Symbol.iterator] 方法
// 所以需要手动实现
const person = {
name: 'xiaoyezi',
age: 18,
hobby: ['coding', 'painting', 'psychology'],
[Symbol.iterator]() {
const keys = Object.keys(this)
let index = 0
return {
next: () => {
if (index < keys.length) {
// 由于箭头函数没有自己的 this, 所以这里的 this 指向 person
return { value: this[keys[index++]], done: false }
} else {
return { done: true }
}
}
}
}
}
// 遍历
for (const value of person) {
console.log(value) // xiaoyezi 18 [ 'coding', 'painting', 'psychology' ]
}

用生成器函数可以更方便地实现迭代器, 见下文

生成器

ES6 中引入了 function* 关键字, 用于定义生成器函数, 可以用于生成迭代器

  • 生成器函数内部可以使用 yield 关键字, 用于返回一个迭代器, 并暂停函数的执行
  • 返回的迭代器可以调用 next 方法, 用于继续函数的执行, 并返回一个迭代器
  • 生成器函数内部可以使用 return 关键字, 用于结束函数的执行
  • 生成器函数内部可以使用 throw 关键字, 用于抛出一个错误
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义生成器函数
function* person(name, age) {
yield name
yield age
}
// 调用
const generator = person('xiaoyezi', 18)
console.log(generator.next()) // { value: 'xiaoyezi', done: false }
console.log(generator.next()) // { value: 18, done: false }
console.log(generator.next()) // { value: undefined, done: true }
// 这只是一个简单的例子
// 可以在 yield 后面返回更丰富的值
// 也可以在 yield 之间添加更多的逻辑
用于发号器
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义生成器函数
function* idMaker() {
let index = 0
while (true) {
yield index++
}
}
// 调用
const generator = idMaker()
console.log(generator.next().value) // 0
console.log(generator.next().value) // 1
console.log(generator.next().value) // 2
// ...
用于实现[Symbol.iterator]()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义对象
const obj = {
name: 'xiaoyezi',
age: 18,
hobby: ['coding', 'painting', 'psychology'],
// 实现 [Symbol.iterator]() 方法
[Symbol.iterator]: function* () {
for (const key in this) yield this[key]
}
}
// 遍历
for (const value of obj) console.log(value)
// xiaoyezi 18 [ 'coding', 'painting', 'psychology' ]

用于异步

生成器函数可以用于异步编程, 可以用于实现 asyncawait 的功能; 调用 next 方法时可以传入参数, 用于传递异步操作的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义生成器函数
function* asyncFunction() {
try {
const result = yield fetch('https://api.xxx.com')
console.log(result) // 下面第二次调用 next 时传入的结果
} catch (error) {
console.log(error) // 下面第二次调用 throw 时传入的结果
}
}
// 调用
const generator = asyncFunction()
// 将 fetch('https://api.xxx.com') 的结果传入生成器函数
generator.next().value.then(
result => generator.next(result)
// 也可以抛出错误
// result => generator.throw('error')
)

Worker

JavaScript 是单线程语言, 即一次只能执行一个任务, 但 HTML5 提供了 Web Worker 接口, 可以创建多个线程, 用于执行一些耗时的任务, 如大量计算、文件读取等, 以提高性能

worker 被标准化后, 也被 Node.jsDeno 等环境支持

  • Worker 不能访问 DOM, 也不能访问 window 对象, 它的全局对象是 DedicatedWorkerGlobalScope, 可以通过 self 访问
  • 专用 Worker 只能被生成它的脚本所使用, 而共享 Worker 可以被多个脚本使用
  • 为了更好的错误处理控制, 推荐在主线程中将 Worker 的相关代码放在 if(window.Worker) {}
  • Worker 可以生成 subWorker, 但这些 subWorker 都是托管在主线程中的, 其 URL 相对路径是相对于主线程的
  • Worker 不能访问 HTML 中引入的外部脚本, 需要使用 importScripts('url') 方法引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// index.js
// 创建专用 Worker
const worker = new Worker('worker.js')
// 发送消息
inputElement.onchange = () => {
worker.postMessage(inputElement.value)
}
// 接收消息
worker.onmessage = function(event) {
console.log(event.data)
}
// 关闭 Worker
setTimeout(() => worker.terminate(), 30000)


// worker.js
// 接收消息
onmessage = event => {
console.log(event.data)
// 发送消息
postMessage('你好')
}
// 错误处理
onerror = event => {
event.preventDefault() // 阻止默认行为
console.log(event.message) // 错误信息
console.log(event.filename) // 错误文件
console.log(event.lineno) // 错误行号
}

注意事项

  • 线程安全: Worker 与主线程之间的通信是通过消息传递的, 且不共享全局对象, 因此不会出现数据竞争、死锁等问题, 相对不容易搞出问题
  • 内容安全策略: Worker 并不受限于主线程的内容安全策略, 有着自己的独立上下文
  • 消息传递: 主线程与 Worker 之间的数据传递是通过拷贝的方式, 而不是共享内存

SharedWorker

SharedWorker 可以被多个脚本使用, 但这些脚本必须来自同一个域名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// index.js
// 创建共享 Worker
const worker = new SharedWorker('worker.js')
// 共享 Worker 必须显式地使用 port 对象连接
// 而在专用 Worker 中, 这个过程是隐式的
// 发送消息
inputElement.onchange = () => {
worker.port.postMessage(inputElement.value)
}
// 接收消息
worker.port.onmessage = event => {
console.log(event.data)
}


// worker.js
// 接收消息
onconnect = event => {
const port = event.ports[0]
port.onmessage = event => {
console.log(event.data)
// 发送消息
port.postMessage('你好')
}
}

代码执行机制

  • 宿主环境: JavaScript 运行的环境, 如浏览器、Node.jsDenoBun
  • 浏览器内核: 浏览器的核心, 也叫排版引擎、浏览器引擎、页面渲染引擎, 负责解析 HTMLCSSJavaScript, 并渲染页面; 如 ChromeBlink 内核、SafariWebKit 内核
  • JavaScript 解释器: 浏览器内核中的一部分, 也叫 JavaScript 引擎, 负责解释 JavaScript 代码并将其转换为机器码执行; 如 ChromeV8 引擎、SafariJavaScriptCore 引擎
  • 每个宿主环境都有 JavaScript 解释器, Chrome浏览器Edge浏览器Node.jsDenoV8 引擎, Safari浏览器BunJavaScriptCore 引擎
  • 宏任务和微任务
  • 事件循环

V8 引擎

Chrome 浏览器的 JavaScript 引擎, 由 Google 公司使用 C++ 语言编写, 可以解释 JavaScriptWebAssembly 代码, 也被用于 Node.jsDeno 等环境; 可以在 GitHub 上查看 V8 的源代码(超过 100 万行); 以下是其主要运行机制, 可以参见这篇文章

  1. Parser 解析器: 将 JavaScript 代码解析为抽象语法树 AST
    词法分析: 将代码分解为词法单元, 如 letfunctionvarNamevarValue+;
    语法分析: 将词法单元转换为 AST
    可以在这个网站 查看指定代码的 AST; Vue 等代码也会被解析为 AST
    在解析过程中, 全局对象 window 会被创建, var 声明的变量会被挂载到 window
  2. Ignition 解释器: 将 AST 转换为字节码, 执行字节码
    Bytecode: 一种中间代码, 类似于汇编语言
    由于需要跨平台, 所以 V8 不会直接将 AST 转换为机器码, 而是转换为 Bytecode(类似于 JavaJVM
    但是字节码的效率比机器码低, 所以引入了下面的 TurboFan 编译器
  3. TurboFan 编译器: 将热点代码(HotSpot)的字节码编译为机器码, 执行机器码
    为什么不直接全部转为机器码: 编译过程耗时且占用内存, 而且有些代码只执行一次, 不值得编译为机器码
    有些时候 TurboFan 会将机器码再转为 Bytecode, 如 函数传参的类型发生变化 等情况
    所以 TypeScript 虽然仍会被解析为 JavaScript, 但性能可能会稍好一些

PreParser 预解析器: V8 引擎会先对代码进行预解析, 找出所有函数和变量(但不一定解析其全部的逻辑), 然后再进行正式解析(针对运行的需要对函数和变量进行选择性解析); 这样可以提高解析速度, 但会占用更多内存

IdexedDB

IndexedDBHTML5 中的一种本地数据库, 用于存储大量的结构化数据, 包括 blob, ArrayBuffer 等; 相比 localStorage 等, IndexedDB 更适合存储大量数据

由于 IndexedDBAPI 比较复杂, 对于简单的应用, 可以使用 localForage, IDB-Keyval 等库简化操作

localForage

1
2
3
# 安装 localForage
pnpm add localforage
# 也可以通过 CDN 引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入 localForage
import localforage from 'localforage'
// 获取值
const value = await localforage.getItem('key')
// 设置值
await localforage.setItem('key', value) // value 可以是任意类型
// 删除值
await localforage.removeItem('key')
// 清空所有数据
await localforage.clear()
// 获取 key 的数量
const length: number = await localforage.length()
// 获取所有 key
const keys: string[] = await localforage.keys()

IDB-Keyval

1
2
# 安装 IDB-Keyval
pnpm add idb-keyval
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 引入 IDB-Keyval
import { set, get, ... } from 'idb-keyval'
// 设置值
// key 支持 string、number、Date 以及它们的数组
await set('key', value)
await setMany([
['key1', value],
['key2', value],
])
// 获取值
const value: any = await get('key')
const [value1, value2] = await getMany(['key1', 'key2'])
const allKV = await entries() // [['key1', value1], ['key2', value2], ...]
const allKeys = await keys()
const allValues = await values()
// 更新值
await update('key', value => value.push('new'))
// 删除值
await del('key')
await delMany(['key1', 'key2'])
// 清空所有数据
await clear()

WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议, 用于实现客户端和服务器之间的实时通信

类型内容说明
构造函数WebSocket(url)创建一个 WebSocket 对象
属性ws.binaryTypeWebSocket 连接所传输的二进制数据类型
'blob''arraybuffer'
属性ws.readyState只读, WebSocket 连接的当前状态
0: 连接尚未建立
1: 连接已建立
2: 连接正在关闭
3: 连接已关闭
属性ws.url只读, WebSocket 连接的 URL
事件ws.onclose连接关闭时触发
事件ws.onerror连接出错时触发
事件ws.onmessage接收到消息时触发
回调函数的参数是一个 MessageEvent
data 属性是接收到的消息
事件ws.onopen连接成功时触发
方法ws.close()关闭连接
方法ws.send(data)发送消息 (将消息加入发送队列)
data 可以是字符串、ArrayBufferBlob
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8080')
// 连接成功
ws.onopen = () => {
console.log('连接成功')
// 发送消息
ws.send('你好')
}
// 接收消息
ws.onmessage = event => {
console.log(event.data)
}
// 连接关闭
ws.onclose = () => {
console.log('连接关闭')
}
ws.close()

Proxy

Proxy 对象用于拦截和自定义基本操作的行为(如属性查找、赋值、函数调用等)

类型内容说明
术语handler处理器事件, 其属性是某个事件的处理函数
构造函数new Proxy(target, handler)创建一个 Proxy 对象
构造函数Proxy.revocable(target, handler)创建一个可撤销的 Proxy 对象
return.proxy: Proxy 对象
return.revoke: 撤销 Proxy 的方法
handler 方法getPrototypeOf(target)拦截 Object.getPrototypeOf()
必须返回一个对象或 null
handler 方法setPrototypeOf(target, prototype)拦截 Object.setPrototypeOf()
如果成功修改 prototype, 返回 true
handler 方法isExtensible(target)拦截 Object.isExtensible()
必须返回一个布尔值
handler 方法defineProperty(target, prop, descriptor)拦截 Object.defineProperty()
如果成功定义属性, 返回 true
handler 方法has(target, prop)拦截 prop in target
必须返回一个布尔值
handler 方法get(target, prop, receiver)拦截属性读取
receiver 是原始操作行为所在的对象, 一般是 Proxy 对象本身或继承的对象
返回值可以是任意类型
handler 方法set(target, prop, value, receiver)拦截属性设置
如果成功设置属性, 返回 true
如果设置失败, 返回 false 或抛出错误
handler 方法deleteProperty(target, prop)拦截属性删除 (delete 操作符)
如果成功删除属性, 返回 true
如果删除失败, 返回 false 或抛出错误
handler 方法apply(target, thisArg, argumentsList)拦截函数调用
thisArg 是函数调用时的 this
argumentsList 是函数调用时的参数列表
handler 方法construct(target, argumentsList, newTarget)拦截 new 操作符
argumentsList 是构造函数的参数列表
newTarget 是被 new 的构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
// 当对象中没有属性时, 不反回默认的 undefined, 而是返回一个默认值
const proxy = new Proxy({}, {
get(target, prop) {
return prop in target ? target[prop] : 'default'
}
})

proxy.name = 'xiaoyezi'
proxy.hobby = undefined

console.log(proxy.name) // xiaoyezi
console.log(proxy.age) // default
console.log(proxy.hobby) // undefined

JSDoc

JSDoc 是一种用于描述 JavaScript 代码的注释规范, 可用于生成文档和类型检查

1
2
3
4
5
6
7
# 如果无需生成文档, 可以直接使用注释

# 安装 JSDoc
pnpm add -g jsdoc
# 生成文档
jsdoc ./index.js
jsdoc ./src -r -d ./docs

类型注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/** @type {number} */
const num = 1

/** @type {number[]} */
const arr = [1, 2, 3]
/** @type {Array<number>} */
const arr = [1, 2, 3]

/** @type {Promise<number>} */
const promise = new Promise(resolve => resolve(1))

/** @type {string | number} */
const strOrNum = 'str'

/** @type {{ name: string, age: number }} */
const obj = { name: 'xiaoyezi', age: 18 }

/** @type {(name: string, age: number) => void} */
const func = (name, age) => console.log(name, age)

导入类型

1
2
3
4
5
6
7
8
9
10
/** @typedef {import('path').PathLike} PathLike */

/** @type {PathLike} */
const path = 'path'

/** @type {import('path').PathLike} */
const path = 'path'

/** @param {import('path').PathLike} path */
const func = path => console.log(path)
1
2
// path.ts
export type PathLike = string

函数注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 打印名字和年龄
* @param {string} name - 名字
* @param {number} age - 年龄
* @returns {void}
*/
function func(name, age) {
console.log(name, age)
}

/**
* 返回 Promise
* @param {string} name - 名字
* @param {number} age - 年龄
* @returns {Promise<{ name: string, age: number }>}
*/
async function func(name, age) {
return { name, age }
}

/**
* 参数为对象
* @param {Object} person 人
* @param {string} person.name 名字
* @param {number} person.age 年龄
* @param {string} [person.gender] 性别 (可选)
* @param {string[]} [person.hobby=[]] 爱好 (可选, 默认值为 [])
* @returns {void}
*/
function func(person = {
hobby: []
}) {
console.log(person.name, person.age)
}

类注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 人类
* @extends Animal
*/
class Person extends Animal {
/**
* 创建一个人
* @param {string} name - 名字
* @param {number} age - 年龄
*/
constructor(name, age) {
this.name = name
this.age = age
}
/**
* 打印名字和年龄
* @returns {void}
*/
print() {
console.log(this.name, this.age)
}
}

类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @typedef {Object} Person 定义一个对象类型
* @property {string} name 名字 - Person 类型的必填字符串属性
* @property {number} age 年龄 - Person 类型的必填数字属性
* @property {string} [gender] 性别 - Person 类型的可选字符串属性
* @property {string[]} [hobby=[]] 爱好 - Person 类型的可选字符串数组属性 (默认值为 [])
*/
/**@typedef {{ name: string, age: number, gender?: string, hobby?: string[] }} Person 人 */

/** @type {Person} */
const person = {
name: 'xiaoyezi',
age: 18,
}

/**
* 用自定义类型作为函数参数
* @param {Person} person 人
* @returns {void}
*/
function func(person) {
console.log(person.name, person.age)
}

/**
* @callback PersonSay 定义一个函数类型
* @param {Person} person 人
* @returns {void}
*/

/**
* @type {PersonSay} 用自定义类型作为函数类型
*/
function func(person) {
console.log(person.name, person.age)
}

Object 可以写成 object, 但为了区分基本类型和引用类型, 一般写成 Object

泛型

1
2
3
4
5
6
7
8
/**
* @template T 泛型类型
* @param {T} value 泛型值
* @returns {T} 泛型值
*/
function identity(value) {
return value
}
  • 标题: JavaScript学习笔记
  • 作者: 小叶子
  • 创建于 : 2024-01-17 15:42:48
  • 更新于 : 2024-05-20 15:13:42
  • 链接: https://blog.leafyee.xyz/2024/01/17/JavaScript学习笔记/
  • 版权声明: 版权所有 © 小叶子,禁止转载。
评论