一、为什么学习 Vue 3 之前,一定要把这些 JavaScript 基础真正弄明白
Vue 3 并没有发明一套脱离 JavaScript 的新语言。无论是在普通 .js 文件中,还是在 Vue 单文件组件的 <script setup> 中,真正负责变量声明、数据加工、函数调用、异步等待、模块导入导出的,始终是 JavaScript。
这也是为什么很多 Vue 代码表面上看像"框架语法",真正拆开后却往往只是 JavaScript:
const、let决定变量如何声明和重新赋值;- 模板字符串负责动态拼接文字;
- 对象简写让配置对象、参数对象更紧凑;
- 解构赋值经常用来从对象、数组或函数返回结果中快速取值;
- 箭头函数大量出现在事件回调、数组遍历回调、Promise 回调中;
filter()、map()、reduce()是列表数据加工最常见的工具;- 扩展运算符
...常用于复制、合并数组与对象; JSON.stringify()与JSON.parse()是 Web Storage 保存复杂数据时的重要桥梁;localStorage、sessionStorage常承担浏览器端简单持久化;Promise、async、await决定异步请求代码能不能写得清楚;import、export是现代 Vue 工程拆分模块的基础。
Vue 官方当前推荐通过 create-vue 创建基于 Vite 的项目,生成的示例组件主要使用 Composition API 和 <script setup>。这意味着现代 JavaScript 的这些写法不是"可学可不学的语法糖",而是每天都会碰到的基础表达方式。
截至 2026 年 8 月,Vue 官方快速开始页面给出的 Node.js 前置版本要求为:
text
^22.18.0 || >=24.12.0
创建项目的官方命令仍然是:
bash
npm create vue@latest
创建完成后,通常执行:
bash
cd 项目目录
npm install
npm run dev
这里暂时不展开 Vue 工程本身,只需要明确一件事:Vue 3 项目运行在现代 JavaScript 工具链之上,所以本文所有知识都是后续写 Vue 代码时直接使用的语言能力。
二、变量与常量:let 和 const 到底区别在哪里
2.1 let:声明"绑定关系可以重新指向"的变量
最基本的写法:
js
// 使用 let 声明一个变量。
// 这个变量后续允许通过赋值运算符 = 改成另外一个值。
let name = 'Jack'
// name 原来保存的是字符串 'Jack',这里把它重新赋值为 'Bit'。
name = 'Bit'
console.log(name) // 'Bit'
let 的语法
js
let 变量名 = 初始值
也可以只声明,不立刻给初始值:
js
let result
// 此时 result 的值是 undefined。
console.log(result)
result = 100
console.log(result) // 100
let 的"参数"和"返回值"怎么理解
let 是 JavaScript 的声明关键字,不是函数,因此它不存在函数意义上的"参数"和"返回值"。
需要真正记住的是两件事:
第一,let 后面跟的是一个绑定名称,也就是变量名。第二,这个变量之后可以被重新赋值。
例如:
js
let page = 1
page = 2
page = 3
这种写法完全合理,因为 page 表示当前页码,本身就是会变化的数据。
let 的块级作用域
let 具有块级作用域。所谓"块",最常见的就是一对大括号 {} 包起来的区域。
js
if (true) {
// message 只在当前 if 的代码块内有效。
let message = 'hello'
console.log(message) // 可以访问
}
// 这里已经离开上面的代码块。
// console.log(message) // ReferenceError
在 Vue 代码里,这一点经常体现在函数内部、条件分支和循环内部的临时变量上。
2.2 const:不能重新赋值,但不代表"内部数据全部不可修改"
基本写法:
js
// const 声明后必须立即赋初始值。
const PI = 3.1415926
下面这种重新赋值是不允许的:
js
const PI = 3.14
// 错误:不能让 PI 重新指向另一个值。
// PI = 3.1415926
const 真正限制的是什么
最容易产生误解的一句话是:"const 声明的是常量,所以里面所有东西都不能变。"
这句话并不准确。
const 真正限制的是:变量名和当前值之间的绑定关系不能被重新赋值。
对于字符串、数字、布尔值等基础值,这通常表现为"值不能改"。
但对于数组和对象,变量保存的是对这个对象的引用。只要不让变量重新指向另一个数组或另一个对象,就仍然可以修改该对象内部的内容。
const 数组为什么仍然可以 push()
js
// arr 始终指向当前这个数组。
const arr = [1, 2, 3]
// push() 修改的是数组内部内容,没有让 arr 指向另一个数组。
arr.push(4)
console.log(arr) // [1, 2, 3, 4]
继续删除也可以:
js
const arr = [1, 2, 3]
// shift() 删除数组第一个元素。
arr.shift()
console.log(arr) // [2, 3]
真正不允许的是:
js
const arr = [1, 2, 3]
// 错误:这不是修改数组内容,而是让 arr 重新指向一个新数组。
// arr = [4, 5, 6]
const 对象为什么能改属性
js
const user = {
name: 'vue3',
age: 9
}
// 添加属性:仍然是在修改原对象内部的数据。
user.birth = 2017
// 修改属性:也没有改变 user 对这个对象的引用。
user.age = 10
console.log(user)
同样,不能这样做:
js
const user = {
name: 'vue3'
}
// 错误:企图让 user 重新指向另一个对象。
// user = { name: 'Vue' }
在 Vue 3 代码里为什么 const 特别常见
现代 Vue 3 代码中,const 的出现频率通常远高于 let。原因并不是 Vue 强制这样写,而是很多变量的"绑定关系"并不需要重新指向。
例如:
vue
<script setup>
// 普通数组引用不需要重新指向另外一个数组时,用 const 更合适。
const goods = [
{ id: 1, name: '键盘' },
{ id: 2, name: '鼠标' }
]
// 工具函数本身也不会被重新赋值,因此使用 const。
const getGoodsName = goodsItem => goodsItem.name
</script>
判断标准可以非常简单:
- 明确需要后续重新赋值:使用
let; - 不需要重新赋值:优先使用
const。
这里的"重新赋值"指变量本身重新指向别的值,不等于对象内部属性永远不能变化。
三、模板字符串:动态文本拼接最清晰的写法
3.1 普通字符串
普通字符串通常使用单引号或双引号:
js
const name = 'Jack'
const message = "Hello World"
单引号和双引号在 JavaScript 中都可以表示普通字符串。团队通常会通过 ESLint、Prettier 或代码规范统一风格,而不是从语言层面规定必须使用哪一种。
3.2 模板字符串使用反引号
模板字符串使用的是反引号:
js
const message = `Hello World`
键盘上这个符号通常位于数字 1 左边。
模板字符串最重要的两个能力是:
- 可以自然书写多行文本;
- 可以通过
${表达式}嵌入动态结果。
3.3 模板字符串可以自然换行
如果使用普通字符串拼接一大段 HTML 或多行文字,代码很容易变得拥挤:
js
const html = '<div><h1>热门商品</h1><p>今日推荐</p></div>'
模板字符串可以直接保持多行结构:
js
const html = `
<div class="goods-box">
<h1>热门商品</h1>
<p>今日推荐</p>
</div>
`
注意:换行和缩进本身也会成为字符串内容的一部分。这一点在生成纯文本时尤其需要注意。
3.4 ${表达式}:模板字符串最核心的能力
语法:
js
`${表达式}`
这里放的不是只能写"变量名",而是可以放一个能计算出结果的 JavaScript 表达式。
例如变量:
js
const name = 'Bit'
const age = 18
const message = `姓名:${name},年龄:${age}`
console.log(message)
// 姓名:Bit,年龄:18
也可以放计算表达式:
js
const price = 99
const count = 3
const message = `总价:${price * count} 元`
console.log(message) // 总价:297 元
也可以放三元表达式:
js
const age = 17
const message = `当前状态:${age >= 18 ? '已成年' : '未成年'}`
console.log(message)
模板字符串有没有返回值
模板字符串本身是一种表达式,它执行后的结果是一个字符串。
例如:
js
const result = `1 + 2 = ${1 + 2}`
右侧模板字符串表达式的最终结果为:
text
1 + 2 = 3
这个字符串再被赋值给 result。
Vue 3 中的实际意义
即使 Vue 模板里有自己的插值语法 {``{ }},JavaScript 区域依然大量需要模板字符串。例如动态生成日志、请求参数中的字符串、路径、提示语等:
js
const id = 1001
const requestPath = `/api/goods/${id}`
这里不要把两种语法混淆:
- Vue 模板插值:
{``{ message }}; - JavaScript 模板字符串:
Hello ${name}。
它们解决的问题不同。
四、对象:属性访问、动态属性名、属性简写与方法简写
4.1 对象是什么
对象适合描述"一个完整事物具有多项信息"的数据结构。
例如一个商品:
js
const goods = {
id: 1001,
name: '机械键盘',
price: 299
}
这里:
id、name、price是属性名;1001、'机械键盘'、299是对应属性值。
4.2 点语法取值
当属性名是明确、固定、合法的标识符时,点语法最直接:
js
const user = {
name: 'Vue',
age: 10
}
console.log(user.name) // 'Vue'
console.log(user.age) // 10
点语法可以理解为:"从 user 这个对象中读取名字就叫 name 的属性"。
4.3 中括号语法取值
中括号语法:
js
对象[属性名表达式]
例如:
js
const user = {
name: 'Vue',
age: 10
}
console.log(user['name']) // 'Vue'
console.log(user['age']) // 10
当中括号里直接写字符串时,与点语法效果类似。
真正重要的是:属性名来自变量时,必须使用中括号语法。
js
const user = {
name: 'Vue',
age: 10
}
const key = 'name'
// key 的值是 'name',所以实际读取的是 user['name']。
console.log(user[key]) // 'Vue'
不能写成:
js
const key = 'name'
// 这表示读取名字真的叫 "key" 的属性,而不是读取变量 key 对应的属性名。
console.log(user.key)
一句话分清 obj.key 和 obj[key]
obj.key 中的 key 是写死的属性名。
obj[key] 中的 key 会先作为 JavaScript 表达式求值,再把求出的结果当成属性名。
例如:
js
const fieldName = 'price'
const goods = {
price: 299
}
console.log(goods[fieldName]) // 299
这种动态属性访问在处理表单字段、后端返回字段、表格列配置等场景中非常常见。
4.4 对象属性简写
当"属性名"和"变量名"完全相同时,可以省略冒号和右边的变量名。
传统写法:
js
const min = 1
const max = 99
const options = {
min: min,
max: max
}
简写:
js
const min = 1
const max = 99
const options = {
// 等价于 min: min
min,
// 等价于 max: max
max
}
注意:这种简写成立的前提就是变量名和希望生成的属性名一致。
例如:
js
const userName = 'Jack'
const user = {
// 这里生成的属性名是 userName,而不是 name。
userName
}
如果希望最终属性名叫 name,仍然应该明确写:
js
const userName = 'Jack'
const user = {
name: userName
}
4.5 对象方法简写
早期常见写法:
js
const calculator = {
add: function (x, y) {
return x + y
}
}
现代简写:
js
const calculator = {
// 省略了冒号和 function 关键字。
add(x, y) {
return x + y
}
}
add(x, y) 的参数
x:第一个参与计算的值。
y:第二个参与计算的值。
这里没有限定必须为数字,但因为函数体执行 x + y,如果传入字符串就会发生字符串拼接,所以实际使用时要保证调用方传入符合预期的数据类型。
add(x, y) 的返回值
返回 x + y 的计算结果。
例如:
js
calculator.add(10, 20) // 30
如果传入:
js
calculator.add('10', '20') // '1020'
结果就不是数值相加,而是字符串拼接。这也是为什么"参数含义"不能只记变量名,还要知道调用时应该传什么数据。
五、解构赋值:从数组和对象中快速"拆值"
5.1 解构赋值解决了什么问题
假设有一个数组:
js
const arr = [11, 22, 33]
传统取值:
js
const a = arr[0]
const b = arr[1]
const c = arr[2]
数组解构:
js
const [a, b, c] = arr
它不是创造了新的数据结构,而是把右侧已有的数据按照一定规则分配给左侧变量。
5.2 数组解构:按照位置匹配
数组解构最核心的规则是:按位置匹配。
js
const arr = [11, 22, 33]
const [a, b, c] = arr
console.log(a) // 11
console.log(b) // 22
console.log(c) // 33
左侧第一个变量接右侧第一个元素,第二个变量接第二个元素,以此类推。
跳过某个位置
js
const arr = [11, 22, 33]
// 第一个位置没有变量,因此跳过 11。
const [, b, c] = arr
console.log(b) // 22
console.log(c) // 33
剩余元素收集
js
const arr = [11, 22, 33, 44]
const [first, ...rest] = arr
console.log(first) // 11
console.log(rest) // [22, 33, 44]
这里 ...rest 的含义是"把还没有被前面变量接走的剩余元素收集成一个新数组"。
注意:它和后面要讲的"扩展运算符"虽然都写成 ...,但所在位置不同,含义也不同。
- 解构左侧的
...rest:收集剩余数据; - 数组/对象字面量右侧的
...arr:展开数据。
嵌套数组解构
js
const arr = [2, [3, 4], 5]
// 第一层第二个元素本身又是一个数组,所以继续使用数组解构。
const [, [a, b]] = arr
console.log(a) // 3
console.log(b) // 4
理解这段代码的关键不是背写法,而是按结构"对齐":
text
右边:[ 2, [3, 4], 5 ]
左边:[ , [a, b] ]
5.3 对象解构:按照属性名匹配
对象解构和数组解构最大的差别是:
- 数组解构看位置;
- 对象解构看属性名。
js
const user = {
name: 'Bit',
age: 9,
address: '西安'
}
const { name, age, address } = user
console.log(name) // 'Bit'
console.log(age) // 9
console.log(address) // '西安'
顺序不重要:
js
const { address, name } = user
因为对象是通过属性名匹配,不是通过书写位置匹配。
5.4 对象解构重命名
如果对象属性名叫 name,但当前作用域里希望变量叫 userName:
js
const user = {
name: 'Bit',
age: 9
}
const { name: userName } = user
console.log(userName) // 'Bit'
这句特别容易被读反。
js
const { name: userName } = user
正确理解是:
读取
user.name,把这个值放进一个名叫userName的局部变量。
不是把 userName 属性改名成 name。
5.5 对象剩余属性
js
const user = {
id: 1,
name: 'Bit',
age: 9
}
const { name, ...rest } = user
console.log(name) // 'Bit'
console.log(rest) // { id: 1, age: 9 }
rest 会得到尚未被前面解构出来的剩余可枚举自有属性组成的新对象。
5.6 深层对象解构
真实接口结果经常是多层结构:
js
const response = {
data: {
code: 10000,
message: '频道列表获取成功',
result: ['HTML', 'CSS', 'JavaScript', 'Vue']
},
status: 200,
statusText: 'OK'
}
如果希望直接得到 code、message,并把 result 改名为 list:
js
const {
data: {
code,
message,
result: list
}
} = response
console.log(code) // 10000
console.log(message) // '频道列表获取成功'
console.log(list) // ['HTML', 'CSS', 'JavaScript', 'Vue']
这段代码值得逐层理解:
第一层先匹配 response.data;第二层再从 data 对象中匹配 code、message、result;最后把 result 的值放进变量 list。
解构本身的返回值问题
解构赋值不是一个普通函数,因此没有"参数列表"和"函数返回值"。它是一种赋值语法。最终效果是创建或更新变量绑定。
六、箭头函数:现代 Vue 代码中最常见的函数表达式之一
6.1 普通函数与函数表达式
函数声明:
js
function add(x, y) {
return x + y
}
函数表达式:
js
const add = function (x, y) {
return x + y
}
箭头函数:
js
const add = (x, y) => {
return x + y
}
这三段代码在这个简单例子里都能实现加法,但箭头函数并不是普通函数的"完全替代品"。在本文范围内,最重要的是掌握它作为回调函数和短小工具函数的写法。
6.2 箭头函数完整语法
js
const 函数名 = (参数1, 参数2) => {
函数体
return 返回值
}
示例:
js
const add = (x, y) => {
// x 和 y 是调用函数时传入的两个参数。
// 使用 + 运算符得到结果。
return x + y
}
const result = add(10, 20)
console.log(result) // 30
参数
x:第一个实参对应的形参。
y:第二个实参对应的形参。
调用 add(10, 20) 时:
x接收到10;y接收到20。
返回值
return x + y 会把运算结果返回给调用位置。
所以:
js
const result = add(10, 20)
最终 result 是 30。
6.3 只有一个参数时可以省略圆括号
完整形式:
js
const double = (value) => {
return value * 2
}
当只有一个普通参数时,可以写成:
js
const double = value => {
return value * 2
}
两个及以上参数仍然需要圆括号:
js
const add = (x, y) => x + y
没有参数时也需要空圆括号:
js
const sayHello = () => 'hello'
6.4 函数体只有一个表达式时可以省略大括号和 return
完整写法:
js
const add = (x, y) => {
return x + y
}
简写:
js
const add = (x, y) => x + y
这叫"隐式返回"。
右侧表达式 x + y 的结果,会自动作为函数返回值。
因此:
js
const result = add(3, 5)
console.log(result) // 8
一旦写了大括号,就不能忘记 return
错误理解:
js
const add = (x, y) => {
x + y
}
这个函数实际返回 undefined,因为大括号函数体没有显式 return。
正确:
js
const add = (x, y) => {
return x + y
}
或者:
js
const add = (x, y) => x + y
6.5 隐式返回对象时必须用圆括号包住对象
如果希望箭头函数直接返回对象,不能直接写:
js
// 容易被解析为函数体的大括号,而不是对象字面量。
// const createUser = () => { name: 'Bit' }
正确:
js
const createUser = () => ({
name: 'Bit',
age: 9
})
为什么外面有一层圆括号?
因为:
js
({ name: 'Bit' })
明确告诉 JavaScript:这是一整个对象表达式,请把这个对象作为返回结果。
createUser() 的参数
当前示例没有参数,所以调用时直接写:
js
createUser()
createUser() 的返回值
返回一个新对象:
js
{
name: 'Bit',
age: 9
}
6.6 箭头函数最常见的应用:回调函数
例如定时器:
js
setTimeout(() => {
console.log('2 秒后执行')
}, 2000)
这里传给 setTimeout() 的第一个参数就是一个箭头函数。
又例如数组:
js
const numbers = [1, 2, 3]
const doubled = numbers.map(item => item * 2)
箭头函数特别适合这种"把一个短小函数当参数传给另一个函数"的场景。
6.7 箭头函数与 this 的关键差异
这部分不需要深入原型或执行上下文,但必须知道一个结论:箭头函数没有自己的 this 绑定,它会使用外层作用域的 this。
因此,箭头函数并不适合作为所有对象方法的机械替代。
例如需要对象方法中的动态 this 时,更适合使用普通方法简写:
js
const user = {
name: 'Bit',
// 普通方法调用 user.getName() 时,this 指向 user。
getName() {
return this.name
}
}
本文在 Vue 3 前置阶段只需要记住:数组回调、Promise 回调、短小纯函数经常用箭头函数;依赖自身 this 的对象方法不要盲目改成箭头函数。
七、数组的重要方法:Vue 列表数据处理最核心的一组工具
数组在 Vue 项目里出现得极其频繁。商品列表、用户列表、菜单列表、表格数据、搜索结果、分类数据,本质上通常都是数组。
真正写业务代码时,很少只是"把数组打印出来",更多是下面这些操作:
- 在头部或尾部添加数据;
- 删除某个元素;
- 判断一个值是否存在;
- 遍历所有元素;
- 筛选满足条件的元素;
- 把每个元素转换成另一种结构;
- 判断是否全部满足条件;
- 把一整个数组汇总成一个结果。
这一章不仅要记方法名,更要明确每个方法的 参数、回调参数、返回值、是否修改原数组、适合解决什么问题。
7.1 push():在数组尾部添加一个或多个元素
基本语法
js
array.push(element1, element2, ..., elementN)
参数解析
element1 到 elementN:希望追加到数组尾部的元素。
可以只传一个,也可以一次传多个。
返回值
push() 的返回值不是"添加进去的元素",也不是"修改后的数组",而是:
数组修改后的新长度。
是否修改原数组
会。
push() 是一个原地修改数组的方法。
示例
js
const goods = ['键盘', '鼠标']
// push() 会直接修改 goods,把 '显示器' 追加到最后。
// 同时,push() 自己的返回值是修改后的数组长度。
const newLength = goods.push('显示器')
console.log(goods)
// ['键盘', '鼠标', '显示器']
console.log(newLength)
// 3
一次添加多个:
js
const list = [1, 2]
// 一次把 3、4、5 都追加到末尾。
const length = list.push(3, 4, 5)
console.log(list) // [1, 2, 3, 4, 5]
console.log(length) // 5
常见误区
错误写法:
js
const arr = [1, 2, 3]
// push 返回长度,因此 newArr 会是数字 4,不是数组。
const newArr = arr.push(4)
如果目标是得到数组本身,应直接继续使用 arr;如果目标是"不修改原数组,得到新数组",则可以使用后面会讲的扩展运算符:
js
const arr = [1, 2, 3]
const newArr = [...arr, 4]
7.2 unshift():在数组头部添加一个或多个元素
基本语法
js
array.unshift(element1, element2, ..., elementN)
参数解析
传入一个或多个需要放到数组开头的元素。
返回值
返回修改后的数组新长度。
是否修改原数组
会。
示例
js
const messages = ['第二条', '第三条']
// 在开头加入 '第一条'。
const newLength = messages.unshift('第一条')
console.log(messages)
// ['第一条', '第二条', '第三条']
console.log(newLength)
// 3
一次添加多个元素:
js
const arr = [3, 4]
arr.unshift(1, 2)
console.log(arr) // [1, 2, 3, 4]
push() 和 unshift() 怎么选
- 尾部添加:
push(); - 头部添加:
unshift()。
从语义上选择即可,不要因为"都能添加"就混着使用。
7.3 pop():删除数组最后一个元素
基本语法
js
array.pop()
参数
没有参数。
返回值
返回被删除的最后一个元素。
如果数组原本为空,则返回 undefined。
是否修改原数组
会。
示例
js
const arr = [11, 22, 33]
// 删除最后一个元素 33,并把被删除的 33 返回。
const removed = arr.pop()
console.log(removed) // 33
console.log(arr) // [11, 22]
空数组:
js
const arr = []
const removed = arr.pop()
console.log(removed) // undefined
console.log(arr) // []
7.4 shift():删除数组第一个元素
基本语法
js
array.shift()
参数
没有参数。
返回值
返回被删除的第一个元素。
空数组调用时返回 undefined。
是否修改原数组
会。
示例
js
const arr = [11, 22, 33]
// 删除开头的 11。
const removed = arr.shift()
console.log(removed) // 11
console.log(arr) // [22, 33]
pop() 和 shift() 怎么选
- 删除末尾:
pop(); - 删除开头:
shift()。
这四个方法可以配对记忆:
| 位置 | 添加 | 删除 |
|---|---|---|
| 头部 | unshift() |
shift() |
| 尾部 | push() |
pop() |
其中 push()、unshift() 返回新长度;pop()、shift() 返回被删除的元素。
7.5 splice():从任意位置删除、替换或插入元素
splice() 是数组修改方法里功能最灵活的一个。它可以完成三类事情:
- 删除;
- 插入;
- 替换。
基本语法
js
array.splice(start, deleteCount, item1, item2, ..., itemN)
参数一:start
表示从哪个下标开始操作。
例如数组:
js
const arr = ['A', 'B', 'C', 'D']
下标分别是:
text
A -> 0
B -> 1
C -> 2
D -> 3
所以 start = 1 表示从 'B' 的位置开始。
start 也可以是负数,负数会从数组末尾方向计算位置。不过日常 Vue 列表操作里,最常见的还是通过 findIndex() 等方式得到正数下标后交给 splice()。
参数二:deleteCount
表示从 start 开始,删除多少个元素。
例如:
js
arr.splice(1, 2)
表示从下标 1 开始删除两个元素。
后续参数:item1...itemN
表示从 start 位置插入的新元素。
如果不提供这些参数,就只是删除;如果 deleteCount 写成 0,就只是插入。
返回值
splice() 返回一个数组,数组中保存"被删除的元素"。
即使只删除一个元素,返回值依然是数组。
是否修改原数组
会。
这是理解 splice() 时最不能忽略的一点。
删除示例
js
const arr = [11, 22, 33, 44]
// 从下标 1 开始删除 1 个元素,也就是删除 22。
const removed = arr.splice(1, 1)
console.log(removed) // [22]
console.log(arr) // [11, 33, 44]
插入示例
js
const arr = [11, 22, 33, 44]
// 从下标 3 的位置开始,不删除任何元素,并插入 55。
arr.splice(3, 0, 55)
console.log(arr)
// [11, 22, 33, 55, 44]
替换示例
js
const arr = ['旧商品', '鼠标', '键盘']
// 从下标 0 开始删除 1 个元素,同时插入 '新商品'。
// 效果等价于把第一个元素替换掉。
const removed = arr.splice(0, 1, '新商品')
console.log(removed) // ['旧商品']
console.log(arr) // ['新商品', '鼠标', '键盘']
在列表删除中的常见思路
假设有商品数组:
js
const goods = [
{ id: 1, name: '键盘' },
{ id: 2, name: '鼠标' },
{ id: 3, name: '显示器' }
]
如果已经知道需要删除的下标:
js
const index = 1
// 从 index 开始只删除一个元素。
goods.splice(index, 1)
如果目标是不修改原数组,现代 JavaScript 还提供 toSpliced(),但在本文的 Vue 前置范围内,先把 splice() 的参数与返回值彻底理解更重要。两者最大的思维区别是:
splice():修改原数组;toSpliced():返回新数组,不修改原数组。
7.6 includes():判断数组是否包含某个值
基本语法
js
array.includes(searchElement, fromIndex)
参数一:searchElement
要搜索的目标值。
这是必需参数。
参数二:fromIndex
可选。
表示从哪个下标开始向后搜索。省略时从下标 0 开始。
返回值
布尔值:
- 找到:
true; - 没找到:
false。
是否修改原数组
不会。
示例
js
const permissions = ['read', 'write', 'delete']
console.log(permissions.includes('write')) // true
console.log(permissions.includes('admin')) // false
指定起始位置:
js
const arr = [10, 20, 10]
console.log(arr.includes(10, 1))
// true,因为从下标 1 开始,后面下标 2 还有一个 10。
对象数组中的重要提醒
includes() 比较对象时比较的是"是不是同一个对象引用",不是看对象内容是否长得一样。
js
const user = { id: 1 }
const list = [user]
console.log(list.includes(user)) // true
console.log(list.includes({ id: 1 })) // false
第二个 { id: 1 } 虽然内容一样,但它是新创建的另一个对象。
如果业务需求是"对象数组里有没有 id === 1 的元素",一般会用 some() 或 find()。这属于数组进一步应用,当前只需要把 includes() 的比较特点分清。
7.7 forEach():对数组中的每个元素执行一次回调
基本语法
js
array.forEach(callbackFn, thisArg)
日常最常写成:
js
array.forEach((item, index, array) => {
// 每次遍历执行一次
})
参数一:callbackFn
这是一个函数。数组中每个实际存在的元素都会触发一次这个函数。
回调函数最多会接收到三个常用参数。
回调参数 1:item / element
当前正在处理的数组元素。
例如:
js
const arr = ['A', 'B', 'C']
arr.forEach(item => {
console.log(item)
})
依次得到:
text
A
B
C
回调参数 2:index
当前元素的数组下标。
js
const arr = ['A', 'B', 'C']
arr.forEach((item, index) => {
console.log(index, item)
})
结果:
text
0 A
1 B
2 C
回调参数 3:array
调用 forEach() 的原数组本身。
js
const arr = [10, 20]
arr.forEach((item, index, array) => {
console.log(array === arr) // true
})
平时如果不需要第三个参数,完全不用写。
参数二:thisArg
可选,用于指定普通回调函数执行时的 this。现代代码大量使用箭头函数,而箭头函数不会使用这个参数来创建自己的 this,所以日常 Vue 业务里这个参数较少出现。
forEach() 的返回值
forEach() 返回 undefined。
这个结论非常重要。
它的职责是"遍历并执行操作",不是"生成一个新数组"。
错误理解:
js
const arr = [1, 2, 3]
const result = arr.forEach(item => item * 2)
console.log(result) // undefined
如果真正想得到 [2, 4, 6],应该用 map()。
是否修改原数组
forEach() 本身不会自动修改数组,但回调函数内部可以主动修改对象或数组内容。
例如:
js
const users = [
{ name: 'A', active: false },
{ name: 'B', active: false }
]
users.forEach(user => {
// 这里主动修改了每个对象的 active 属性。
user.active = true
})
所以不要把"forEach() 返回 undefined"误解成"它绝对不会造成任何数据变化"。真正是否变化取决于回调里做了什么。
使用场景
如果目标是"对每项执行某个副作用操作",例如打印、记录、调用另一个函数,forEach() 很合适。
js
const names = ['Vue', 'Vite', 'JavaScript']
names.forEach(name => {
console.log(`当前名称:${name}`)
})
7.8 filter():筛选出满足条件的元素
filter() 的核心思想可以概括为一句话:
遍历原数组,让回调函数决定当前元素"留下"还是"不要"。
基本语法
js
array.filter(callbackFn, thisArg)
常见写法:
js
const newArray = array.filter((item, index, array) => {
return 条件结果
})
回调参数
与 forEach() 类似:
item:当前元素;index:当前下标;array:原数组。
回调返回值
这里和 forEach() 的区别非常大。
filter() 会使用回调函数的返回结果来决定是否保留当前元素:
- 返回真值:保留;
- 返回假值:过滤掉。
通常最好直接返回明确的布尔条件。
filter() 自身的返回值
返回一个新数组。
新数组中只包含通过条件测试的元素。
是否修改原数组
不会直接修改原数组。
它会创建一个浅拷贝形式的筛选结果数组。
示例:保留偶数
js
const arr = [11, 22, 33, 44]
const evenNumbers = arr.filter(item => {
// 如果 item 能被 2 整除,表达式为 true,当前元素会被保留。
// 如果不能整除,表达式为 false,当前元素会被过滤掉。
return item % 2 === 0
})
console.log(evenNumbers) // [22, 44]
console.log(arr) // [11, 22, 33, 44]
简写:
js
const evenNumbers = arr.filter(item => item % 2 === 0)
示例:筛选价格不低于 100 的商品
js
const goods = [
{ id: 1, name: '鼠标', price: 69 },
{ id: 2, name: '键盘', price: 299 },
{ id: 3, name: '显示器', price: 1299 }
]
const expensiveGoods = goods.filter(item => {
// 只保留 price >= 100 的对象。
return item.price >= 100
})
console.log(expensiveGoods)
搜索过滤的思路
js
const keyword = 'Vue'
const articles = [
{ id: 1, title: 'Vue 组件基础' },
{ id: 2, title: 'JavaScript 数组方法' },
{ id: 3, title: 'Vue 工程化配置' }
]
const result = articles.filter(article => {
// includes() 返回布尔值,正好可以直接作为 filter() 回调的判断结果。
return article.title.includes(keyword)
})
这就是数组方法组合使用的典型方式:一个方法返回的结果,恰好可以成为另一个方法的判断依据。
7.9 map():把每个元素映射成一个新元素
map() 和 filter() 都会返回新数组,但它们解决的问题完全不同:
filter()决定"要不要这个元素";map()决定"这个元素在新数组里变成什么"。
基本语法
js
array.map(callbackFn, thisArg)
常见形式:
js
const newArray = array.map((item, index, array) => {
return 新值
})
回调参数
item:当前元素;index:当前下标;array:原数组。
回调返回值
每一次回调返回什么,那个结果就会成为新数组对应位置上的元素。
map() 自身返回值
返回一个新数组。
通常情况下,新数组长度与原数组长度相同,因为原数组每个已存在元素都会得到一个对应的映射结果。
是否修改原数组
map() 本身用于创建新数组,不直接改写原数组的结构。
但如果回调内部主动修改对象属性,仍然可能影响共享引用对象,因此写代码时不要把"返回新数组"简单理解成"内部所有对象也都被深复制了一份"。
示例:每个数字乘 2
js
const arr = [11, 22, 33, 44]
const doubled = arr.map(item => {
// 当前 item 乘 2 后作为新数组对应位置的值。
return item * 2
})
console.log(doubled) // [22, 44, 66, 88]
console.log(arr) // [11, 22, 33, 44]
简写:
js
const doubled = arr.map(item => item * 2)
示例:把字符串数组转换成对象数组
js
const names = ['Vue', 'Vite', 'JavaScript']
const options = names.map((name, index) => {
// 每个字符串被映射成一个对象。
return {
id: index + 1,
label: name
}
})
最终得到:
js
[
{ id: 1, label: 'Vue' },
{ id: 2, label: 'Vite' },
{ id: 3, label: 'JavaScript' }
]
直接返回对象时的箭头函数简写
js
const arr = [11, 22, 33, 44]
const result = arr.map((value, index) => ({
index,
value
}))
这里外层圆括号必须保留,因为箭头函数要隐式返回一个对象。
最终:
js
[
{ index: 0, value: 11 },
{ index: 1, value: 22 },
{ index: 2, value: 33 },
{ index: 3, value: 44 }
]
map() 最常见的误用
如果只是想遍历打印,不需要返回新数组,就不要为了"看起来高级"而使用 map()。
不推荐:
js
arr.map(item => {
console.log(item)
})
因为 map() 的语义是"转换并得到新数组"。纯遍历副作用更适合 forEach()。
7.10 every():判断是否每一个元素都满足条件
基本语法
js
array.every(callbackFn, thisArg)
回调参数
item:当前元素;index:当前下标;array:原数组。
回调返回值
回调应返回真值或假值,用来表示当前元素是否通过检查。
every() 自身返回值
返回布尔值。
逻辑是:
- 如果所有被检查的元素都通过,最终返回
true; - 一旦遇到一个不通过的元素,立即返回
false,后面的元素不再继续检查。
这种"遇到失败立即停止"的行为叫短路。
是否修改原数组
不会自动修改原数组。
示例:是否全部大于 10
js
const arr = [11, 22, 33, 44]
const result = arr.every(item => {
return item > 10
})
console.log(result) // true
如果其中有一个不满足:
js
const arr = [11, 5, 33, 44]
const result = arr.every(item => item > 10)
console.log(result) // false
示例:表单数据是否全部填写
js
const fields = ['张三', '13800000000', '陕西西安']
const allFilled = fields.every(value => {
// String(value).trim() 把值转为字符串后去掉首尾空白。
// 长度大于 0 表示不是空内容。
return String(value).trim().length > 0
})
空数组的特殊结果
js
[].every(() => false) // true
这看起来反直觉,但符合 every() 的逻辑定义:空数组里不存在任何一个"不满足条件的元素",因此结果为 true。
实际业务中,如果"必须至少有一项并且全部合格",就不能只写 every(),还要先判断长度:
js
const isValid = list.length > 0 && list.every(item => item.valid)
7.11 reduce():把整个数组汇总成一个最终结果
reduce() 往往是数组方法里最难读懂的一个,但它的核心其实很稳定:
拿一个"累计结果",从左到右不断吸收当前元素,最后得到一个最终值。
最终值不一定是数字,也可以是字符串、对象、数组等。
基本语法
js
array.reduce(callbackFn, initialValue)
常见形式:
js
const result = array.reduce((accumulator, currentValue, currentIndex, array) => {
return 新的累计结果
}, initialValue)
参数一:callbackFn
这是归并回调函数。
它最常用的四个参数如下。
回调参数 1:accumulator
累计值。
它不是固定等于"上一个数组元素"。更准确地说,它是:
上一次回调返回的结果。
第一次执行时,如果提供了 initialValue,那么 accumulator 就等于这个初始值。
回调参数 2:currentValue
当前正在处理的数组元素。
回调参数 3:currentIndex
当前元素下标。
回调参数 4:array
调用 reduce() 的原数组。
参数二:initialValue
累计计算的初始值。
虽然它在语法上可选,但业务代码中通常非常推荐明确提供,因为这样逻辑更稳定、更容易读,也能安全处理空数组。
reduce() 自身返回值
返回最终累计得到的那个单一结果。
是否修改原数组
reduce() 自身不会自动修改原数组。
例子一:数组求和
js
const arr = [11, 22, 33, 44]
const sum = arr.reduce((total, item) => {
// total 是累计值。
// 第一次执行时 total 为初始值 0。
// 每一次把当前 item 加到 total 上,并返回新的累计结果。
return total + item
}, 0)
console.log(sum) // 110
执行过程可以拆成:
text
初始 total = 0
第 1 次:0 + 11 -> 返回 11
第 2 次:11 + 22 -> 返回 33
第 3 次:33 + 33 -> 返回 66
第 4 次:66 + 44 -> 返回 110
最终 reduce() 返回 110
简写:
js
const sum = arr.reduce((total, item) => total + item, 0)
例子二:统计购物车总数量
js
const goodsList = [
{ id: 1, name: '篮球', num: 1 },
{ id: 2, name: '玩具', num: 3 },
{ id: 3, name: '书籍', num: 2 }
]
const totalNum = goodsList.reduce((total, item) => {
// total 保存之前已经累计出来的商品数量。
// item.num 是当前商品的数量。
// 返回两者相加的结果,让它成为下一轮的 total。
return total + item.num
}, 0)
console.log(totalNum) // 6
例子三:统计购物车总金额
js
const cart = [
{ id: 1, price: 99, count: 2 },
{ id: 2, price: 299, count: 1 },
{ id: 3, price: 15, count: 3 }
]
const totalPrice = cart.reduce((total, item) => {
// 当前商品小计 = 单价 × 数量。
const subtotal = item.price * item.count
// 把当前小计加到累计总价上。
return total + subtotal
}, 0)
console.log(totalPrice)
不传初始值时会发生什么
js
const arr = [10, 20, 30]
const sum = arr.reduce((total, item) => total + item)
没有提供 initialValue 时:
- 第一个元素
10会被当作初始累计值; - 回调从第二个元素
20开始执行。
这段代码仍然能得到 60。
但空数组会出问题:
js
const arr = []
// TypeError:空数组没有第一个元素可以充当初始累计值。
// arr.reduce((total, item) => total + item)
如果明确提供初始值:
js
const arr = []
const sum = arr.reduce((total, item) => total + item, 0)
console.log(sum) // 0
因此,对于求和、计数、构建对象等常见业务,主动写出合理的 initialValue 通常是更清晰、更稳妥的习惯。
7.12 这些数组方法应该怎么选择
看到需求时,可以先把需求翻译成一句自然语言:
- "我要在尾部加数据" →
push(); - "我要在头部加数据" →
unshift(); - "我要删最后一个" →
pop(); - "我要删第一个" →
shift(); - "我要从任意下标删除/插入/替换" →
splice(); - "我要判断一个基础值是否存在" →
includes(); - "我要对每项执行操作,不关心新数组" →
forEach(); - "我要保留满足条件的项" →
filter(); - "我要把每一项转换成新值" →
map(); - "我要判断是不是全部满足条件" →
every(); - "我要把整组数据汇总成一个结果" →
reduce()。
这比死背 API 更有效。数组方法真正的难点从来不是方法名,而是"把业务需求翻译成对应的数据操作"。
八、对象的重要方法:Object.keys() 如何把对象转换成可遍历的键数组
8.1 为什么对象不能像数组一样直接使用 map()
数组天然按下标组织数据,因此可以直接:
js
arr.map(...)
arr.filter(...)
arr.forEach(...)
普通对象本身没有这些数组实例方法。
例如:
js
const user = {
id: 10001,
name: 'Bit',
age: 9,
address: '西安'
}
可以用 for...in 遍历:
js
for (const key in user) {
console.log(key, user[key])
}
但在很多场景里,我们希望先把对象的属性名拿成一个真正的数组,再继续使用数组方法。Object.keys() 就是最常见的工具之一。
8.2 Object.keys() 语法
js
Object.keys(obj)
参数:obj
需要读取属性名的对象。
例如:
js
const user = {
id: 10001,
name: 'Bit',
age: 9
}
这里调用:
js
Object.keys(user)
参数 obj 就是 user。
返回值
返回一个字符串数组。
数组中包含这个对象自身的、可枚举的、字符串键属性名。
对于普通对象,可以先简单理解为:返回日常通过对象字面量写进去的那些属性名。
js
const user = {
id: 10001,
name: 'Bit',
age: 9
}
const keys = Object.keys(user)
console.log(keys)
// ['id', 'name', 'age']
是否修改原对象
不会。
Object.keys() 只是读取键名并生成数组。
8.3 配合 forEach() 遍历对象
js
const user = {
id: 10001,
name: 'Bit',
age: 9
}
Object.keys(user).forEach(key => {
// key 每次分别是 'id'、'name'、'age'。
// user[key] 使用动态属性访问获取对应属性值。
console.log(key, user[key])
})
这段代码把前面几个知识点串了起来:
Object.keys(user)先得到键数组;- 键数组可以调用
forEach(); key是变量,所以读取属性值必须使用user[key];- 每次就能拿到对应键和值。
8.4 配合 filter() 先筛选属性名,再处理属性值
假设对象为:
js
const user = {
id: 10001,
name: 'Bit',
age: 9,
address: '西安',
avatar: '/avatar.png'
}
希望只处理以字母 a 开头的属性:
js
Object
.keys(user)
.filter(key => {
// startsWith('a') 会判断字符串是否以 a 开头。
return key.startsWith('a')
})
.forEach(key => {
// key 最终可能是 age、address、avatar。
console.log(key, user[key])
})
也可以简写:
js
Object.keys(user)
.filter(key => key.startsWith('a'))
.forEach(key => {
console.log(key, user[key])
})
这里最值得理解的是"链式数据流":
text
对象
↓ Object.keys()
键数组
↓ filter()
符合条件的键数组
↓ forEach()
逐个处理
每一步都知道输入和返回值后,链式调用就不会显得神秘。
九、扩展语法 ...:复制与合并数组、对象
三个点 ... 在 JavaScript 中会根据位置表达不同含义。
本文最重要的两个场景是:
- 解构左边:剩余语法,用于"收集剩余值";
- 数组或对象字面量中:扩展语法,用于"展开已有内容"。
这里重点讲第二种。
9.1 为什么直接赋值不等于复制
数组是对象类型。
js
const arr1 = [11, 22, 33]
const arr2 = arr1
这并没有创建两份独立数组。
可以把它想象成:arr1 和 arr2 两个变量都指向同一个数组。
js
arr2.push(44)
console.log(arr2) // [11, 22, 33, 44]
console.log(arr1) // [11, 22, 33, 44]
为什么 arr1 也变了?
因为根本没有两份数组。
9.2 使用扩展语法复制数组
js
const arr1 = [11, 22, 33]
// ...arr1 会把 arr1 中的元素逐个展开,
// 外层 [] 再把这些值装进一个新的数组。
const arr2 = [...arr1]
arr2.push(44)
console.log(arr1) // [11, 22, 33]
console.log(arr2) // [11, 22, 33, 44]
...arr1 的"参数"和"返回值"怎么理解
扩展语法不是函数,所以没有函数参数与函数返回值。
它是一种语法操作:把可展开的数据内容展开到当前位置。
在数组字面量中:
js
[...arr1]
最终整个数组字面量表达式会产生一个新数组。
9.3 使用扩展语法复制对象
直接赋值同样会共享对象引用:
js
const user1 = {
name: 'Bit',
age: 9
}
const user2 = user1
user2.age = 10
console.log(user1.age) // 10
复制第一层属性:
js
const user1 = {
name: 'Bit',
age: 9
}
const user2 = {
...user1
}
user2.age = 10
console.log(user1.age) // 9
console.log(user2.age) // 10
9.4 非常重要:扩展语法是浅复制,不是深复制
假设对象内部还有嵌套对象:
js
const user1 = {
name: 'Bit',
profile: {
city: '西安'
}
}
const user2 = {
...user1
}
最外层 user2 的确是一个新对象,但内部的 profile 仍然与 user1.profile 指向同一个嵌套对象。
js
user2.profile.city = '北京'
console.log(user1.profile.city) // '北京'
因此:
[...arr]和{...obj}解决的是常见的浅复制问题,不等于把任意深度的数据全部递归复制。
在 Vue 数据处理里,这个概念非常重要,因为"看起来创建了新对象"并不代表所有嵌套层级都彼此独立。
9.5 合并数组
js
const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
const result = [...arr1, ...arr2]
console.log(result)
// [1, 2, 3, 4, 5, 6]
原理可以展开理解成:
text
先把 arr1 的 1、2、3 展开进去,
再把 arr2 的 4、5、6 展开进去,
最后由外层 [] 创建一个新数组。
也可以在中间插值:
js
const result = [...arr1, 100, ...arr2]
console.log(result)
// [1, 2, 3, 100, 4, 5, 6]
9.6 合并对象以及同名属性覆盖规则
js
const obj1 = {
name: 'Jack',
height: 176
}
const obj2 = {
height: 180,
age: 18
}
const result = {
...obj1,
...obj2
}
结果:
js
{
name: 'Jack',
height: 180,
age: 18
}
为什么 height 是 180?
因为对象属性发生同名覆盖时,后面的值覆盖前面的值。
js
const result = {
...obj1,
...obj2
}
顺序就是:
- 先放入
obj1; - 再放入
obj2; obj2.height和已有height同名,因此覆盖为180。
如果顺序反过来:
js
const result = {
...obj2,
...obj1
}
那最终 height 就是 176。
利用覆盖规则更新对象某个字段
js
const user = {
id: 1,
name: 'Jack',
age: 18
}
const updatedUser = {
...user,
age: 19
}
最终:
js
{
id: 1,
name: 'Jack',
age: 19
}
这是一种非常常见的数据更新表达方式:先保留旧属性,再用后面的新值覆盖指定字段。
十、JSON 序列化与反序列化:对象和字符串之间如何安全转换
10.1 先分清 JavaScript 对象和 JSON 文本
下面是一个 JavaScript 对象:
js
const user = {
id: 10001,
name: 'Bit',
age: 9
}
它是 JavaScript 运行时里的对象,可以通过:
js
user.name
user.age
直接读取属性。
而下面是一个字符串:
js
const jsonText = '{"id":10001,"name":"Bit","age":9}'
虽然字符串内部长得很像对象,但它依然只是字符串。
js
console.log(typeof user) // 'object'
console.log(typeof jsonText) // 'string'
所以"JSON"在日常开发中至少要区分两个概念:
- JavaScript 对象数据;
- JSON 格式的字符串文本。
Web Storage 只能直接保存字符串形式的键和值,因此对象、数组想放进去时,通常需要先序列化。
10.2 JSON.stringify():把 JavaScript 值转换成 JSON 字符串
基本语法
js
JSON.stringify(value)
完整语法还支持 replacer、space 参数:
js
JSON.stringify(value, replacer, space)
在 Vue 常规浏览器存储场景里,最常用的是第一个参数。
参数一:value
需要被转换成 JSON 字符串的 JavaScript 值。
常见的是对象或数组:
js
const user = {
id: 1,
name: 'Bit'
}
const text = JSON.stringify(user)
参数二:replacer
可选。
可以是函数或属性名数组,用于控制哪些值参与序列化、或者序列化前怎样转换。
这不是 Vue 前置阶段最常用的能力,但知道它存在即可。
参数三:space
可选。
主要控制输出 JSON 文本的缩进格式,适合调试或生成更易读的文本。
例如:
js
const user = {
id: 1,
name: 'Bit'
}
const prettyText = JSON.stringify(user, null, 2)
console.log(prettyText)
会得到带缩进的字符串。
返回值
正常情况下返回 JSON 字符串。
例如:
js
const user = {
id: 10001,
name: 'Bit',
age: 9
}
const jsonText = JSON.stringify(user)
console.log(jsonText)
// '{"id":10001,"name":"Bit","age":9}'
注意控制台显示时引号表现可能因环境而略有差异,但变量类型是字符串。
常见限制
不是所有 JavaScript 值都能原封不动变成 JSON。
例如对象属性中的 undefined、函数、Symbol 通常不会像普通字符串数字那样被序列化;BigInt 直接序列化会抛出错误,除非额外设计转换方案。
在本文涉及的普通 Vue 业务数据里,建议存储的是可 JSON 化的简单数据:字符串、数字、布尔值、null、普通数组和普通对象。
循环引用会报错
js
const obj = {}
obj.self = obj
// TypeError:对象形成了循环引用,普通 JSON.stringify() 无法直接序列化。
// JSON.stringify(obj)
常规接口数据和本地存储对象通常应该避免这种结构。
10.3 JSON.parse():把 JSON 字符串解析成 JavaScript 值
基本语法
js
JSON.parse(text)
完整形式:
js
JSON.parse(text, reviver)
参数一:text
需要解析的 JSON 字符串。
例如:
js
const text = '{"id":1,"name":"Bit"}'
参数二:reviver
可选。
解析完成过程中可以借助这个函数进一步转换值。普通 Vue 本地存储场景通常不需要它。
返回值
返回由 JSON 文本表示的 JavaScript 值。
例如:
js
const jsonText = '{"id":10001,"name":"Bit","age":9}'
const user = JSON.parse(jsonText)
console.log(user.name) // 'Bit'
console.log(user.age) // 9
非法 JSON 会抛出 SyntaxError
js
const wrongText = '{name: Bit}'
// JSON 的属性名和字符串值需要符合 JSON 语法。
// JSON.parse(wrongText) // SyntaxError
因此当 JSON 字符串来源不完全可信时,可以使用 try...catch 兜底。
js
const text = localStorage.getItem('user')
let user = null
if (text !== null) {
try {
// 只有 text 确实是合法 JSON 时才会成功。
user = JSON.parse(text)
} catch (error) {
// 如果字符串损坏或不是合法 JSON,进入这里。
console.error('用户数据解析失败', error)
}
}
10.4 序列化和反序列化的完整闭环
js
const user = {
id: 10001,
name: 'Bit',
age: 9
}
// 第一步:对象 -> JSON 字符串。
const jsonText = JSON.stringify(user)
// 第二步:JSON 字符串 -> JavaScript 对象。
const parsedUser = JSON.parse(jsonText)
console.log(parsedUser.name) // 'Bit'
可以把它记成:
text
JavaScript 对象 / 数组
↓ JSON.stringify()
JSON 字符串
↓ JSON.parse()
JavaScript 对象 / 数组
十一、Web Storage:localStorage 与 sessionStorage 的正确使用方式
11.1 Web Storage 到底保存在哪里
Web Storage 是浏览器提供的键值存储机制。最常用的两个入口:
js
localStorage
sessionStorage
它们都实现了 Storage 接口,常用方法一致:
js
setItem()
getItem()
removeItem()
clear()
本文重点放在前三个最常见方法。
11.2 localStorage 和 sessionStorage 的区别
localStorage
数据在浏览器会话结束后仍可继续存在,除非:
- 页面代码主动删除;
- 用户清理站点数据;
- 浏览器因策略或环境原因清理数据;
- 处于某些隐私/无痕环境并在会话结束后被清理。
它适合保存"不要求服务器托管、允许在浏览器端持续存在"的简单状态,例如界面偏好、某些非敏感筛选条件等。
sessionStorage
它以浏览器标签页会话为重要边界。同一标签页刷新页面时数据通常仍然存在,但关闭该标签页后,对应会话数据会被销毁。
可以简单记成:
text
localStorage -> 更持久
sessionStorage -> 当前标签页会话级
但不要把它误解成安全机制。二者都能被页面 JavaScript 访问,因此不适合保存应该对前端脚本不可见的敏感秘密。
11.3 存储容量不要简单背成"永远只有 5MB"
截至 2026 年的 MDN 文档说明,Web Storage 通常按每个源限制总计约 10 MiB,其中常见拆分是:
localStorage约 5 MiB;sessionStorage约 5 MiB。
达到额度时,写入可能抛出 QuotaExceededError。
这里真正应该掌握的是:
- Web Storage 容量有限;
- 不适合当大型数据库使用;
- 写入大量数据时应该考虑异常处理;
- 浏览器存储策略存在环境差异,不应该把"5MB"当成任何场景绝对不变的业务保证。
如果需要存储大量结构化数据,应该选择更合适的浏览器存储技术,而不是把所有东西都塞进 localStorage。
11.4 setItem():保存或更新一个键值
基本语法
js
storage.setItem(key, value)
例如:
js
localStorage.setItem('userName', 'Bit')
参数一:key
字符串形式的键名,用来标识这条数据。
例如:
text
'userName'
'userInfo'
'cart'
命名最好能够表达含义,避免所有项目都使用模糊的 'data'、'temp'。
参数二:value
要保存的值。
Web Storage 保存的是字符串键值。因此复杂对象应先通过 JSON.stringify() 转成字符串。
返回值
setItem() 没有业务数据返回值,正常执行时返回 undefined。
如果 key 已经存在
不是新增第二条同名数据,而是更新原 key 对应的值。
js
localStorage.setItem('theme', 'light')
localStorage.setItem('theme', 'dark')
console.log(localStorage.getItem('theme'))
// 'dark'
保存对象
错误思路:
js
const user = {
id: 1,
name: 'Bit'
}
// 不应该直接依赖对象被隐式转成字符串。
// localStorage.setItem('user', user)
正确:
js
const user = {
id: 1,
name: 'Bit'
}
// JSON.stringify() 先把对象转换成可保存的 JSON 字符串。
const text = JSON.stringify(user)
localStorage.setItem('user', text)
可以合并写:
js
localStorage.setItem('user', JSON.stringify(user))
11.5 getItem():根据 key 读取数据
基本语法
js
storage.getItem(key)
参数:key
要读取的键名。
返回值
如果 key 存在,返回对应字符串值。
如果 key 不存在,返回:
js
null
不是 undefined。
示例
js
localStorage.setItem('userName', 'Bit')
const userName = localStorage.getItem('userName')
console.log(userName) // 'Bit'
不存在:
js
const value = localStorage.getItem('not-exists')
console.log(value) // null
读取对象
存入对象时进行了序列化,取出后也需要反序列化:
js
const text = localStorage.getItem('user')
if (text !== null) {
// JSON.parse() 把 JSON 字符串恢复成 JavaScript 对象。
const user = JSON.parse(text)
console.log(user.name)
}
为什么先判断 null
因为 key 不存在时:
js
localStorage.getItem('user') // null
如果业务逻辑默认"没有存储就是没有用户数据",先判断会更清晰。
虽然 JSON.parse(null) 在 JavaScript 中有特殊转换行为,但业务代码不应该依赖这种不直观的隐式结果,明确检查缺失值可读性更高。
11.6 removeItem():删除指定 key
基本语法
js
storage.removeItem(key)
参数:key
需要删除的键名。
返回值
返回 undefined。
如果 key 本来就不存在,也不会因为"找不到这个键"而返回失败对象。
示例
js
localStorage.setItem('userName', 'Bit')
// 删除 userName。
localStorage.removeItem('userName')
console.log(localStorage.getItem('userName'))
// null
11.7 一套更稳妥的对象存取写法
js
const STORAGE_KEY = 'userInfo'
const user = {
id: 10001,
name: 'Bit',
age: 9
}
// ------------------------------
// 保存:对象 -> JSON 字符串 -> localStorage
// ------------------------------
try {
const text = JSON.stringify(user)
localStorage.setItem(STORAGE_KEY, text)
} catch (error) {
// JSON 序列化失败或浏览器存储写入失败时进入这里。
// 写入失败可能与存储额度、浏览器策略等因素有关。
console.error('保存失败', error)
}
// ------------------------------
// 读取:localStorage -> 字符串 -> 对象
// ------------------------------
let savedUser = null
const savedText = localStorage.getItem(STORAGE_KEY)
if (savedText !== null) {
try {
savedUser = JSON.parse(savedText)
} catch (error) {
// 如果保存内容被破坏或者格式不是合法 JSON,解析可能失败。
console.error('解析失败', error)
}
}
console.log(savedUser)
这段代码体现了一条非常重要的完整链路:
text
对象
↓ stringify
字符串
↓ setItem
浏览器存储
↓ getItem
字符串
↓ parse
对象
十二、Promise:如何描述一个"未来才有结果"的异步操作
12.1 为什么异步代码会出现嵌套
假设需求是:
- 2 秒后输出 1;
- 再过 1 秒输出 2;
- 再过 1 秒输出 3。
使用层层嵌套的定时器可以实现:
js
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 1000)
}, 1000)
}, 2000)
代码本身不是"不能运行",问题在于依赖关系一多,就会不断向右嵌套:
text
完成 A
-> 完成 B
-> 完成 C
-> 完成 D
这会让成功、失败、后续处理越来越难阅读。
Promise 的价值不只是"消除回调地狱"这一句口号,更准确地说,它提供了一个标准对象,用来表示:
某个异步操作未来最终会成功得到值,或者失败得到原因。
12.2 Promise 的三种状态
Promise 有三个重要状态:
pending
进行中、尚未确定最终结果。
fulfilled
异步操作成功完成,并且有一个成功值。
rejected
异步操作失败,并且有一个失败原因。
状态变化只能是:
text
pending -> fulfilled
或者:
text
pending -> rejected
一旦已经确定为成功或失败,就不会再次改变为另一个最终状态。
12.3 new Promise() 构造器
基本语法
js
const promise = new Promise((resolve, reject) => {
// 异步操作
})
Promise 构造器参数
new Promise() 接收一个函数,这个函数通常叫 executor,执行器函数。
js
(resolve, reject) => {
// ...
}
执行器函数会接收到两个函数参数:resolve 和 reject。
resolve(value)
用于把当前 Promise 标记为成功,并提供成功结果。
例如:
js
resolve('ok')
表示异步操作成功,成功值是字符串 'ok'。
reject(reason)
用于把当前 Promise 标记为失败,并提供失败原因。
例如:
js
reject(new Error('请求失败'))
业务代码里通常推荐使用 Error 对象表达失败原因,这样错误堆栈和错误处理更清晰。
new Promise() 的返回值
构造表达式会创建并返回一个 Promise 实例。
js
const promise = new Promise((resolve, reject) => {
// ...
})
此时 promise 就是这个 Promise 对象。
12.4 一个正确的延迟 Promise
js
function delay(duration, value) {
// 返回一个 Promise,让调用方能够知道"延迟操作什么时候完成"。
return new Promise(resolve => {
// setTimeout() 在 duration 毫秒后执行回调。
setTimeout(() => {
// 到达指定时间后,把 Promise 标记为 fulfilled。
// value 会成为这个 Promise 的成功结果。
resolve(value)
}, duration)
})
}
delay(duration, value) 参数
duration:等待时长,单位毫秒。例如 2000 代表大约 2 秒后触发定时器回调。
value:延迟完成后希望交给调用方的成功值。
delay(duration, value) 返回值
返回一个 Promise 实例。
这个 Promise 会先处于 pending;定时器触发并执行 resolve(value) 后变成 fulfilled。
调用:
js
const p = delay(2000, 1)
此时 p 不是数字 1,而是一个"未来会成功得到 1"的 Promise。
12.5 then():注册成功处理和失败处理
基本语法
js
promise.then(onFulfilled, onRejected)
第二个失败回调可以省略,失败通常更推荐统一使用 catch() 处理。
参数一:onFulfilled
Promise 成功后调用的函数。
它会接收到 Promise 的成功值。
js
const p = delay(1000, '完成')
p.then(value => {
// value 就是 resolve('完成') 传出的成功值。
console.log(value)
})
参数二:onRejected
可选。
Promise 失败后执行的函数,会接收到失败原因。
js
promise.then(
value => {
console.log('成功', value)
},
error => {
console.error('失败', error)
}
)
then() 的返回值
这是 Promise 链能够成立的关键:
then()会返回一个新的 Promise。
如果成功回调里 return 一个普通值,这个值会成为下一段链的成功结果。
js
Promise.resolve(10)
.then(value => {
return value * 2
})
.then(value => {
console.log(value) // 20
})
如果回调返回另一个 Promise,下一段会等待这个 Promise 最终完成。
这就是连续异步步骤可以写成链式结构的基础。
12.6 使用 Promise 链改写连续延迟
js
delay(2000, 1)
.then(n1 => {
// 第一个 Promise 完成后打印 1。
console.log(n1)
// 返回第二个 Promise。
// 因为 then() 会采用这个 Promise 的最终结果,
// 所以下一个 then() 会等它完成。
return delay(1000, 2)
})
.then(n2 => {
console.log(n2)
// 继续返回下一段异步操作。
return delay(1000, 3)
})
.then(n3 => {
console.log(n3)
})
这段代码比嵌套回调更接近"按步骤从上到下阅读"。
最核心的规则是:链中需要等待的下一段异步操作,记得 return 出去。
如果忘了:
js
.then(() => {
// 没有 return,下一段链不会等待这个 Promise。
delay(1000, 2)
})
顺序语义就可能和预期不一致。
12.7 catch():统一处理 Promise 链错误
基本语法
js
promise.catch(onRejected)
参数:onRejected
失败处理函数,接收错误原因。
返回值
catch() 也会返回一个新的 Promise,因此后面仍然可以继续链式调用。
示例
js
function requestData() {
return new Promise((resolve, reject) => {
const success = false
if (success) {
resolve({ code: 0, data: [] })
} else {
reject(new Error('数据获取失败'))
}
})
}
requestData()
.then(result => {
console.log(result)
})
.catch(error => {
console.error(error.message)
})
12.8 finally():无论成功失败都执行收尾逻辑
基本语法
js
promise.finally(onFinally)
参数:onFinally
无论 Promise 最终 fulfilled 还是 rejected,都会执行的回调。
它通常用于"无论成功失败都必须做"的收尾工作,例如关闭加载状态。
返回值
finally() 返回一个 Promise,可以继续参与链式处理。
示例
js
requestData()
.then(result => {
console.log('成功', result)
})
.catch(error => {
console.error('失败', error)
})
.finally(() => {
console.log('本次异步流程结束')
})
十三、async / await:用接近同步阅读顺序的方式组织 Promise
13.1 async 函数最重要的事实:它一定返回 Promise
js
async function getNumber() {
return 100
}
很多人第一眼会以为:
js
getNumber() // 100
实际上不是。
async 函数调用后返回 Promise。
js
const result = getNumber()
console.log(result)
// Promise
这个 Promise 最终会 fulfilled,成功值为 100。
等价理解:
js
function getNumber() {
return Promise.resolve(100)
}
async function 的参数
它和普通函数一样,可以声明任意业务参数:
js
async function getUser(id) {
// ...
}
这里 id 仍然只是普通函数参数。
async function 的返回值
无论函数体写 return 100、return {},还是不显式返回,调用 async 函数得到的都是 Promise。
如果函数中抛出错误,返回的 Promise 会变成 rejected。
13.2 await 到底在等待什么
await 通常写在 async 函数内部:
js
async function run() {
const value = await delay(1000, 10)
console.log(value)
}
await 后面的值
最典型的是 Promise:
js
await promise
当 Promise 成功时,await 表达式得到它的成功值。
例如:
js
const value = await delay(1000, 10)
delay() 返回 Promise;这个 Promise 最终 resolve(10);因此 await 表达式最终得到 10,赋值给 value。
await 的结果
如果等待的 Promise fulfilled:得到成功值。
如果等待的 Promise rejected:会在当前 async 函数执行路径中抛出异常,可以用 try...catch 捕获。
13.3 用 async/await 改写连续异步步骤
前面的 Promise 链:
js
delay(2000, 1)
.then(n1 => {
console.log(n1)
return delay(1000, 2)
})
.then(n2 => {
console.log(n2)
return delay(1000, 3)
})
.then(n3 => {
console.log(n3)
})
可以改成:
js
async function logNumbers() {
// 等第一个 Promise 完成,得到 resolve 传出的 1。
const n1 = await delay(2000, 1)
console.log(n1)
// 第一段完成后才走到这里,再等待第二个 Promise。
const n2 = await delay(1000, 2)
console.log(n2)
// 再等待第三个 Promise。
const n3 = await delay(1000, 3)
console.log(n3)
}
// async 函数调用会立刻返回 Promise,
// 函数内部则按照 await 的依赖顺序继续推进。
logNumbers()
这里所谓"像同步代码一样写",指的是阅读结构从上到下更直观,并不是说 JavaScript 把异步任务真的变成了阻塞整个页面的同步任务。
13.4 try...catch:await 最常见的错误处理方式
js
async function loadData() {
try {
// 如果 requestData() 返回的 Promise 成功,
// result 得到成功值。
const result = await requestData()
console.log('成功', result)
} catch (error) {
// 如果等待的 Promise rejected,错误会进入这里。
console.error('失败', error)
}
}
这套结构特别适合一个函数内部有多步连续异步操作时统一处理异常。
try 部分
放可能抛错的同步代码,以及可能 rejected 的 await。
catch (error)
error 接收错误对象或 rejection 原因。
finally
如果需要无论成功失败都执行收尾,可以继续加:
js
async function loadData() {
try {
const result = await requestData()
console.log(result)
} catch (error) {
console.error(error)
} finally {
console.log('流程结束')
}
}
13.5 Vue 3 中为什么经常看到 async 箭头函数
在 <script setup> 中,函数经常用变量绑定形式定义:
vue
<script setup>
const loadData = async () => {
try {
// 假设 getData() 返回 Promise。
const result = await getData()
console.log(result)
} catch (error) {
console.error('加载失败', error)
}
}
</script>
这里其实只是把两种 JavaScript 语法组合起来:
text
箭头函数 + async/await
没有新的"Vue 专属异步语法"。
const loadData = async () => {} 的调用结果仍然是 Promise。
13.6 不要机械地把所有异步操作都串行 await
如果第二个任务必须依赖第一个任务的结果,那么顺序等待是正确的:
js
const user = await getUser()
const orders = await getOrders(user.id)
第二步需要 user.id,所以必须等第一步。
如果两个任务彼此独立,连续等待会让总时间变成两段时间叠加。此时可以先同时启动,再统一等待。
js
const userPromise = getUser()
const categoryPromise = getCategories()
const user = await userPromise
const categories = await categoryPromise
更常见的完整写法会结合 Promise.all(),但为了保持本文边界,这里只建立一个基本意识:await 表达的是依赖顺序,不应该因为写法舒服就把所有异步任务机械串行化。
十四、ES Module:现代 Vue 项目为什么到处都是 import 和 export
14.1 模块化到底解决什么问题
如果所有代码都写在一个文件里,项目一大就会出现:
- 函数越来越多;
- 变量名字越来越容易冲突;
- 工具代码无法清晰复用;
- 一个文件同时承担多个职责;
- 修改某块功能时很难快速定位。
模块化的核心思路是:
按职责把代码拆到不同文件中,每个文件只暴露其他模块真正需要使用的内容。
例如:
text
src/
├─ main.js
├─ utils/
│ ├─ math.js
│ └─ storage.js
└─ services/
└─ goods.js
现代 Vue + Vite 项目天然建立在 ES Module 体系上,所以:
js
import ... from '...'
export ...
是最基础的工程语法。
14.2 默认导出 export default
假设文件:
text
src/utils/min.js
内容:
js
// 定义一个求较小值的函数,并把它作为当前模块的默认导出。
export default function min(m, n) {
// 如果 m > n,返回 n;否则返回 m。
// 当 m === n 时也会走冒号后面的 m,但二者本来相等,因此结果正确。
return m > n ? n : m
}
min(m, n) 参数
m:第一个比较值。
n:第二个比较值。
在这个函数设计中,通常预期传入可比较的数值。
min(m, n) 返回值
返回 m 与 n 中较小的那个值。
例如:
js
min(12, 45) // 12
export default 的规则
一个模块最多只能有一个默认导出。
因为"默认"本身就表示:这个模块最主要、默认暴露的是这一份内容。
14.3 默认导入
在另一个文件中:
js
import min from './utils/min.js'
console.log(min(12, 45)) // 12
import min from ... 的含义
这里的 min 是当前文件接收默认导出时使用的本地名称。
默认导入的一个特点是:本地名字可以自己决定。
例如默认导出不变:
js
export default function min(m, n) {
return m > n ? n : m
}
导入时也可以写:
js
import getMin from './utils/min.js'
因为默认导入不是靠 {} 中的导出名称严格匹配。
但工程实践中应该使用清晰、符合功能含义的命名,避免随意改成难以理解的变量名。
14.4 命名导出:一个模块导出多个成员
文件:
text
src/utils/math.js
js
// 命名导出 add。
export function add(x, y) {
return x + y
}
// 命名导出 sub。
export function sub(x, y) {
return x - y
}
add(x, y) 参数
x:第一个参与加法的值。
y:第二个参与加法的值。
add(x, y) 返回值
返回 x + y 的结果。
sub(x, y) 参数
x:被减数。
y:减数。
sub(x, y) 返回值
返回 x - y 的结果。
命名导出的数量
一个模块可以存在多个命名导出。
除了声明时直接加 export:
js
export function add() {}
export function sub() {}
也可以先定义,最后集中导出:
js
function add(x, y) {
return x + y
}
function sub(x, y) {
return x - y
}
export { add, sub }
14.5 命名导入
js
import { add, sub } from './utils/math.js'
console.log(add(33, 18)) // 51
console.log(sub(33, 18)) // 15
命名导入使用大括号:
js
{ add, sub }
这里默认需要和模块实际导出的名字匹配。
如果只需要 add:
js
import { add } from './utils/math.js'
不需要为了"完整"把不用的 sub 也导进来。
14.6 命名导入重命名
如果当前文件已经有一个同名变量,可以使用 as:
js
import { add as addNumber } from './utils/math.js'
console.log(addNumber(1, 2))
含义:
从模块中读取名为
add的导出,但在当前文件里把它绑定为addNumber。
14.7 默认导出和命名导出可以怎么区分
最直观的对照:
默认导出
js
export default function min() {}
导入:
js
import min from './min.js'
没有大括号。
命名导出
js
export function add() {}
export function sub() {}
导入:
js
import { add, sub } from './math.js'
有大括号,并按名字匹配。
14.8 Node.js 中的 ES Module 标记
如果是单独用 Node.js 运行 .js 模块,而不是在 Vite 项目里由工具链管理模块,可以通过 package.json 明确声明:
json
{
"type": "module"
}
现代 Node.js 官方文档把 ECMAScript modules 作为稳定支持的模块体系之一。package.json 中的:
json
"type": "module"
可以明确告诉 Node.js:这个包作用域内相关 .js 文件按 ES Module 解释。
Node.js 也支持 .mjs 等明确标记方式。
不过在 Vue + Vite 工程里,不需要为了"让 Vue 支持 import"而自己到处添加这种配置;项目模板和构建工具已经为现代模块化工作流准备好了必要环境。
14.9 浏览器原生 ES Module 与 type="module"
现代浏览器也能原生执行 ES Module:
html
<script type="module" src="./main.js"></script>
或者:
html
<script type="module">
import { add } from './math.js'
console.log(add(1, 2))
</script>
Vue 官方文档在不使用构建步骤的示例中也会采用 <script type="module"> 加 ES Module 形式导入 Vue。
不过真正的 Vue 单文件组件开发通常通过 Vite 开发服务器运行,不会靠双击本地 HTML 文件直接启动整个工程。
十五、把全部知识串起来:一段 Vue 3 风格的数据处理代码到底包含了多少 JavaScript
下面故意写一段不涉及高级 Vue API、只体现 JavaScript 前置能力的 <script setup> 示例:
vue
<script setup>
// ------------------------------
// 1. const:声明不需要重新赋值的数组引用。
// ------------------------------
const goodsList = [
{ id: 1, name: '机械键盘', price: 299, count: 1, category: '外设' },
{ id: 2, name: '无线鼠标', price: 99, count: 2, category: '外设' },
{ id: 3, name: 'Vue 3 图书', price: 79, count: 1, category: '图书' }
]
// ------------------------------
// 2. 箭头函数 + filter():
// 根据价格筛选商品。
//
// 参数 minPrice:最低价格。
// 返回值:一个新的商品数组,只包含 price >= minPrice 的元素。
// ------------------------------
const filterByPrice = minPrice => {
return goodsList.filter(goods => {
return goods.price >= minPrice
})
}
// ------------------------------
// 3. map():
// 把商品对象映射成更适合展示的简单对象。
//
// 参数 list:商品数组。
// 返回值:新数组;每个元素包含 id、label、subtotal。
// ------------------------------
const createDisplayList = list => {
return list.map(goods => ({
id: goods.id,
// 模板字符串:把名称和单价组成一段文字。
label: `${goods.name} - ¥${goods.price}`,
// 当前商品小计。
subtotal: goods.price * goods.count
}))
}
// ------------------------------
// 4. reduce():
// 计算整个购物列表总金额。
//
// 参数 list:商品数组。
// 返回值:number,总金额。
// ------------------------------
const getTotalPrice = list => {
return list.reduce((total, goods) => {
return total + goods.price * goods.count
}, 0)
}
// ------------------------------
// 5. localStorage + JSON.stringify():
// 把商品数组保存到浏览器。
//
// 参数 list:需要保存的数组。
// 返回值:没有业务返回值;函数完成保存动作。
// ------------------------------
const saveGoods = list => {
const text = JSON.stringify(list)
localStorage.setItem('goodsList', text)
}
// ------------------------------
// 6. localStorage + JSON.parse():
// 从浏览器读取商品数组。
//
// 返回值:
// - 如果没有保存数据,返回空数组 [];
// - 如果存在合法 JSON,返回解析后的数组。
// ------------------------------
const loadGoods = () => {
const text = localStorage.getItem('goodsList')
// 如果 key 不存在,getItem() 返回 null。
if (text === null) {
return []
}
return JSON.parse(text)
}
// ------------------------------
// 7. 实际调用。
// ------------------------------
const filtered = filterByPrice(100)
const displayList = createDisplayList(filtered)
const totalPrice = getTotalPrice(goodsList)
console.log(displayList)
console.log(`购物车总金额:¥${totalPrice}`)
saveGoods(goodsList)
console.log(loadGoods())
</script>
这段代码没有依赖复杂框架知识,却已经把大量 Vue 3 日常 JavaScript 语法串了起来:
text
const
对象数组
箭头函数
filter()
map()
reduce()
模板字符串
对象简写/对象字面量
JSON.stringify()
JSON.parse()
localStorage.setItem()
localStorage.getItem()
当这些东西都能一眼看懂以后,再进入 Vue 3 的响应式 API、计算属性、事件、组件通信等主题时,注意力才不会被 JavaScript 语法本身不断打断。
十六、最容易写错或理解错的关键点总整理
16.1 const 不是"对象内部永远不能变"
错误理解:
text
const = 完全不可变
正确理解:
text
const = 当前变量绑定不能重新赋值
对象属性、数组内容依然可以修改。
16.2 动态属性名必须优先想到中括号
js
const key = 'name'
obj[key]
读取的是变量 key 指向的属性名。
js
obj.key
读取的是字面属性 'key'。
两者不是一回事。
16.3 对象解构看名字,数组解构看位置
js
const [a, b] = arr
按位置。
js
const { name, age } = user
按属性名。
16.4 箭头函数省略大括号时才会隐式返回表达式
js
const add = (a, b) => a + b
会返回结果。
js
const add = (a, b) => {
a + b
}
不会自动返回,结果是 undefined。
16.5 箭头函数直接返回对象必须包圆括号
js
const create = () => ({
id: 1
})
16.6 push() 返回的不是数组
js
const length = arr.push(value)
length 是新长度。
16.7 pop() / shift() 返回被删除元素
js
const last = arr.pop()
const first = arr.shift()
16.8 splice() 返回"被删除元素组成的数组"
js
const removed = arr.splice(1, 1)
即使只删一项,removed 仍然是数组。
16.9 forEach() 不负责生成新数组
js
const result = arr.forEach(...)
result 是 undefined。
16.10 filter() 是筛选,map() 是转换
js
arr.filter(item => 条件)
决定保留谁。
js
arr.map(item => 新值)
决定每一项变成什么。
16.11 every() 空数组返回 true
如果业务要求"必须至少一项并且全部通过":
js
list.length > 0 && list.every(item => 条件)
16.12 reduce() 最好明确初始值
求和:
js
arr.reduce((sum, item) => sum + item, 0)
这样空数组也能得到合理结果 0。
16.13 扩展语法复制是浅复制
js
const copy = { ...source }
只保证最外层是新对象,不代表所有嵌套对象都独立。
16.14 Web Storage 保存复杂数据要经过 JSON
存:
js
localStorage.setItem('key', JSON.stringify(data))
取:
js
const text = localStorage.getItem('key')
const data = text === null ? null : JSON.parse(text)
16.15 async 函数总是返回 Promise
js
async function fn() {
return 100
}
调用结果不是直接的 100,而是最终成功值为 100 的 Promise。
16.16 await 等待的是 Promise 的最终结果,而不是"让整个 JavaScript 世界停止"
await 影响的是当前异步函数后续执行顺序,让当前函数在这个依赖点等待结果;它不是把浏览器页面线程粗暴地同步阻塞住。
16.17 默认导入不加大括号,命名导入要加大括号
默认:
js
import min from './min.js'
命名:
js
import { add, sub } from './math.js'
十七、方法速查表:参数、返回值、是否修改原数据
| API / 语法 | 主要参数 | 返回值 / 结果 | 是否修改原数据 | 核心用途 |
|---|---|---|---|---|
push(...items) |
一个或多个新元素 | 新长度 | 是 | 尾部添加 |
unshift(...items) |
一个或多个新元素 | 新长度 | 是 | 头部添加 |
pop() |
无 | 被删除的最后元素;空数组为 undefined |
是 | 尾部删除 |
shift() |
无 | 被删除的第一元素;空数组为 undefined |
是 | 头部删除 |
splice(start, deleteCount, ...items) |
起始下标、删除数量、可选插入元素 | 被删除元素组成的新数组 | 是 | 任意位置删/插/换 |
includes(value, fromIndex?) |
目标值、可选起始下标 | boolean |
否 | 判断是否包含 |
forEach(callback, thisArg?) |
回调函数 | undefined |
本身不自动修改 | 遍历执行操作 |
filter(callback, thisArg?) |
条件回调 | 筛选后的新数组 | 否 | 条件筛选 |
map(callback, thisArg?) |
映射回调 | 映射后的新数组 | 否 | 数据转换 |
every(callback, thisArg?) |
检测回调 | boolean |
否 | 判断是否全部满足 |
reduce(callback, initialValue?) |
累计回调、初始值 | 最终累计结果 | 否 | 汇总计算 |
Object.keys(obj) |
对象 | 自有可枚举字符串键数组 | 否 | 获取属性名 |
JSON.stringify(value, replacer?, space?) |
要序列化的值 | JSON 字符串;部分特殊输入存在限制 | 否 | 对象/数组转字符串 |
JSON.parse(text, reviver?) |
JSON 字符串 | 解析后的 JavaScript 值 | 否 | JSON 字符串转数据 |
storage.setItem(key, value) |
键、字符串值 | undefined |
修改 Storage | 保存数据 |
storage.getItem(key) |
键 | 字符串或 null |
否 | 读取数据 |
storage.removeItem(key) |
键 | undefined |
修改 Storage | 删除指定数据 |
new Promise(executor) |
执行器函数 | Promise 实例 | --- | 表示异步最终结果 |
promise.then(success?, failure?) |
成功/失败处理函数 | 新 Promise | --- | 链式处理结果 |
promise.catch(failure) |
失败处理函数 | 新 Promise | --- | 捕获 Promise 错误 |
promise.finally(callback) |
收尾函数 | Promise | --- | 成功失败都执行 |
async function |
与普通函数相同 | 总是 Promise | --- | 声明异步函数 |
await expression |
通常为 Promise 表达式 | fulfilled 值;rejected 时抛出异常 | --- | 等待异步依赖 |
Object/Array ...spread |
要展开的数据 | 由外层表达式决定 | 浅复制本身不改源数据 | 复制/合并 |
export default |
默认导出内容 | 模块导出关系 | --- | 默认导出 |
export { name } |
一个或多个命名成员 | 模块导出关系 | --- | 命名导出 |
import name from |
模块路径 | 创建本地绑定 | --- | 默认导入 |
import { name } from |
模块路径 | 创建本地绑定 | --- | 命名导入 |
十八、学习这些内容时最有效的检验方式
真正掌握不是"看过一遍",而是看到代码就能回答下面的问题。
例如:
js
const result = goods
.filter(item => item.price >= 100)
.map(item => ({
id: item.id,
name: item.name
}))
应该能立刻解释:
goods是数组;filter()的回调参数item是当前商品;item.price >= 100返回布尔值;filter()返回价格至少 100 的新数组;- 新数组继续调用
map(); map()对每个商品返回一个新对象;- 箭头函数用
({})是为了隐式返回对象; - 最终
result是一个新数组; result中每项只保留id和name;- 这段链式调用本身没有使用
push()、splice()等方法去修改goods的数组结构。
再例如:
js
const saved = localStorage.getItem('cart')
const cart = saved === null ? [] : JSON.parse(saved)
应该能解释:
getItem('cart')参数是 key;- key 不存在返回
null; - 三元表达式判断是否存在保存内容;
- 不存在就使用空数组;
- 存在就用
JSON.parse()把 JSON 字符串恢复成 JavaScript 数据; - 最终
cart不是 Promise,也不是 Storage 对象,而是业务数组。
再例如:
js
const load = async () => {
const result = await requestData()
return result.data
}
应该能解释:
load是箭头函数;async让它成为异步函数;requestData()预期返回 Promise;await等待 Promise 成功结果;result得到成功值;return result.data看似返回普通值;- 但因为
load是async函数,所以load()最终依然返回 Promise; - 这个 Promise 的成功值是
result.data。
能把这些话自己完整讲出来,才说明语法、参数、返回值和执行关系真正串起来了。
十九、结语:Vue 3 代码是否"难懂",往往先取决于 JavaScript 是否扎实
Vue 3 的很多现代写法之所以一开始显得紧凑,是因为它大量建立在现代 JavaScript 已经提供的表达能力上。
一段看起来只有几行的代码:
js
const total = cart
.filter(item => item.checked)
.reduce((sum, item) => sum + item.price * item.count, 0)
实际上同时包含:
const;- 数组方法链;
filter();- 箭头函数;
- 对象属性访问;
reduce();- 累计值;
- 回调返回值;
- 初始值;
- 最终返回结果。
如果每一层都理解,这两行代码非常清楚:先筛选已勾选商品,再累计总价。
如果只认识 Vue 指令,却不清楚 filter() 和 reduce() 的参数、返回值与数据流,那么同样的代码就会变成一团"框架魔法"。
因此,真正高效的路线不是机械背 Vue API,而是先把本文这组 JavaScript 核心能力掌握到能够阅读、解释、修改和组合使用:
text
let / const
模板字符串
对象取值与简写
数组 / 对象解构
箭头函数
数组增删改查与遍历加工
Object.keys()
扩展语法
JSON.stringify() / JSON.parse()
localStorage / sessionStorage
Promise / then / catch / finally
async / await
ES Module import / export
这些内容彼此并不是孤立知识点。真实 Vue 代码往往会把它们组合在一起。学习时最重要的也不是追求一次记住所有细枝末节,而是建立稳定的数据流思维:
现在手里的数据是什么类型?调用这个方法需要什么参数?回调每次拿到什么?回调应该返回什么?整个方法最终又返回什么?原数据有没有被修改?下一步拿到的结果是什么?
只要每写一段代码都能回答这几个问题,很多"看起来复杂"的 Vue 业务代码都会迅速变得透明。
二十、从真实开发步骤理解这些语法:不要只记"API 长什么样"
到这里,单个语法点已经讲得足够细,但真正写 Vue 代码时,困难往往不是"不知道 map() 的名字",而是拿到一个具体需求后,不知道应该先做哪一步、后做哪一步。下面把几个非常典型的数据处理过程完整拆开,只使用前面已经讲过的知识,不引入额外体系。
20.1 场景一:从商品列表中筛选、转换并计算结果
假设现在有一组商品数据。页面只需要显示价格不低于 100 元的商品,并且展示时只保留商品名称、价格和小计。与此同时,还要计算这些商品的总金额。
遇到这类需求,第一反应不应该是"我要写几个数组方法",而应该先把业务动作拆成顺序明确的中文步骤:
第一步,原始数据是一整个商品数组。第二步,价格不足 100 元的商品不需要参与后续展示,因此先筛选。第三步,筛选后的对象字段太多,而页面只需要少数字段,因此再做结构转换。第四步,总金额不是一个数组,而是一个数字,因此最后需要做汇总。
翻译成数组方法就是:
text
原数组
↓ filter()
符合价格条件的新数组
↓ map()
适合展示的新数组
↓ reduce()
最终汇总数字
这里最关键的是不要把三个方法混为一谈。
filter() 的任务不是"修改商品",而是决定哪些商品还能继续存在于结果数组中。因此它的回调最重要的是返回条件结果。只要回调对某个商品返回真值,这个商品就会被放进筛选结果;返回假值,则不会进入结果数组。
map() 的任务不是"判断保不保留",而是决定每个商品在新数组里变成什么结构。因此它的回调必须返回"新的元素"。如果返回对象,那么新数组每一项就是对象;如果返回字符串,那么新数组每一项就是字符串。
reduce() 的任务也完全不同。它不再关心"新数组的每一项长什么样",而是把整组数据汇成一个最终值。在计算金额时,累计值应该是数字,所以初始值最自然就是 0。
一个稳定的思考顺序是:先问自己"最终要的是数组还是单个值"。如果最终还要一组数据,优先考虑 filter() 或 map();如果最终只要一个统计数字,则考虑 reduce()。
20.2 场景二:删除列表中的指定项,为什么先需要知道位置
在页面列表中删除一项时,如果已经知道数组下标,splice(index, 1) 非常直接。这里两个参数都有明确业务意义:第一个参数是从哪里开始,第二个参数是删除多少个。
很多代码难读,是因为只看到了:
js
list.splice(index, 1)
却没有把它翻译成中文:
从
index对应的位置开始,只删除一个元素。
只要完成这一步翻译,就会发现 1 并不是神秘数字,而是"删除一个"的业务数量。
如果需求变成"从这个位置删除三个",第二个参数自然就是 3;如果需求变成"在这个位置插入数据但不删除",第二个参数自然就是 0。
所以学习参数时,不要只背顺序,还要给每个参数建立语义:
text
splice(从哪里开始, 删除几个, 要插入什么...)
这比机械背 start, deleteCount, item1... 更容易真正用于代码。
同时还要始终记得,splice() 会直接修改原数组。只要调用成功,原数组的内容就已经改变。它的返回值只是"被删除的那部分",不是修改完成后的整个数组。
20.3 场景三:为什么搜索列表常见 filter() + includes()
假设页面有文章数组,并且有一个搜索词。业务语言通常是:
保留标题中包含搜索词的文章。
这句话里其实已经隐藏了两个动作。
"包含搜索词"是一个判断问题,所以需要一个能产生布尔值的操作,includes() 正好返回 true 或 false。
"保留满足条件的文章"是筛选问题,所以外层使用 filter()。
于是整个逻辑自然形成:
text
每拿到一篇文章
↓
检查 article.title.includes(keyword)
↓
得到 true / false
↓
交给 filter() 决定保留还是过滤
这就是为什么数组方法经常连在一起出现:一个方法返回的类型,恰好符合另一个方法所需要的条件。
理解数据类型,比背组合套路更重要。includes() 返回布尔值,而 filter() 回调恰好需要一个真/假判断,所以二者组合非常自然。
20.4 场景四:为什么 map() 常用于"接口数据转页面数据"
真实业务中的原始对象往往字段很多:编号、名称、价格、库存、创建时间、后端状态码、内部标记等。但页面组件可能只关心其中三四项。
这时最自然的思路不是原地删除原对象属性,而是"基于原数据创建一份更适合当前页面消费的新数据"。这正是 map() 的语义。
例如原数据每项包含十个字段,页面只需要:
text
id
label
value
那么 map() 回调每执行一次,就返回一个只包含这三个字段的新对象。最终得到的新数组仍然与原数组一一对应,但结构更加适合当前用途。
这类写法清晰的原因是数据方向明确:
text
后端结构 -> 页面结构
而不是在原对象上边删边改,让同一份数据同时承担多个结构含义。
当然,map() 只保证新数组容器是新的。如果新对象内部继续直接引用原对象中的嵌套对象,内部引用仍可能共享。这和前面讲过的浅复制概念完全一致。
20.5 场景五:Object.keys() 为什么经常和中括号取值一起出现
只调用:
js
Object.keys(obj)
拿到的是属性名数组,而不是属性值数组。
假设结果是:
js
['id', 'name', 'age']
遍历时,key 每一次都只是一个字符串变量:第一次可能是 'id',第二次可能是 'name'。
既然属性名此刻存放在变量中,访问对象属性就不能写死成 obj.key。因为 obj.key 的意思是读取真正叫 'key' 的属性。
正确表达应该是:
text
变量 key 当前是什么字符串,就读取那个字符串对应的属性
对应语法就是:
js
obj[key]
所以 Object.keys() 与中括号访问不是偶然经常放在一起,而是由数据形式决定的:Object.keys() 产生动态键名,动态键名需要中括号读取对应值。
20.6 场景六:对象合并时,为什么顺序本身就是业务规则
下面两段代码看起来只交换了位置:
js
const result1 = {
...oldData,
...newData
}
js
const result2 = {
...newData,
...oldData
}
但它们可能产生完全不同的结果,因为同名属性采用"后面的覆盖前面的"。
如果业务语义是"以旧数据为基础,用新数据更新其中一部分字段",顺序应该是:
text
先铺旧数据
再铺新数据
也就是:
js
const result = {
...oldData,
...newData
}
如果反过来,旧数据反而会把新字段重新覆盖回去。
因此,对象扩展语法不仅是"复制技巧",它的书写顺序本身就在表达优先级。看到对象合并时,要主动问:同名字段到底应该听谁的?答案决定谁写在后面。
20.7 场景七:保存浏览器数据时为什么必须建立完整的数据转换链
很多本地存储问题都来自只记住一句:
js
localStorage.setItem('key', value)
却没有先问 value 当前是什么类型。
如果 value 本来就是字符串,可以直接保存。
如果 value 是对象或数组,则需要明确转换:
text
对象/数组
↓ JSON.stringify()
字符串
↓ setItem()
Storage
读取过程恰好反过来:
text
Storage
↓ getItem()
字符串或 null
↓ 判断是否为 null
存在时 JSON.parse()
↓
对象/数组
这里每一步都有返回值类型。
getItem() 返回字符串或 null。所以取出后不要马上把它当对象使用。
JSON.parse() 接收的是 JSON 字符串,返回解析后的 JavaScript 值。所以解析成功后,才能再使用对象属性、数组方法。
把这条链路记清楚以后,很多"为什么读取后不能直接 .map()"的问题都会自然消失:因为取出的第一步仍然是字符串,而不是数组。
20.8 场景八:为什么 Promise 要"返回出去"
写 Promise 链时最常见的逻辑错误之一,是在 then() 内部启动了下一段异步操作,却没有 return。
理解这个问题不需要背复杂规则,只要抓住一条:
下一段
.then()只能等待上一段返回给它的 Promise。
如果上一段只是内部悄悄调用一个 Promise,却没有返回,那么链条本身并不知道需要等待它。
所以:
js
.then(() => {
return delay(1000, 2)
})
里的 return 不是为了"把 2 返回出去"这么简单,而是在告诉整个 Promise 链:下一步要以这个异步操作的完成结果为准。
当使用 async/await 后,这个关系会更直观:
text
await 某个 Promise
本身就明确写出了"当前后续逻辑依赖它完成"。这也是 async/await 可读性更强的重要原因之一。
20.9 场景九:async 函数为什么外面仍然需要继续等待
一个 async 函数内部可以把异步过程写得像顺序代码:
js
async function load() {
const result = await requestData()
return result
}
但不能因为内部出现了 return result,就认为外部调用 load() 会同步拿到结果。
外部拿到的仍然是 Promise。
原因是:async 只是让函数内部能够用更清晰的方式组织 Promise,不会改变异步结果"未来才能确定"的事实。
因此调用方仍然需要:
text
await load()
或者:
text
load().then(...)
这形成了一个非常重要的异步传播规律:只要某个函数的最终结果依赖异步任务,那么上层调用者也需要用 Promise 的方式接收这个未来结果。
20.10 场景十:模块化最应该按"职责"拆,而不是按"代码行数"拆
模块化并不是"文件超过 100 行就拆成两个"。真正合理的拆分依据是职责。
例如数学工具函数放进 math.js,本地存储操作放进 storage.js,商品相关请求放进商品服务模块。这样导入时,路径和导入名本身就在解释代码用途。
默认导出适合一个模块有一个非常明确的主要导出对象;命名导出适合同一模块包含多项并列工具能力。
无论选择哪一种,最重要的是让调用处容易理解:
text
我从哪个模块拿到了什么能力?
当这一点清楚时,import 与 export 就不再只是语法,而是项目结构的边界。
二十一、最后再用"输入、处理、输出"统一所有常用方法
如果前面知识很多,最简单的统一方式是把每一个 API 都套进三个问题:
text
输入是什么?
中间做什么?
输出是什么?
filter():输入一个数组和判断函数,中间逐个测试元素,输出筛选后的新数组。
map():输入一个数组和转换函数,中间逐个计算新值,输出映射后的新数组。
every():输入一个数组和判断函数,中间检查所有元素是否通过,输出布尔值。
reduce():输入一个数组、累计函数和可选初始值,中间不断更新累计结果,输出最终单一结果。
Object.keys():输入对象,中间读取它自身可枚举字符串键,输出键名数组。
JSON.stringify():输入可序列化的 JavaScript 值,中间按照 JSON 规则转换,输出字符串。
JSON.parse():输入 JSON 字符串,中间解析文本,输出 JavaScript 值。
localStorage.getItem():输入键名,中间从当前源的本地存储中查找,输出字符串或 null。
Promise.then():输入处理函数,中间在 Promise 确定结果后执行对应回调,输出一个新的 Promise。
await:输入一个通常为 Promise 的表达式,中间等待它确定结果,成功时输出 fulfillment 值,失败时抛出 rejection 原因。
import:输入模块说明符和导入声明,中间由模块系统建立依赖关系,输出当前模块可使用的本地绑定。
当所有知识都能用"输入 → 处理 → 输出"解释时,代码会明显更容易推理。因为任何一段链式调用,本质上就是上一步的输出成为下一步的输入。
例如:
text
Object.keys(obj)
输出键数组
↓
filter(...)
输出筛选后的键数组
↓
map(...)
输出转换后的新数组
又例如:
text
localStorage.getItem(key)
输出字符串 / null
↓
判断 null
↓
JSON.parse(text)
输出对象 / 数组
↓
filter() / map() / reduce()
再例如异步:
text
requestData()
输出 Promise
↓
await
输出成功结果
↓
解构对象
输出具体字段变量
↓
数组方法继续处理
这就是现代 Vue 3 JavaScript 代码最核心的阅读方式:不要把每个符号孤立看待,而要一直追踪数据从哪里来、现在是什么类型、经过这一行以后变成什么、下一行又拿它做什么。
二十、2026 年核对参考资料
本文涉及的 JavaScript 与浏览器 API 行为,按 2026-08-08 可访问的官方文档进行了核对。继续查阅时应优先使用规范文档、MDN、Vue 官方文档与 Node.js 官方文档。
-
Vue 官方 Quick Start
-
Vite 官方 Getting Started
-
MDN:
consthttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
-
MDN:Template literals
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
-
MDN:Destructuring
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring
-
MDN:Arrow function expressions
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
-
MDN:Array.prototype.push()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push
-
MDN:Array.prototype.unshift()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift
-
MDN:Array.prototype.pop()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop
-
MDN:Array.prototype.shift()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift
-
MDN:Array.prototype.splice()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice
-
MDN:Array.prototype.includes()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
-
MDN:Array.prototype.forEach()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
-
MDN:Array.prototype.filter()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
-
MDN:Array.prototype.map()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
-
MDN:Array.prototype.every()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every
-
MDN:Array.prototype.reduce()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
-
MDN:Object.keys()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
-
MDN:Spread syntax
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
-
MDN:JSON.stringify()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
-
MDN:JSON.parse()
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
-
MDN:Web Storage API
https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
-
MDN:Storage quotas and eviction criteria
https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria
-
MDN:Storage.setItem()
https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
-
MDN:Storage.getItem()
https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
-
MDN:Storage.removeItem()
https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
-
MDN:Promise
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
-
MDN:async function
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
-
MDN:JavaScript modules
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
-
Node.js 官方 ECMAScript modules 文档