13、杂项
sidebarDepth: 2 tags:
- js
现代的 javaScript
1、简介
JavaScript 最开始是专门为浏览器设计的一门语言,但是现在也被用于很多其他的环境。
JavaScript 作为被应用最广泛的浏览器语言,且与 HTML/CSS 完全集成,具有独特的地位。
有很多其他的语言可以被“编译”成 JavaScript,这些语言还提供了更多的功能。
规范
ECMA-262 规范 包含了大部分深入的、详细的、规范化的关于 JavaScript 的信息。这份规范明确地定义了这门语言。
每年都会发布一个新版本的规范。 新的规范草案请见。
手册
兼容性表
微软已放弃 IE,可忽略,不关注。
如有需要看查看 兼容性表
2、JavaScript 基础知识
2.1 Hello, world!
提示
本文章是关于 JavaScript 的,但是为了演示效果,会用到一些浏览器的命令,如:alert
“script” 标签
我们几乎可以使用 script 标签将 JavaScript 程序插入到 HTML 文档的任何位置。
Demo
也可 F12 查看 控制台
<!DOCTYPE HTML> <html> <body> <p>script 标签之前...</p> <script> alert('Hello, world!'); console.log('你好,VuePress!') </script> <p>...script 标签之后</p> </body> </html>
你可以通过点击“运行”按钮来运行这个例子。
script 标签中包裹了 JavaScript 代码,当浏览器遇到 script 标签,代码会自动运行。
外部脚本
如果有大量的 JavaScript 代码,我们可以将它放入一个单独的文件。
脚本文件可以通过 src 特性(attribute)添加到 HTML 文件中。
<script src="/path/to/script.js"></script>
这里,/path/to/script.js 是脚本文件从网站根目录开始的绝对路径。当然也可以提供当前页面的相对路径。例如,src ="script.js",就像 src="./script.js",表示当前文件夹中的 "script.js" 文件。
我们也可以提供一个完整的 URL 地址,例如:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
要附加多个脚本,请使用多个标签:
<script src="/js/script1.js"></script> <script src="/js/script2.js"></script> …
Tips:
一般来说,只有最简单的脚本才嵌入到 HTML 中。更复杂的脚本存放在单独的文件中。
使用独立文件的好处是浏览器会下载它,然后将它保存到浏览器的 缓存 中。
之后,其他页面想要相同的脚本就会从缓存中获取,而不是下载它。所以文件实际上只会下载一次。
这可以节省流量,并使得页面(加载)更快。
Tips:如果设置了 src 特性,script 标签内容将会被忽略。
一个单独的 script 标签不能同时有 src 特性和内部包裹的代码。
这将不会工作:
<script src="file.js">alert(1); </script> // 此内容会被忽略,因为设定了 src
我们必须进行选择,要么使用外部的 script src="…",要么使用正常包裹代码的 script。
为了让上面的例子工作,我们可以将它分成两个 script 标签。
<script src="file.js"></script> <script> alert(1); </script>
2.2 代码结构
语句
语句是执行行为(action)的语法结构和命令。
我们已经见过了 alert('Hello, world!') 这样可以用来显示消息的语句。
我们可以在代码中编写任意数量的语句。语句之间可以使用分号进行分割。
例如,我们将 “Hello World” 这条信息一分为二:
Demo
也可 F12 查看 控制台
alert('Hello');alert('World!')
通常,每条语句独占一行,以提高代码的可读性:
alert('Hello'); alert('World!');
分号
当存在换行符(line break)时,在大多数情况下可以省略分号。
下面的代码也是可以运行的:
alert('Hello') alert('World!')
在这,JavaScript 将换行符理解成“隐式”的分号。这也被称为 自动分号插入。
在大多数情况下,换行意味着一个分号。但是“大多数情况”并不意味着“总是”!
有很多换行并不是分号的例子,例如:
Demo
也可 F12 查看 控制台
alert(3 + 1 + 2);
代码输出 6,因为 JavaScript 并没有在这里插入分号。显而易见的是,如果一行以加号 "+" 结尾,那么这是一个“不完整的表达式”,不需要分号。所以,这个例子得到了预期的结果。
但存在 JavaScript 无法确定是否真的需要自动插入分号的情况。
这种情况下发生的错误是很难被找到和解决的。
Tips:一个错误的例子
正常的例子
Demo
也可 F12 查看 控制台
alert("Hello"); [1, 2].forEach(alert);
我们必须进行选择,要么使用外部的 script src="…",要么使用正常包裹代码的 script。
为了让上面的例子工作,我们可以将它分成两个 script 标签。
<script src="file.js"></script> <script> alert(1); </script>
:::
2.3 现代模式,"use strict"
2.4 变量
2.5 数据类型
2.6 交互:alert、prompt 和 confirm
2.7 类型转换
2.8 基础运算符,数学运算
2.9 值的比较
2.10 条件分支:if 和 '?'
2.11 逻辑运算符
2.12 空值合并运算符 '??'
2.13 循环:while 和 for
2.14 "switch" 语句
2.15 函数
2.16 函数表达式
2.17 箭头函数,基础知识
2.18 JavaScript 特性
3、Object(对象):基础知识
4、数据类型
5、函数进阶内容
6、对象属性配置
7、原型,继承
8、类
9、错误处理
10、Promise,async/await
11、Generator,高级 iteration
12、模块
13、杂项
数据类型
JavaScript 中有八种基本的数据类型(译注:前七种为基本数据类型,也称为原始数据类型,而 object 为复杂数据类型)。
七种原始数据类型:
- number 用于任何类型的数字:整数或浮点数,在 ±(253-1) 范围内的整数。
- bigint 用于任意长度的整数。
- string 用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。
- boolean 用于 true 和 false。
- null 用于未知的值 —— 只有一个 null 值的独立类型。
- undefined 用于未定义的值 —— 只有一个 undefined 值的独立类型。
- symbol 用于唯一的标识符。
以及一种非原始数据类型:
- object 用于更复杂的数据结构。 我们可以通过 typeof 运算符查看存储在变量中的数据类型。
通常用作 typeof x,但 typeof(x) 也可行。
以字符串的形式返回类型名称,例如 "string"。
typeof null 会返回 "object" —— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个 object。
类型转换
有三种常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型。
字符串转换
转换发生在输出内容的时候,也可以通过 String(value) 进行显式转换。原始类型值的 string 类型转换通常是很明显的。
数字型转换
转换发生在进行算术操作时,也可以通过 Number(value) 进行显式转换。
数字型转换遵循以下规则:
值 | 变成…… |
---|---|
undefined | NaN |
null | 0 |
true / false | 1 / 0 |
string | “按原样读取”字符串,两端的空白字符(空格、换行符 \n、制表符 \t 等)会被忽略。空字符串变成 0。转换出错则输出 NaN。 |
布尔型转换
转换发生在进行逻辑操作时,也可以通过 Boolean(value) 进行显式转换。
布尔型转换遵循以下规则:
值 | 变成…… |
---|---|
0, null, undefined, NaN, "" | false |
其他值 | true |
上述的大多数规则都容易理解和记忆。人们通常会犯错误的值得注意的例子有以下几个:
对 undefined 进行数字型转换时,输出结果为 NaN,而非 0。
对 "0" 和只有空格的字符串(比如:" ")进行布尔型转换时,输出结果为 true。
值的比较
在 JavaScript 中,它们的编写方式如下:
大于 / 小于:a > b,a < b。
大于等于 / 小于等于:a >= b,a <= b。
检查两个值的相等:a == b,请注意双等号 == 表示相等性检查,而单等号 a = b 表示赋值。
检查两个值不相等:不相等在数学中的符号是 ≠,但在 JavaScript 中写成 a != b。
总结
比较运算符始终返回布尔值。
字符串的比较,会按照“词典”顺序逐字符地比较大小。
当对不同类型的值进行比较时,它们会先被转化为数字(不包括严格相等检查)再进行比较。
在非严格相等 == 下,null 和 undefined 相等且各自不等于任何其他的值。
在使用 > 或 < 进行比较时,需要注意变量可能为 null/undefined 的情况。比较好的方法是单独检查变量是否等于 null/undefined。
空值合并运算符 '??'
空值合并运算符 ?? 提供了一种从列表中选择第一个“已定义的”值的简便方式。
它被用于为变量分配默认值:
// 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100 height = height ?? 100;
?? 运算符的优先级非常低,仅略高于 ? 和 =,因此在表达式中使用它时请考虑添加括号。
如果没有明确添加括号,不能将其与 || 或 && 一起使用。
与 || 比较
或运算符 || 可以以与 ?? 运算符相同的方式使用。
例如,在上面的代码中,我们可以用 || 替换掉 ??,也可以获得相同的结果:
let firstName = null
let lastName = null
let nickName = 'Supercoder'
// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? '匿名') // Supercoder
// 显示第一个真值:
alert(firstName || lastName || nickName || 'Anonymous') // Supercoder
它们之间重要的区别是:
|| 返回第一个 真 值。
?? 返回第一个 已定义的 值。
换句话说,|| 无法区分 false、0、空字符串 "" 和 null/undefined。它们都一样 —— 假值(falsy values)。如果其中任何一个是 || 的第一个参数,那么我们将得到第二个参数作为结果。
不过在实际中,我们可能只想在变量的值为 null/undefined 时使用默认值。也就是说,当该值确实未知或未被设置时。
例如,考虑下面这种情况:
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0
height || 100 首先会检查 height 是否为一个假值,它是 0,确实是假值。
所以,|| 运算的结果为第二个参数,100。
height ?? 100 首先会检查 height 是否为 null/undefined,发现它不是。
所以,结果为 height 的原始值,0。
实际上,高度 0 通常是一个有效值,它不应该被替换为默认值。所以 ?? 运算得到的是正确的结果。
Object ---- 可选链 "?."
可选链 ?. 是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
- 如果可选链 ?. 前面的值为 undefined 或者 null,它会停止运算并返回 undefined。
例:
let user = {} // 一个没有 "address" 属性的 user 对象
alert(user.address.street) // Error!
alert(user?.address?.street) // undefined(不报错)
alert(user?.address) // undefined
alert(user?.address.street) // undefined
其它变体:?.(),?.[]
可选链 ?. 不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。
例如,将 ?.() 用于调用一个可能不存在的函数。
在下面这段代码中,有些用户具有 admin 方法,而有些没有:
let userAdmin = {
admin() {
alert('I am admin')
},
}
let userGuest = {}
userAdmin.admin?.() // I am admin
userGuest.admin?.() // 啥都没发生(没有这样的方法)
此外,我们还可以将 ?. 跟 delete 一起使用:
delete user?.name // 如果 user 存在,则删除 user.name
我们可以使用 ?. 来安全地读取或删除,但不能写入
- 可选链 ?. 不能用在赋值语句的左侧。
例如:
let user = null;
user?.name = "John"; // Error,不起作用
// 因为它在计算的是:undefined = "John"
总结
可选链 ?. 语法有三种形式:
obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined。
obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined。
obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined。
正如我们所看到的,这些语法形式用起来都很简单直接。?. 检查左边部分是否为 null/undefined,如果不是则继续运算。
?. 链使我们能够安全地访问嵌套属性。
但是,我们应该谨慎地使用 ?.,根据我们的代码逻辑,仅在当左侧部分不存在也可接受的情况下使用为宜。以保证在代码中有编程上的错误出现时,也不会对我们隐藏。
数据类型
要写有很多零的数字:
- 将 "e" 和 0 的数量附加到数字后。就像:123e6 与 123 后面接 6 个 0 相同。
- "e" 后面的负数将使数字除以 1 后面接着给定数量的零的数字。例如 123e-6 表示 0.000123(123 的百万分之一)。
对于不同的数字系统:
- 可以直接在十六进制(0x),八进制(0o)和二进制(0b)系统中写入数字。
- parseInt(str, base) 将字符串 str 解析为在给定的 base 数字系统中的整数,2 ≤ base ≤ 36。
- num.toString(base) 将数字转换为在给定的 base 数字系统中的字符串。
对于常规数字检测:
- isNaN(value) 将其参数转换为数字,然后检测它是否为 NaN
- isFinite(value) 将其参数转换为数字,如果它是常规数字,则返回 true,而不是 NaN/Infinity/-Infinity
要将 12pt 和 100px 之类的值转换为数字:
- 使用 parseInt/parseFloat 进行“软”转换,它从字符串中读取数字,然后返回在发生 error 前可以读取到的值。
小数:
- 使用 Math.floor,Math.ceil,Math.trunc,Math.round 或 num.toFixed(precision) 进行舍入。
- 请确保记住使用小数时会损失精度。
字符串
字符串的内部格式始终是 UTF-16,它不依赖于页面编码。
字符串可以包含在单引号、双引号或反引号中:
let single = 'single-quoted'
let double = 'double-quoted'
let backticks = `backticks` // 反引号允许我们通过 ${…} 将任何表达式嵌入到字符串中(包含函数)
function sum(a, b) {
return a + b
}
alert(`1 + 2 = ${sum(1, 2)}.`) // 1 + 2 = 3.
字符串长度
length 属性表示字符串长度:
alert( `My\n`.length ); // 3
注意 \n 是一个单独的“特殊”字符,所以长度确实是 3。
length 是一个属性
请注意 str.length 是一个数字属性,而不是函数。后面不需要添加括号。
访问字符
要获取在 pos 位置的一个字符,可以使用方括号 [pos] 或者调用 str.charAt(pos) 方法。第一个字符从零位置开始:
let str = `Hello`
// 第一个字符
alert(str[0]) // H
alert(str.charAt(0)) // H
// 最后一个字符
alert(str[str.length - 1]) // o
- 它们之间的唯一区别是,如果没有找到字符,[] 返回 undefined,而 charAt 返回一个空字符串:
let str = `Hello`
alert(str[1000]) // undefined
alert(str.charAt(1000)) // ''(空字符串)
我们也可以使用 for..of 遍历字符:
for (let char of 'Hello') {
alert(char) // H,e,l,l,o(char 变为 "H",然后是 "e",然后是 "l" 等)
}
字符串是不可变的
在 JavaScript 中,字符串不可更改。改变字符是不可能的。
let str = 'Hi'
str[0] = 'h' // error
alert(str[0]) // 无法运行
通常的解决方法是创建一个新的字符串,并将其分配给 str 而不是以前的字符串。
例如:
let str = 'Hi'
str = 'h' + str[1] // 替换字符串
alert(str) // hi
改变大小写
toLowerCase() 和 toUpperCase() 方法可以改变大小写:
alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface
或者我们想要使一个字符变成小写:
alert( 'Interface'[0].toLowerCase() ); // 'i'
查找子字符串
在字符串中查找子字符串有很多种方法。
- str.indexOf
第一个方法是 str.indexOf(substr, pos)。
它从给定位置 pos 开始,在 str 中查找 substr,如果没有找到,则返回 -1,否则返回匹配成功的位置。
例如:
let str = 'Widget with id'
alert(str.indexOf('Widget')) // 0,因为 'Widget' 一开始就被找到
alert(str.indexOf('widget')) // -1,没有找到,检索是大小写敏感的
alert(str.indexOf('id')) // 1,"id" 在位置 1 处(……idget 和 id)
可选的第二个参数允许我们从一个给定的位置开始检索。
例如,"id" 第一次出现的位置是 1。查询下一个存在位置时,我们从 2 开始检索:
let str = 'Widget with id'
alert(str.indexOf('id', 2)) // 12
如果我们对所有存在位置都感兴趣,可以在一个循环中使用 indexOf。每一次新的调用都发生在上一匹配位置之后:
let str = 'As sly as a fox, as strong as an ox'
let target = 'as' // 这是我们要查找的目标
let pos = 0
while (true) {
let foundPos = str.indexOf(target, pos)
if (foundPos == -1) break
alert(`Found at ${foundPos}`)
pos = foundPos + 1 // 继续从下一个位置查找
}
相同的算法可以简写:
let str = 'As sly as a fox, as strong as an ox'
let target = 'as'
let pos = -1
while ((pos = str.indexOf(target, pos + 1)) != -1) {
alert(pos)
}
- str.lastIndexOf(substr, pos)
还有一个类似的方法 str.lastIndexOf(substr, position),它从字符串的末尾开始搜索到开头。
它会以相反的顺序列出这些事件。
在 if 测试中 indexOf 有一点不方便。我们不能像这样把它放在 if 中:
let str = "Widget with id";
if (str.indexOf("Widget")) {
alert("We found it"); // 不工作!
}
上述示例中的 alert 不会显示,因为 str.indexOf("Widget") 返回 0
(意思是它在起始位置就查找到了匹配项)。是的,但是 if 认为 0 表示 false。
因此我们应该检查 -1,像这样:
let str = 'Widget with id'
if (str.indexOf('Widget') != -1) {
alert('We found it') // 现在工作了!
}
现在我们只会在旧的代码中看到这个技巧,因为现代 JavaScript 提供了 .includes 方法(见下文)。
- includes,startsWith,endsWith
更现代的方法 str.includes(substr, pos) 根据 str 中是否包含 substr 来返回 true/false。
如果我们需要检测匹配,但不需要它的位置,那么这是正确的选择:
alert('Widget with id'.includes('Widget')) // true
alert('Hello'.includes('Bye')) // false
str.includes 的第二个可选参数是开始搜索的起始位置:
alert('Widget'.includes('id')) // true
alert('Widget'.includes('id', 3)) // false, 从位置 3 开始没有 "id"
方法 str.startsWith 和 str.endsWith 的功能与其名称所表示的意思相同:
alert('Widget'.startsWith('Wid')) // true,"Widget" 以 "Wid" 开始
alert('Widget'.endsWith('get')) // true,"Widget" 以 "get" 结束
获取子字符串
JavaScript 中有三种获取字符串的方法:substring、substr 和 slice。
str.slice(start [, end])
str.substring(start [, end])
str.substr(start [, length])
我们回顾一下这些方法,以免混淆:
方法 | 选择方式…… | 负值参数 |
---|---|---|
slice(start, end) | 从 start 到 end(不含 end) | 允许 |
substring(start, end) | 从 start 到 end(不含 end) | 负值被视为 0 |
substr(start, length) | 从 start 开始获取长为 length 的字符串 | 允许 start 为负数 |
总结
有 3 种类型的引号。反引号允许字符串跨越多行并可以使用 ${…} 在字符串中嵌入表达式。
JavaScript 中的字符串使用的是 UTF-16 编码。
我们可以使用像 \n 这样的特殊字符或通过使用 \u... 来操作它们的 Unicode 进行字符插入。
获取字符时,使用 []。
获取子字符串,使用 slice 或 substring。
字符串的大/小写转换,使用:toLowerCase/toUpperCase。
查找子字符串时,使用 indexOf 或 includes/startsWith/endsWith 进行简单检查。
根据语言比较字符串时使用 localeCompare,否则将按字符代码进行比较。
还有其他几种有用的字符串方法:
str.trim() —— 删除字符串前后的空格 (“trims”)。
str.repeat(n) —— 重复字符串 n 次。
数组
获取最后一个元素
arr.at(i)
let fruits = ['Apple', 'Orange', 'Plum'] alert(fruits[fruits.length - 1]) // Plum 有点麻烦,不是吗?我们需要写两次变量名。 alert(fruits.at(-1)) // Plum
换句话说,arr.at(i):
如果 i >= 0,则与 arr[i] 完全相同。
对于 i 为负数的情况,它则从数组的尾部向前数。
pop/push, shift/unshift 方法
方法 | 作用 | 返回值 | 是否改变原数组 | 性能 |
---|---|---|---|---|
arr.push() | 在末端添加一个元素 | 数组长度 | 改变 | 快 |
arr.pop() | 从末端取出一个元素 | 返回移除的元素 | 改变 | 快 |
arr.shift() | 取出数组的第一个元素并返回它 | 取出的元素 | 改变 | 慢 |
arr.unshift() | 在数组的首端添加元素 | 数组长度 | 改变 | 慢 |
循环
for 循环
let arr = ['Apple', 'Orange', 'Pear'] for (let i = 0; i < arr.length; i++) { alert(arr[i]) }
for..of
let fruits = ['Apple', 'Orange', 'Plum'] // 遍历数组元素 for (let fruit of fruits) { alert(fruit) }
for..of 不能获取当前元素的索引,只是获取元素值,
for..in
— 永远不要用这个(比上面慢 10-100 倍)
let arr = ['Apple', 'Orange', 'Pear'] for (let key in arr) { alert(arr[key]) // Apple, Orange, Pear }
toString
数组有自己的 toString 方法的实现,会返回以逗号隔开的元素列表。
let arr = [123, [5, 6], 7]
console.log(String(arr)) // 123,5,6,7
数组方法备忘单:
添加/删除元素:
- push(...items) —— 向尾端添加元素,
- pop() —— 从尾端提取一个元素,
- shift() —— 从首端提取一个元素,
- unshift(...items) —— 向首端添加元素,
- splice(pos, deleteCount, ...items) —— 从 pos 开始删除 deleteCount 个元素,并插入 items。
- slice(start, end) —— 创建一个新数组,将从索引 start 到索引 end(但不包括 end)的元素复制进去。
- concat(...items) —— 返回一个新数组:复制当前数组的所有元素,并向其中添加 items。如果 items 中的任意一项是一个数组,那么就取其元素。
搜索元素:
- indexOf/lastIndexOf(item, pos) —— 从索引 pos 开始搜索 item,搜索到则返回该项的索引,否则返回 -1。
- includes(value) —— 如果数组有 value,则返回 true,否则返回 false。
- find/filter(func) —— 通过 func 过滤元素,返回使 func 返回 true 的第一个值/所有值。
- findIndex 和 find 类似,但返回索引而不是值。
遍历元素:
- forEach(func) —— 对每个元素都调用 func,不返回任何内容。
转换数组:
- map(func) —— 根据对每个元素调用 func 的结果创建一个新数组。
- sort(func) —— 对数组进行原位(in-place)排序,然后返回它。
- reverse() —— 原位(in-place)反转数组,然后返回它。
- split/join —— 将字符串转换为数组并返回。
- reduce/reduceRight(func, initial) —— 通过对每个元素调用 func 计算数组上的单个值,并在调用之间传递中间结果。
其他:
- Array.isArray(value) 检查 value 是否是一个数组,如果是则返回 true,否则返回 false。
请注意,sort,reverse 和 splice 方法修改的是数组本身。
arr.some(fn)/arr.every(fn) 检查数组。
与 map 类似,对数组的每个元素调用函数 fn。如果任何/所有结果为 true,则返回 true,否则返回 false。
这两个方法的行为类似于 || 和 && 运算符:如果 fn 返回一个真值,arr.some() 立即返回 true 并停止迭代其余数组项;如果 fn 返回一个假值,arr.every() 立即返回 false 并停止对其余数组项的迭代。
- 我们可以使用 every 来比较数组:
function arraysEqual(arr1, arr2) {
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}
alert( arraysEqual([1, 2], [1, 2])); // true
arr.fill(value, start, end) —— 从索引 start 到 end,用重复的 value 填充数组。
arr.copyWithin(target, start, end) —— 将从位置 start 到 end 的所有元素复制到 自身 的 target 位置(覆盖现有元素)。
arr.flat(depth)/arr.flatMap(fn) 从多维数组创建一个新的扁平数组。
Array.of(element0[, element1[, …[, elementN]]]) 基于可变数量的参数创建一个新的 Array 实例,而不需要考虑参数的数量或类型。