`
:::
写一个 HTML5 语义化的例子
::: details
```html
HTML5 语义化示例
```
:::
## DOM 节点的 attr 和 property 有何区别
- attr 指的是 HTML 属性(attribute)
- property 指的是 DOM 对象的属性(property)
主要区别
::: details
定义不同
- attr 定义在 HTML 元素上的初始属性,存储在 DOM 元素的属性列表中,与 HTML 源代码一一对应。
- property 是 DOM 对象的属性,是通过浏览器解析 HTML 并生成 DOM 对象时动态创建的,供 JavaScript 操作。
存储位置不同
- attr 是 HTML 的一部分,存储在元素的 HTML 标记 中。
- property 是 DOM 的一部分,存储在 JavaScript 对象中。
行为不同
- attr 一般是静态的,表示元素初始的值,即从 HTML 源代码中解析的值,通常不会因用户操作或脚本修改而自动更新。除非你手动使用 JS 修改值。
- property 一般是动态的,表示当前状态,可以通过 JavaScript 修改,并反映在 DOM 中。
对于一些常用的属性(如 id、value、checked 等),attr 和 property 会部分同步:
- 修改 attr 会影响 property 值。
- 而修改 property 可能不会同步回 attr。
总结,一般来说,attr 用于设置元素的初始状态,而 property 用于操作和获取当前状态。
:::
## 如何一次性插入多个 DOM 节点?考虑性能
参考答案
::: details
直接多次操作 DOM(如多次 `appendChild` 或 `innerHTML` 更新)会导致性能问题,因为每次操作都会触发 DOM 的重新渲染。
`DocumentFragment` 是一个轻量级的文档片段,可以在内存中操作节点,最后一次性插入到 DOM 中,从而减少重绘和回流。
```js
// 获取目标容器
const container = document.getElementById('list')
// 创建 DocumentFragment
const fragment = document.createDocumentFragment()
// 创建多个节点并添加到 fragment 中
for (let i = 1; i <= 1000; i++) {
const li = document.createElement('li')
li.textContent = `item ${i}`
fragment.appendChild(li)
}
// 一次性插入到 DOM
container.appendChild(fragment)
```
:::
## offsetHeight scrollHeight clientHeight 有什么区别
参考答案
::: details
`offsetHeight` 元素的总高度,包括内容高度、内边距(padding)、水平滚动条高度(如果存在)、以及边框(border)。不包括外边距(margin)。
`scrollHeight` 元素的实际内容高度,包括不可见的溢出部分(scrollable content),大于等于 `clientHeight`。
`clientHeight` 元素的可见内容高度,包括内容高度和内边距(padding),但不包括水平滚动条高度、边框(border)和外边距(margin)。
:::
## HTMLCollection 和 NodeList 的区别
在操作 DOM 时,`HTMLCollection` 和 `NodeList` 都是用来表示节点集合的对象,它们的区别是:
::: details
`HTMLCollection` 只包含 **HTML 元素**节点。通过 `document.getElementsByTagName` 或 `document.getElementsByClassName` 返回的结果是 `HTMLCollection。`
`NodeList` 包括 **元素节点、文本节点、注释节点** 等,不仅仅是 **HTML 元素**节点
- 通过 `document.querySelectorAll` 返回的是 静态 `NodeList`
- 通过 `childNodes` 返回的是 动态 `NodeList`
当文档结构发生变化时
- `HTMLCollection` 和 动态 `NodeList` 会随着 DOM 的变化自动更新
- 静态 `NodeList` **不会**随着 DOM 的变化自动更新
:::
## Node 和 Element 有什么区别?
在 DOM(文档对象模型)中,HTML Element 和 Node 都是表示文档结构中的对象,但它们有不同的定义和用途。
::: details
Node 是 DOM 树中所有类型对象的基类,是一个接口,表示文档树中的一个节点。它有多个子类型,Element 是其中的一个。其他的还有 Text、Comment 等。
Node 常见属性如 `nodeName` `nodeValue`
HTML Element 是 Node 的子类,专门表示 HTML 元素节点。它提供了与 HTML 元素相关的更多功能,如属性、样式等。HTML Element 仅表示 HTML 元素节点,通常对应 HTML 标签,如 ``, `
`, `` 等。
Element 常见属性和方法如 `innerHTML` `getAttribute` `setAttribute`
:::
## 开发一个无限下拉加载图片的页面,如何给每个图片绑定 click 事件?
参考答案
::: details
使用 **事件委托** 实现,避免重复绑定事件,性能高,适合动态加载的场景。
代码示例
```html
```
以上代码中,我们把 `click` 事件统一绑定在 `container` 容器中,然后判断 `event.target.tagName === 'IMG'` 即触发事件。
:::
## window.onload 和 DOMContentLoaded 的区别是什么?
这两个事件都用于检测页面的加载状态,但触发的时机和作用范围有所不同。
::: details
`DOMContentLoaded` 是当 **DOM 树构建完成**(HTML 被解析完成,不等待样式表、图片、iframe 等资源加载)时触发,不依赖于外部资源。
`window.onload` 是当 **整个页面及所有资源**(包括样式表、图片、iframe、脚本等)加载完成时触发,依赖于外部资源。
`DOMContentLoaded` 会更早触发。
使用推荐
- 如果你的逻辑只依赖 DOM 的加载(如操作页面结构、绑定事件),使用 `DOMContentLoaded`。
- 如果你的逻辑需要依赖页面所有资源加载完成(如获取图片尺寸、执行动画),使用 `window.onload`。
:::
## script 标签放在 head 里,怎么解决加载阻塞的问题
在 HTML 中,`
```
2. 使用 `defer` 属性。`defer` 属性使得脚本延迟执行,直到 HTML 文档解析完毕。这意味着脚本不会阻塞 HTML 渲染,且会按照文档中 `
```
3. 将 `
```
Chrome Remote Debug
- 安卓设备通过 USB 连接电脑
- 开启开发者模式和 USB 调试
- Chrome 访问 chrome://inspect
- 可以使用完整的 Chrome DevTools
Safari Web Inspector
- iOS 设备通过 USB 连接 Mac
- 开启 Web 检查器
- Safari 开发菜单中选择设备
- 可以使用完整的 Safari 调试工具
注意事项:
- HTTPS 抓包需要安装证书
- 部分 App 可能有反抓包机制
- 生产环境建议移除调试工具
- 注意数据安全和隐私保护
:::
## script 标签的 defer 和 async 有什么区别
参考答案
::: details
script 标签的 defer 和 async 属性都是用于控制脚本的加载和执行时机:
普通 script
- 阻塞 HTML 解析
- 立即下载并执行脚本
- 按照在文档中的顺序执行
defer
- 异步下载脚本,不阻塞 HTML 解析
- 等到 HTML 解析完成后,DOMContentLoaded 事件触发前执行
- 多个 defer 脚本按照在文档中的顺序执行
- 适用于需要操作 DOM 的脚本
- 只对外部脚本文件有效
async
- 异步下载脚本,不阻塞 HTML 解析
- 下载完成后立即执行,可能在 HTML 解析完成前执行
- 多个 async 脚本的执行顺序不确定,取决于下载完成时间
- 适用于独立的脚本,如统计和广告代码
- 只对外部脚本文件有效
使用建议:
- 需要操作 DOM 或依赖其他脚本的代码使用 defer
- 独立的、不依赖 DOM 和其他脚本的代码使用 async
- 如果脚本之间有依赖关系,不要使用 async
示例:
```html
```
:::
## prefetch 和 dns-prefetch 分别是什么
参考答案
::: details
prefetch 和 dns-prefetch 是两种不同的资源预加载技术:
prefetch
- 用于预加载将来可能需要的资源
- 浏览器空闲时才会下载
- 优先级较低,不影响当前页面加载
- 适用于下一页可能用到的资源
```html
```
dns-prefetch
- 预先解析域名的 DNS 记录
- 减少 DNS 解析时间
- 适用于即将请求其他域名的资源
- 对跨域资源加载特别有效
```html
```
使用建议:
- 对确定即将访问的资源使用 prefetch
- 对跨域资源较多的站点使用 dns-prefetch
- 不要过度预加载,可能浪费带宽
- 移动端要谨慎使用,考虑流量消耗
相关技术:
- preload:当前页面必需资源的预加载
- preconnect:预先建立连接(DNS + TCP + TLS)
- prerender:预先渲染整个页面
:::
## WebSocket 和 HTTP 协议有什么区别
参考答案
::: details
WebSocket 和 HTTP 的主要区别:
连接特性
- HTTP 是短连接:每次请求都需要建立新的 TCP 连接(除非使用 keep-alive)
- WebSocket 是持久化的长连接:只需要一次握手,后续可以持续通信
通信方式
- HTTP 是单向通信:客户端请求,服务器响应
- WebSocket 是双向通信:客户端和服务器都可以主动发送数据
数据格式
- HTTP 每次请求都要带完整的 HTTP 头
- WebSocket 第一次握手完成后,后续数据传输只需要很小的头部
应用场景
- HTTP 适合一次性的数据交互
- WebSocket 适合实时性要求高的场景,如:
- 实时聊天
- 游戏实时数据
- 实时协作文档
性能
- WebSocket 的性能和效率通常优于 HTTP 轮询
- WebSocket 可以更好地节省服务器资源和带宽
支持性
- HTTP 被所有浏览器支持
- WebSocket 需要浏览器支持(现代浏览器普遍已支持)
:::
## 如何上传文件?使用 fetch 或者 axios
参考答案
::: details
文件上传主要有以下几种方式:
使用 FormData
```js
// HTML
// fetch
const file = document.querySelector('#file').files[0]
const formData = new FormData()
formData.append('file', file)
fetch('/upload', {
method: 'POST',
body: formData
})
// axios
const formData = new FormData()
formData.append('file', file)
axios.post('/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
```
使用 Base64
```js
// 将文件转为 Base64
function fileToBase64(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result)
reader.readAsDataURL(file)
})
}
// fetch
const base64 = await fileToBase64(file)
fetch('/upload', {
method: 'POST',
body: JSON.stringify({ file: base64 }),
headers: {
'Content-Type': 'application/json',
},
})
// axios
const base64 = await fileToBase64(file)
axios.post('/upload', {
file: base64,
})
```
多文件上传
```js
// HTML
// fetch
const files = document.querySelector('#files').files
const formData = new FormData()
Array.from(files).forEach(file => {
formData.append('files', file)
})
fetch('/upload', {
method: 'POST',
body: formData
})
// axios
const formData = new FormData()
Array.from(files).forEach(file => {
formData.append('files', file)
})
axios.post('/upload', formData)
```
注意事项:
- 设置正确的 Content-Type
- 考虑文件大小限制
- 添加上传进度显示
- 处理上传错误
- 考虑文件类型限制
- 添加取消上传功能
:::
## 如何上传大文件?
参考答案
::: details
大文件上传主要有以下几种方案:
切片上传
- 将大文件分割成小块
- 并发上传多个切片
- 服务端合并所有切片
- 支持断点续传和进度显示
实现步骤:
前端切片
```js
function createFileChunk(file, size = 1 * 1024 * 1024) {
const chunks = []
let cur = 0
while (cur < file.size) {
chunks.push(file.slice(cur, cur + size))
cur += size
}
return chunks
}
```
上传切片
```js
async function uploadChunks(chunks) {
const requests = chunks.map((chunk, index) => {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('index', index)
return axios.post('/upload', formData)
})
await Promise.all(requests)
}
```
发送合并请求
```js
await axios.post('/merge', {
filename: file.name,
size: chunks.length,
})
```
断点续传
- 记录已上传的切片
- 重新上传时跳过已上传的部分
- 可以通过 localStorage 存储进度
- 使用 hash 标识文件和切片
秒传
- 上传前先发送文件 hash
- 服务端存在相同文件则直接返回
- 可以使用 spark-md5 计算文件 hash
性能优化
- 并发控制:限制同时上传的切片数
- 切片大小:根据网络状况动态调整
- 进度显示:计算整体上传进度
- 错误重试:单个切片上传失败后重试
:::
参考资料
::: details
- https://juejin.cn/post/7356817667574136884
:::
## 如何实现图片懒加载?
参考答案
::: details
IntersectionObserver API
```js
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.original
entry.target.removeAttribute('data-original')
io.unobserve(entry.target)
}
})
})
const imgs = document.querySelectorAll('img[data-original]')
imgs.forEach((item) => {
io.observe(item)
})
```
原生 loading 属性
```js
let viewHeight = window.innerHeight
function lazyLoad() {
let imgs = document.querySelectorAll('img[data-original]')
imgs.forEach((el) => {
let rect = el.getBoundingClientRect()
if (rect.top < viewHeight) {
let image = new Image()
image.src = el.dataset.original
image.onload = function () {
el.src = image.src
}
el.removeAttribute('data-original')
}
})
}
lazyLoad() // 页面初始加载时调用一次
document.addEventListener('scroll', lazyLoad)
```
:::
参考资料
::: details
- https://juejin.cn/post/7320513026188116003
:::
## 在网络层面可做哪些性能优化?
参考答案
::: details
网络性能优化可以从以下几个方面考虑:
减少请求数量
- 合并文件(CSS/JS 打包)
- 雪碧图(CSS Sprites)
- 图片懒加载
- 按需加载/异步加载
- 合理使用缓存
减小资源体积
- 代码压缩(minify)
- Gzip/Brotli 压缩
- 图片优化(压缩、webp格式)
- Tree Shaking
- 代码分割(Code Splitting)
CDN 优化
- 使用 CDN 分发静态资源
- 合理设置 CDN 缓存
- 选择合适的 CDN 节点
- 配置 CDN 预热和刷新策略
HTTP 优化
- 使用 HTTP/2 多路复用
- 开启 Keep-Alive
- 合理设置缓存策略
- DNS 预解析(dns-prefetch)
- 预连接(preconnect)
- 预加载(prefetch/preload)
资源加载优化
- 关键资源优先加载
- 非关键资源延迟加载
- 内联关键 CSS/JS
- 异步加载非关键 JS(async/defer)
- 优化资源加载顺序
接口优化
- 接口合并
- GraphQL 按需查询
- 数据缓存
- 避免重复请求
- 设置合理的超时时间
监控和分析
- 性能监控
- 错误监控
- 用户体验监控
- 性能数据分析
- 持续优化
:::
参考资料
::: details
- https://juejin.cn/post/7362080157237116978
:::
================================================
FILE: docs/first-exam/JS.md
================================================
# JS 基础知识
JS 是前端开发的核心能力,面试重点考察,无论工作经验长短。
::: tip
如有疑问,可免费 [加群](/docs/services/group.md) 讨论咨询,也可参与 [1v1 面试咨询服务](/docs/services/1v1.md), 专业、系统、高效、全流程 准备前端面试
:::
## 了解哪些最新的 ES 新特性?
参考答案
::: details
**特性 1: ES2024 的 JSON 模块**
支持直接通过 `import` 语法加载 JSON 文件,避免额外的文件读取逻辑。
```js
import config from './config.json' with { type: 'json' }
console.log(config.setting) // 输出 JSON 文件中的指定属性
```
::: tip
在 Node.js v18.20+ 和 v20+ 版本中,JSON 模块导入语法已从 `assert { type: 'json' }` 更新为 `with { type: 'json' }`,`assert` 语法已被弃用。
:::
**特性 2: ES2023 的 Array.prototype.findLast & Array.prototype.findLastIndex**
两个数组新方法,用于从最后一个元素搜索数组元素。它们的功能与 `find() 和 findIndex()` 类似,但搜索从数组末尾开始。
这些方法可在 `Array 和 TypedArray` 原型上使用。此功能通过消除手动数组反转的过程,为逆序搜索提供了一种有效的方法。
```js
const isOdd = (number) => number % 2 === 1
const numbers = [1, 2, 3, 4, 5]
console.log(numbers.findLast(isOdd)) // 5
console.log(numbers.findLastIndex(isOdd)) // 4
```
**特性 3: ES2022 的类字段与私有方法**
支持类中的私有字段 `(#field)` 和私有方法,增强了封装性。
```js
class Counter {
#count = 0
increment() {
this.#count++
}
#logCount() {
console.log(this.#count)
}
}
const counter = new Counter()
counter.increment()
// counter.#logCount(); // 报错,私有方法不可访问
```
**特性 4: ES2021 的逻辑赋值运算符**
新增 `&&=, ||=, ??=`,简化条件赋值逻辑。
```js
let user = { name: 'Alice', age: null }
user.name ||= 'Default Name' // 如果 name 为 falsy,则赋值
user.age ??= 18 // 如果 age 为 null 或 undefined,则赋值
console.log(user) // { name: 'Alice', age: 18 }
```
**特性 5: ES2020 的可选链和空值合并操作符**
简化深层嵌套对象属性的访问,并安全处理空值。
```js
const user = {
profile: {
details: { name: 'Alice' },
},
}
const name = user.profile?.details?.name ?? 'Anonymous'
console.log(name) // 输出 'Alice'
const age = user.profile?.age ?? 18
console.log(age) // 输出 18
```
**特性 6: ES2019 的数组 flat 和 flatMap 方法**
flat 展开多层嵌套数组,flatMap 结合映射与扁平化操作。
```js
const nestedArray = [1, [2, [3, 4]], 5]
console.log(nestedArray.flat(2)) // [1, 2, 3, 4, 5]
const strings = ['hello', 'world']
console.log(strings.flatMap((str) => str.split('')))
// ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
```
:::
参考文档
::: details
- https://juejin.cn/post/7459351912133132351
:::
## `typeof` 能判断哪些类型
参考答案
::: details
| **类型** | **返回值** | **备注** |
| ----------------------- | ------------- | ------------------------------------------------------ |
| **Undefined** | `"undefined"` | 当变量未被定义或未赋值时,返回此值。 |
| **Null** | `"object"` | 历史遗留问题,`null` 被错误地识别为对象。 |
| **Boolean** | `"boolean"` | 适用于 `true` 或 `false` 值。 |
| **Number** | `"number"` | 适用于整数和浮点数(包括特殊值 `NaN` 和 `Infinity`)。 |
| **String** | `"string"` | 适用于字符串(例如 `"hello"`)。 |
| **BigInt** | `"bigint"` | 适用于任意大的整数(例如 `10n`)。 |
| **Symbol** | `"symbol"` | 适用于 `Symbol` 类型。 |
| **Function(classes)** | `"function"` | 适用于可调用的对象(如函数和类定义)。 |
| **其他对象** | `"object"` | 包括数组、普通对象、日期对象、正则表达式等非函数对象。 |
**注意:**
1. **`typeof null === "object"`**
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"
2. **实际使用**
对于更复杂的类型检测,可以使用工具函数,如 `Object.prototype.toString.call()` 或第三方库(如 `lodash`)。
```js
// 数值
typeof 37 === 'number'
typeof 3.14 === 'number'
typeof 42 === 'number'
typeof Math.LN2 === 'number'
typeof Infinity === 'number'
typeof NaN === 'number' // 尽管它是 "Not-A-Number" (非数值) 的缩写
typeof Number(1) === 'number' // Number 会尝试把参数解析成数值
typeof Number('shoe') === 'number' // 包括不能将类型强制转换为数字的值
typeof 42n === 'bigint'
// 字符串
typeof '' === 'string'
typeof 'bla' === 'string'
typeof `template literal` === 'string'
typeof '1' === 'string' // 注意内容为数字的字符串仍是字符串
typeof typeof 1 === 'string' // typeof 总是返回一个字符串
typeof String(1) === 'string' // String 将任意值转换为字符串,比 toString 更安全
// 布尔值
typeof true === 'boolean'
typeof false === 'boolean'
typeof Boolean(1) === 'boolean' // Boolean() 会基于参数是真值还是虚值进行转换
typeof !!1 === 'boolean' // 两次调用 !(逻辑非)运算符相当于 Boolean()
// Symbols
typeof Symbol() === 'symbol'
typeof Symbol('foo') === 'symbol'
typeof Symbol.iterator === 'symbol'
// Undefined
typeof undefined === 'undefined'
typeof declaredButUndefinedVariable === 'undefined'
typeof undeclaredVariable === 'undefined'
// 对象
typeof { a: 1 } === 'object'
// 使用 Array.isArray 或者 Object.prototype.toString.call
// 区分数组和普通对象
typeof [1, 2, 4] === 'object'
typeof new Date() === 'object'
typeof /regex/ === 'object'
// 下面的例子令人迷惑,非常危险,没有用处。避免使用它们。
typeof new Boolean(true) === 'object'
typeof new Number(1) === 'object'
typeof new String('abc') === 'object'
// 函数
typeof function () {} === 'function'
typeof class C {} === 'function'
typeof Math.sin === 'function'
```
:::
## `==` 和 `===` 有什么区别?
参考答案
::: details
- **`==`(宽松相等)**:会在比较两个操作数时执行 **类型转换**,尝试将两者转换为相同类型后再比较。
- **`===`(严格相等)**:不会执行类型转换,仅在类型和值完全相同的情况下返回 `true`。
- **推荐使用 `===`**:因为它更严格、更符合预期,能避免潜在的错误。尤其是在需要精确判断值和类型时。
- 实际工作中,使用 if (a == null) 可判断 a 是否是 null 或者 undefined。
**常见比较结果**
| **x** | **y** | **==** | **===** |
| ------------------- | ------------------- | ------ | ------- |
| `undefined` | `undefined` | ✅ | ✅ |
| `null` | `null` | ✅ | ✅ |
| `true` | `true` | ✅ | ✅ |
| `false` | `false` | ✅ | ✅ |
| `'foo'` | `'foo'` | ✅ | ✅ |
| `0` | `0` | ✅ | ✅ |
| `+0` | `-0` | ✅ | ✅ |
| `+0` | `0` | ✅ | ✅ |
| `-0` | `0` | ✅ | ✅ |
| `0n` | `-0n` | ✅ | ✅ |
| `0` | `false` | ✅ | ❌ |
| `""` | `false` | ✅ | ❌ |
| `""` | `0` | ✅ | ❌ |
| `'0'` | `0` | ✅ | ❌ |
| `'17'` | `17` | ✅ | ❌ |
| `[1, 2]` | `'1,2'` | ✅ | ❌ |
| `new String('foo')` | `'foo'` | ✅ | ❌ |
| `null` | `undefined` | ✅ | ❌ |
| `null` | `false` | ❌ | ❌ |
| `undefined` | `false` | ❌ | ❌ |
| `{ foo: 'bar' }` | `{ foo: 'bar' }` | ❌ | ❌ |
| `new String('foo')` | `new String('foo')` | ❌ | ❌ |
| `0` | `null` | ❌ | ❌ |
| `0` | `NaN` | ❌ | ❌ |
| `'foo'` | `NaN` | ❌ | ❌ |
| `NaN` | `NaN` | ❌ | ❌ |
说明:
- ✅ 表示比较结果为 `true`
- ❌ 表示比较结果为 `false`
:::
## 你熟悉哪些数组 API ?
参考答案
::: details
1. **创建数组**
- `Array()`, `Array.of()`, `Array.from()`
```js
Array.of(1, 2, 3) // [1, 2, 3]
Array.from('123') // ['1', '2', '3']
```
2. **添加/删除元素**
- `push()`: 在末尾添加
- `pop()`: 删除末尾
- `unshift()`: 在开头添加
- `shift()`: 删除开头
```js
let arr = [1, 2]
arr.push(3) // [1, 2, 3]
arr.pop() // [1, 2]
arr.unshift(4) // [4, 1, 2]
arr.shift() // [1, 2]
```
3. **组合/拆分数组**
- `concat()`: 合并数组,不影响原数组,浅拷贝
- `join()`: 将数组连接为字符串
- `slice()`: 截取部分数组(不修改原数组)
```js
;[1, 2].concat([3, 4]) // [1, 2, 3, 4]
;['a', 'b', 'c'].join('-') // 'a-b-c'
```
4. **替换/重组**
- `splice()`: 添加、删除或替换元素
```js
let arr = [1, 2, 3]
arr.splice(1, 1, 'a') // [1, 'a', 3]
```
5. **查找单个元素**
- `indexOf()`: 查找首次出现的索引
- `lastIndexOf()`: 查找最后出现的索引
- `find()`: 找到第一个满足条件的元素
- `findIndex()`: 找到第一个满足条件的索引
```js
;[1, 2, 3].indexOf(2) // 1
;[1, 2, 3, 2].lastIndexOf(2) // 3
;[1, 2, 3].find((x) => x > 2) // 3
```
6. **判断**
- `includes()`: 判断是否包含某元素
- `some()`: 判断是否至少有一个元素满足条件
- `every()`: 判断是否所有元素满足条件
```js
;[1, 2, 3].includes(2) // true
;[1, 2, 3].some((x) => x > 2) // true
;[1, 2, 3].every((x) => x > 0) // true
```
7. **迭代**
- `forEach()`: 遍历元素,无法 break,可以用 try/catch 中 throw new Error 来停止
```js
;[1, 2, 3].forEach((item, index) => console.log(item, index))
```
8. **映射/变换**
- `map()`: 对每个元素进行操作并生成新数组
```javascript
;[1, 2, 3].map((x) => x * 2) // [2, 4, 6]
```
9. **过滤**
- `filter()`: 筛选出满足条件的元素
```js
;[1, 2, 3].filter((x) => x > 1) // [2, 3]
```
10. **规约**
- `reduce()`: 将数组缩减为单一值
- `reduceRight()`: 从右向左缩减
```js
;[1, 2, 3].reduce((acc, val) => acc + val, 0) // 6
;['a', 'b', 'c'].reduceRight((acc, val) => acc + val, '') // 'cba'
```
11. **排序**
- `sort()`: 对数组进行排序
- `reverse()`: 反转数组顺序
```js
;[3, 1, 2].sort((a, b) => a - b) // [1, 2, 3]
;[1, 2, 3].reverse() // [3, 2, 1]
```
12. **填充**
- `fill()`: 用指定值填充数组
```js
new Array(3).fill(0) // [0, 0, 0]
```
13. **扁平化**
- `flat()`: 将多维数组展平成一维
- `flatMap()`: 映射并展平
```js
;[1, [2, [3]]].flat(2) // [1, 2, 3]
;[1, 2].flatMap((x) => [x, x * 2]) // [1, 2, 2, 4]
```
14. **复制/填充**
- `copyWithin()`: 将数组的部分内容复制到其他位置
```js
;[1, 2, 3, 4].copyWithin(1, 2) // [1, 3, 4, 4]
```
15. **生成键值对**
- `keys()`, `values()`, `entries()`
```js
const arr = ['a', 'b', 'c']
;[...arr.keys()] // [0, 1, 2]
;[...arr.entries()] // [[0, 'a'], [1, 'b'], [2, 'c']]
```
16. **判断是否是数组**
- `Array.isArray()`
```js
Array.isArray([1, 2, 3]) // true
```
:::
## 值类型和引用类型的区别
```js
// 值类型
let a = 100
let b = a
a = 200
console.log(b) // 100
```
```js
// 引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age) // 21
```
参考答案
::: details
| 特性 | 值类型 | 引用类型 |
| ------------------ | --------------------------------------------------------------------- | -------------------------------------------- |
| **存储内容** | 数据值本身 | 数据的引用(地址) |
| **存储位置** | 栈内存 | 栈存引用,堆存实际数据 |
| **赋值方式** | 拷贝值 | 拷贝引用(地址) |
| **变量之间独立性** | 互相独立,互不影响 | 指向同一数据,互相影响 |
| **常见数据类型** | 基本数据类型(如 `number,string,boolean,undefined,null,symbol`) | 复杂数据类型(如 `Object,Array,Function`) |
1. 为什么有值类型和引用类型?
- **值类型**适合存储简单、占用内存较小的数据,操作快速。
- **引用类型**适合存储复杂、占用内存较大的数据,支持动态扩展。
2. 如何避免引用类型的共享问题?
- 如果需要创建引用类型的副本,使用深拷贝,而非浅拷贝。
深拷贝例子:
```javascript
const obj1 = { name: 'Alice' }
const obj2 = JSON.parse(JSON.stringify(obj1)) // 创建深拷贝
obj2.name = 'Bob'
console.log(obj1.name) // "Alice"
```
浅拷贝例子:
```javascript
const obj1 = { name: 'Alice' }
const obj2 = { ...obj1 } // 浅拷贝
obj2.name = 'Bob'
console.log(obj1.name) // "Alice"
```
:::
## 箭头函数和普通函数的区别
参考答案
::: details
| 特性 | 箭头函数 | 普通函数 |
| -------------------------- | ---------------------------------------------- | ------------------------------------- |
| 语法 | 简洁,使用 `=>` 定义 | 使用 `function` 定义 |
| `this` 绑定 | 词法绑定,继承外层 `this` | 动态绑定,调用时决定 |
| `arguments` 对象 | 没有,需要使用 `...args` | 有自己的 `arguments` 对象 |
| 是否能作为构造函数 | 不能 | 可以 |
| 是否有 `prototype` 属性 | 没有 | 有 |
| 是否支持 `bind/call/apply` | 不支持 | 支持 |
| 适用场景 | 用于回调函数、闭包、需要继承外层 `this` 的场景 | 需要动态绑定 `this`,或用作构造函数时 |
```js
// 箭头函数 this
const obj = {
name: 'Alice',
say: () => {
console.log(this.name) // undefined (继承全局作用域的 this)
},
}
obj.say()
// 普通函数 this
const obj = {
name: 'Alice',
say: function () {
console.log(this.name) // "Alice" (this 指向 obj)
},
}
obj.say()
// 箭头函数 不能作为构造函数
const Person = (name) => {
this.name = name
}
const p = new Person('Alice') // TypeError: Person is not a constructor
// 普通函数 构造函数
function Person(name) {
this.name = name
}
const p = new Person('Alice')
console.log(p.name) // "Alice"
// 箭头函数 ...args
const add = (...args) => {
console.log(args) // [1, 2, 3]
}
add(1, 2, 3)
// 普通函数 arguments
function add() {
console.log(arguments) // Arguments(3) [1, 2, 3]
}
add(1, 2, 3)
// 箭头函数 不支持 `bind/call/apply`
const obj = {
value: 42,
}
const arrowFn = () => {
console.log(this.value)
}
arrowFn.call(obj) // undefined
// 普通函数 支持 `bind/call/apply`
const obj = {
value: 42,
}
function normalFn() {
console.log(this.value)
}
normalFn.call(obj) // 42
```
:::
## 什么时候不能使用箭头函数
参考答案
::: details
1. 需要动态绑定 `this` 的场景。
2. 作为`构造函数`。
3. 需要 `arguments` 对象的场景。
4. 需要显式修改 `this` 的场景(使用 `bind/call/apply` 等)。
5. 类的实例方法(特别是 `getter 和 setter`)。—— 无法动态绑定 `this`
:::
## for...in 和 for...of 的区别
参考答案
::: details
| 特性 | `for...in` | `for...of` |
| ------------------ | ---------------------------- | --------------------------------------- |
| **用途** | 遍历对象的 **可枚举属性** | 遍历 **可迭代对象**(如数组、字符串等) |
| **返回值** | 返回 **键**(属性名) | 返回 **值**(元素值) |
| **适用范围** | 对象、数组(不推荐用于数组) | 数组、字符串、Set、Map等可迭代对象 |
| **是否遍历原型链** | 会遍历原型链上的可枚举属性 | 不会遍历原型链 |
```javascript
// for...in 遍历对象
const obj = { name: 'Alice', age: 25 }
for (let key in obj) {
console.log(key) // 输出属性名:name, age
console.log(obj[key]) // 输出属性值:Alice, 25
}
// for...in 遍历数组,不推荐
const arr = [10, 20, 30]
for (let index in arr) {
console.log(index) // 输出索引:0, 1, 2
console.log(arr[index]) // 输出值:10, 20, 30
}
// for...of 遍历数组
const arr = [10, 20, 30]
for (let value of arr) {
console.log(value) // 输出值:10, 20, 30
}
```
:::
## JS 原型和原型链
参考答案
::: details

**1. 原型(Prototype)**
- 每个 **函数**(构造函数)都有一个 `prototype` 属性,指向其 **原型对象**。
- 每个 **对象** 都有一个 `__proto__` 指向其构造函数的 `prototype`,形成继承关系。
**2. 原型链(Prototype Chain)**
- 访问对象属性时,先查找自身属性,找不到则沿 `__proto__` 逐级向上查找,直到 `null` 终止。
- `Object.prototype.__proto__ === null`,原型链的顶端是 `Object.prototype`。
```js
function Person(name) {
this.name = name
}
Person.prototype.sayHello = function () {
console.log('Hello!')
}
const p = new Person('Rain')
console.log(p.__proto__ === Person.prototype) // true
console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
```
:::
## JS 继承有几种方式?
参考答案
::: details
**1. 原型链继承**
**核心思路:** 让子类的 `prototype` 指向父类实例。
```js
function Parent() {
this.name = 'Parent'
}
Parent.prototype.sayHello = function () {
console.log('Hello from Parent')
}
function Child() {}
Child.prototype = new Parent() // 继承 Parent
Child.prototype.constructor = Child
const child = new Child()
console.log(child.name) // "Parent"
child.sayHello() // "Hello from Parent"
```
✅ **优点:** 父类方法可复用
❌ **缺点:** 1. 共享引用类型属性(如 `arr = []` 会被多个实例共享),2. 无法向父类构造函数传参
**2. 借用构造函数继承**
**核心思路:** 在子类构造函数中使用 `call` 继承父类属性。
```js
function Parent(name) {
this.name = name
}
function Child(name, age) {
Parent.call(this, name) // 继承 Parent
this.age = age
}
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
```
✅ **优点:** 1. 解决原型链继承共享问题,2. 可传参
❌ **缺点:** 无法继承父类原型上的方法
**3. 组合继承(原型链 + 构造函数继承,最常用)**
**核心思路:** 结合前两种方式,**继承属性用构造函数,继承方法用原型链**。
```js
function Parent(name) {
this.name = name
}
Parent.prototype.sayHello = function () {
console.log('Hello from Parent')
}
function Child(name, age) {
Parent.call(this, name) // 第 1 次调用 Parent
this.age = age
}
Child.prototype = new Parent() // 第 2 次调用 Parent
Child.prototype.constructor = Child
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
child.sayHello() // "Hello from Parent"
```
✅ **优点:** 解决了前两种方法的缺陷
❌ **缺点:** 调用两次 `Parent` 构造函数(一次 `call`,一次 `Object.create()`)
**4. Object.create() 继承(原型式继承)**
**核心思路:** 直接用 `Object.create()` 创建一个新对象,继承已有对象。
```js
const parent = {
name: 'Parent',
sayHello() {
console.log('Hello!')
},
}
const child = Object.create(parent)
child.age = 18
console.log(child.name, child.age) // "Parent", 18
child.sayHello() // "Hello!"
```
✅ **优点:** 适合创建对象而非类的继承
❌ **缺点:** 不能传参,只适用于简单继承
**5. 寄生组合继承(优化版,推荐)**
**核心思路:** **组合继承的优化版本**,避免了 `Parent` 被调用两次的问题。
```js
function Parent(name) {
this.name = name
}
Parent.prototype.sayHello = function () {
console.log('Hello from Parent')
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Child.prototype = Object.create(Parent.prototype) // 关键优化
Child.prototype.constructor = Child
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
child.sayHello() // "Hello from Parent"
```
✅ **优点:** 1. 继承属性和方法,2. 只调用一次 `Parent`
❌ **缺点:** 代码略微复杂
**6. ES6 class 继承(最现代化的方式)**
**核心思路:** `class` 语法糖,实际仍然基于原型继承。
```js
class Parent {
constructor(name) {
this.name = name
}
sayHello() {
console.log('Hello from Parent')
}
}
class Child extends Parent {
constructor(name, age) {
super(name) // 继承属性
this.age = age
}
}
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
child.sayHello() // "Hello from Parent"
```
✅ **优点:** 语法更清晰,易读易用
❌ **缺点:** 本质仍是 `prototype` 继承
:::
## JS 作用域和作用域链
参考答案
::: details
- **作用域**:变量的可访问范围,分为 **全局作用域、函数作用域、块级作用域**。
- **作用域链**:变量查找机制,从当前作用域 **逐级向上查找**,直到全局作用域或 `ReferenceError`。
- **ES6 关键点**:
- `let` / `const` **具有块级作用域**,避免 `var` 变量提升带来的问题。
- **闭包** 利用作用域链,保留外部作用域的变量。
```js
var a = 'global'
function outer() {
var b = 'outer'
function inner() {
var c = 'inner'
console.log(a, b, c) // ✅ global outer inner
}
inner()
}
outer()
console.log(b) // ❌ ReferenceError: b is not defined
```
:::
## JS 自由变量,如何理解
参考答案
::: details
**自由变量** 指的是 **在当前作用域中未声明,但在上层作用域中找到的变量**。
在 JavaScript 中,当代码执行时,如果遇到一个变量:
- **当前作用域** 找不到该变量,就会沿着 **作用域链** 向上查找,直到找到该变量或报 `ReferenceError`。
- **这个在外层作用域中找到的变量,就是自由变量。**
```js
var a = 10 // 全局变量(自由变量)
function foo() {
console.log(a) // 访问自由变量 a
}
foo() // 10
```
:::
## JS 闭包,如何理解
参考答案
::: details
**闭包的核心特性:**
1. 访问外部函数作用域的变量
2. 即使外部函数执行结束,变量依然被保留
3. 不会被垃圾回收,直到闭包不再被引用
**闭包的应用场景:**
1. 私有变量(模拟封装)
```js
function createCounter() {
let count = 0 // 私有变量,外部无法直接访问
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
}
}
const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.increment()) // 2
console.log(counter.getCount()) // 2
console.log(counter.count) // undefined(外部无法直接访问)
```
2. 回调 & 事件监听
```js
function addEventLogger(eventName) {
return function () {
console.log(`Event ${eventName} triggered!`)
}
}
document.addEventListener('click', addEventLogger('click'))
```
3. 定时器 & 异步操作
```js
function delayedGreeting(name) {
setTimeout(() => {
console.log(`Hello, ${name}!`)
}, 2000)
}
delayedGreeting('Rain') // 2 秒后打印 "Hello, Rain!"
```
**闭包的缺点:**
1. 可能导致内存泄漏
- 闭包会持有外部变量的引用,导致变量无法被垃圾回收
- 解决方案:手动将变量置为 null 或谨慎管理作用域
2. 滥用闭包可能影响性能
- 每次调用都会创建新的作用域,影响垃圾回收机制
- 适度使用,避免不必要的闭包
:::
## 同步和异步有什么区别?异步的意义是什么?
参考答案
::: details
**同步**:任务按顺序执行,当前任务未完成时,后续代码必须等待,代码是**阻塞**的。
**异步**:任务可以**不按顺序执行**,不会阻塞代码,后续代码可以继续执行,代码是**非阻塞**的。
| 特性 | **同步** | **异步** |
| ------------ | -------------------------------- | ---------------------------- |
| **执行方式** | 顺序执行,阻塞后续任务 | 非阻塞,任务可以并行执行 |
| **代码特点** | **阻塞**,必须等待上一个任务完成 | **非阻塞**,任务可以同时进行 |
| **适用场景** | 计算密集型、简单逻辑处理 | 网络请求、I/O 操作、高并发 |
```js
// 同步
console.log('任务 1')
alert('等待用户操作...')
console.log('任务 2') // 只有用户关闭 alert,任务 2 才能执行
```
```js
// 异步
console.log('任务 1')
setTimeout(() => {
console.log('任务 2(延迟 2 秒)')
}, 2000)
console.log('任务 3') // 任务 3 不会等待 任务 2
// 任务 1
// 任务 3
// (2 秒后)
// 任务 2(延迟 2 秒)
```
**为什么要用异步?(异步的意义)**
1. 避免阻塞,提升用户体验
- 异步任务(如网络请求、文件读写)可以在后台执行,避免阻塞 UI,保证页面流畅。
2. 提升系统性能,支持高并发
- 服务器可以同时处理多个请求,提高吞吐量(如 Node.js 处理高并发)。
3. 更适合现代 Web 开发
- `Promise` / `async-await` 让异步代码更可读,配合 `fetch` 进行网络请求,提升开发效率。
:::
## JS Promise 有几种状态?如何变化
参考答案
::: details
**1. Promise 有几种状态?**
| 状态 | 说明 | 是否可变更 |
| ----------------------- | ----------------------------- | ----------- |
| **Pending(进行中)** | 初始状态,异步操作未完成 | ✅ 可以变更 |
| **Fulfilled(已完成)** | 操作成功,返回 `resolve` 结果 | ❌ 变更结束 |
| **Rejected(已拒绝)** | 操作失败,返回 `reject` 错误 | ❌ 变更结束 |
**2. Promise 状态如何变化?**
Promise 的状态**只会从 `Pending` → `Fulfilled` 或 `Pending` → `Rejected`**,且**一旦变化就不会再改变**(不可逆)。
```js
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
// reject("失败"); // 只会触发一次,状态不可逆
}, 1000)
})
promise.then((result) => console.log('Fulfilled:', result)).catch((error) => console.log('Rejected:', error))
```
:::
## JS Promise 使用
参考答案
::: details
**1. 什么是 Promise?**
> **Promise 是 JavaScript 处理异步操作的一种方式**,用于解决回调地狱(Callback Hell)问题。
> 它表示一个未来才会完成(或失败)的异步操作,并提供 `.then()`、`.catch()`、`.finally()` 方法进行处理。
**2. Promise 的基本用法**
**创建一个 Promise**
```js
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
let success = true
success ? resolve('操作成功') : reject('操作失败')
}, 1000)
})
```
**使用 `then`、`catch` 处理结果**
```js
myPromise
.then((result) => console.log('成功:', result)) // 处理成功
.catch((error) => console.log('失败:', error)) // 处理失败
.finally(() => console.log('操作结束')) // 无论成功或失败都会执行
```
**3. Promise 串行执行**
**多个异步操作依次执行(避免回调地狱)**
```js
function step1() {
return new Promise((resolve) => setTimeout(() => resolve('Step 1 完成'), 1000))
}
function step2() {
return new Promise((resolve) => setTimeout(() => resolve('Step 2 完成'), 1000))
}
step1()
.then((result) => {
console.log(result)
return step2() // 返回 Promise
})
.then((result) => console.log(result))
.catch((error) => console.error('错误:', error))
```
**4. Promise 并行执行**
**多个异步任务同时执行,全部完成后再处理**
```js
const p1 = new Promise((resolve) => setTimeout(() => resolve('任务 1'), 1000))
const p2 = new Promise((resolve) => setTimeout(() => resolve('任务 2'), 1500))
Promise.all([p1, p2])
.then((results) => console.log('所有任务完成:', results))
.catch((error) => console.error('任务失败:', error))
```
**如果只要最快完成的结果**
```js
Promise.race([p1, p2])
.then((result) => console.log('最先完成的:', result))
.catch((error) => console.error('失败:', error))
```
**5. 面试回答总结**
> **Promise 解决异步回调问题,提供 `.then()`、`.catch()`、`.finally()` 处理状态变化。支持 `Promise.all()` 并行执行,`Promise.race()` 竞争执行。用 `async/await` 可以让异步代码更清晰。**
:::
## async/await 使用
参考答案
::: details
async/await 是 ES2017(ES8)引入的 基于 Promise 的语法糖,用于更清晰地编写异步代码,使其看起来像同步代码,提高可读性。
- async 关键字:用于声明一个异步函数,返回值始终是 Promise。
- await 关键字:只能在 async 函数中使用,等待 Promise 解析(resolve)并返回结果,而不会阻塞线程。
```js
async function fetchData() {
try {
let response = await fetch('https://api.example.com/data')
let data = await response.json()
console.log(data)
} catch (error) {
console.error('Error:', error)
}
}
fetchData()
```
:::
## JS 异步执行顺序
执行以下代码,会输出什么?
```js
async function async1() {
console.log('async1')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(() => {
console.log('setTimeOut')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('script end')
```
答案
::: details
```
script start
async1
async2
promise
script end
async1 end
promise2
setTimeOut
```
:::
## 宏任务和微任务的区别
参考答案
::: details
在 JavaScript 的 事件循环(Event Loop) 机制中,任务分为 **宏任务(Macro Task)** 和 **微任务(Micro Task)**:
- **微任务优先**:微任务队列会在每次 宏任务执行完毕 后立即执行,保证微任务先执行完再进入下一个宏任务。
- **宏任务**:常见的宏任务包括 `setTimeout、setInterval、setImmediate(Node.js)、I/O、UI 渲染`。
- **微任务**:常见的微任务包括 `Promise.then、MutationObserver、queueMicrotask、process.nextTick(Node.js)`。
```js
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve()
.then(() => {
console.log('promise1')
})
.then(() => {
console.log('promise2')
})
console.log('end')
// 输出:
// start
// end
// promise1
// promise2
// setTimeout
```
:::
## 描述 Event Loop 运行机制
参考答案
::: details

Event Loop(事件循环)是 JavaScript 处理 **异步操作** 的核心机制。它允许 JavaScript 以 **非阻塞** 的方式执行代码,即使遇到 I/O 操作(如网络请求、定时器),也不会影响主线程继续执行其他任务。
**执行流程(核心步骤)**
1. **执行同步任务**
- 所有同步任务在 调用栈(Call Stack) 中依次执行,直到调用栈清空。
2. **处理微任务**
- 检查 微任务队列(MicroTask Queue) 是否有任务(如 Promise.then()、queueMicrotask())。
- 依次执行所有微任务,直到微任务队列清空。
3. **执行宏任务**
- 从 宏任务队列(MacroTask Queue) 取出 一个 任务(如 setTimeout 回调、I/O 任务),放入调用栈执行。
4. **重复步骤 2(处理新的微任务)**
- 宏任务执行完毕后,再次检查微任务队列,如果有新产生的微任务,立即执行所有微任务。
5. **重复步骤 3(执行下一个宏任务)**
- 继续取出下一个 宏任务,重复整个过程,形成循环(Event Loop)
:::
## Set 和 Array 有什么区别
参考答案
::: details
| 特性 | **Array** | **Set** |
| ------------------ | ------------------------------- | ------------------------------ |
| **是否允许重复值** | ✅ 允许重复元素 | ❌ 只能存储唯一值,自动去重 |
| **索引访问** | ✅ 可通过索引 (`arr[0]`) 访问 | ❌ 不支持索引访问 |
| **查找性能** | 🔴 `O(n)`,需要遍历整个数组 | 🟢 `O(1)`,基于哈希表查找更快 |
| **删除性能** | 🔴 `O(n)`,需要遍历查找删除 | 🟢 `O(1)`,删除性能更优 |
| **遍历方式** | ✅ `forEach` / `map` / `filter` | ✅ `forEach` / `for...of` |
| **适合的场景** | 存储有序数据,支持索引访问 | 需要唯一值集合,去重、快速查找 |
| **转换方式** | `Array.from(set)` (Set → Array) | `new Set(array)` (Array → Set) |
```js
// Array 允许重复值
const arr = [1, 2, 2, 3, 4, 4]
console.log(arr) // [1, 2, 2, 3, 4, 4]
// Set 自动去重
const set = new Set(arr)
console.log([...set]) // [1, 2, 3, 4]
// Set 无索引访问
console.log(set[0]) // undefined
// Set 转 Array
const arrFromSet = Array.from(set)
console.log(arrFromSet) // [1, 2, 3, 4]
```
:::
## Map 和 Object 有什么区别
参考答案
::: details
| 特性 | **Object** | **Map** |
| ------------------------- | --------------------------------------- | ------------------------------------- |
| **键的类型** | 只能是 `string` 或 `symbol` | 可以是任何类型(对象、函数等) |
| **键值对的存储顺序** | **无序**(属性顺序可能变化) | **有序**(插入顺序保持不变) |
| **查找性能** | 相对较慢(基于哈希表) | 更快(专门优化的键值存储结构) |
| **迭代方式** | `for...in`,`Object.keys()` 等 | `forEach()`,`for...of`(支持迭代器) |
| **获取键的方式** | `Object.keys(obj)` 只能获取 `string` 键 | `map.keys()` 可获取所有类型的键 |
| **获取大小** | 需手动计算 `Object.keys(obj).length` | `map.size` 直接获取大小 |
| **是否能轻松转换为 JSON** | ✅ 可以 `JSON.stringify()` | ❌ 不能直接 `JSON.stringify()` |
| **适用场景** | 适用于存储结构化数据,如对象属性 | 适用于 **高效键值存储和查找** |
```js
// Object 只能用字符串作为键
const obj = {}
obj['key1'] = 'value1'
obj[1] = 'value2' // 这里的 1 会被转换为 "1"
console.log(obj) // { '1': 'value2', key1: 'value1' }
// Map 可用任何类型作为键
const map = new Map()
map.set('key1', 'value1')
map.set(1, 'value2') // 数字 1 不会被转换为字符串
console.log(map) // Map(2) { 'key1' => 'value1', 1 => 'value2' }
// Object 迭代(无序)
console.log(Object.keys(obj)) // ['1', 'key1']
// Map 迭代(有序)
console.log([...map.keys()]) // ['key1', 1]
// Map 直接获取大小
console.log(map.size) // 2
// Object 需要手动计算大小
console.log(Object.keys(obj).length) // 2
```
:::
## setTimeout、requestAnimationFrame 和 requestIdleCallback 有什么区别
参考答案
::: details
| 特性 | `setTimeout` | `requestAnimationFrame` | `requestIdleCallback` |
| ------------------- | ---------------------------- | -------------------------------- | -------------------------------------------------- |
| **执行时机** | 设定时间后执行(不保证准时) | **下一帧渲染前**(16.6ms 以内) | **浏览器空闲时**(可能延迟执行) |
| **主要用途** | 延迟执行代码 | **动画和流畅渲染** | **低优先级任务**(如日志、分析) |
| **帧率控制** | **无**,可能丢帧 | **跟随屏幕刷新率**(一般 60FPS) | **不受限制**,完全取决于浏览器 |
| **影响页面性能** | **可能影响页面流畅度** | **保证流畅动画** | **不会阻塞主线程** |
| **是否适用于动画** | ❌ 可能卡顿 | ✅ 适合 | ❌ 不适合 |
| **是否受 CPU 影响** | ✅ 受影响 | ✅ 受影响 | ✅ 受影响 |
| **适用场景** | **定时任务、轮询** | **动画、过渡、流畅 UI 渲染** | **后台任务、低优先级执行(如数据同步、日志收集)** |
**`setTimeout` - 定时执行**
```js
setTimeout(() => {
console.log('100ms 后执行')
}, 100)
```
**`requestAnimationFrame` - 适用于动画**
```js
function animate() {
console.log('下一帧渲染前执行')
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
```
**`requestIdleCallback` - 空闲时执行**
```js
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
console.log('空闲时执行低优先级任务')
}
})
```
:::
## 写一个验证 email 的正则表达式
参考答案
::: details
```js
const reg = /\w+((-\w+)|(\.\w+))*@[a-zA-Z0-9]+((\.|-)[a-zA-Z0-9]+)*\.[a-zA-Z0-9]+$/
reg.test(email)
```
:::
## JS 模块化规范有哪些?
参考答案
::: details
1. **CommonJS**
- **概述**:这是 Node.js 中使用的模块化规范。它通过 `module.exports` 和 `require()` 来导出和引入模块。
- **特点**:同步加载,主要用于服务器端(Node.js)。
- **使用场景**:服务器端开发,尤其是在 Node.js 中。
```javascript
// 导出模块
module.exports = function () {
console.log('Hello, CommonJS!')
}
// 导入模块
const hello = require('./hello')
hello()
```
2. **AMD(Asynchronous Module Definition)**
- **概述**:AMD 是一种异步加载模块的规范,常用于浏览器端。
- **特点**:支持异步加载,模块和依赖是按需加载的,通常使用 `define()` 和 `require()`。
- **使用场景**:浏览器端的模块化,尤其是当需要异步加载模块时。
```javascript
define(['dependency'], function (dep) {
return function () {
console.log('Hello, AMD!')
}
})
```
3. **UMD(Universal Module Definition)**
- **概述**:UMD 是一个兼容多种模块化规范(CommonJS、AMD 和全局变量)的模块化方案。
- **特点**:确保模块在不同的环境中都能使用。
- **使用场景**:需要在多种环境下(如 Node.js、浏览器)使用的库或框架。
```javascript
;(function (root, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory()
} else if (typeof define === 'function' && define.amd) {
define(factory)
} else {
root.myModule = factory()
}
})(this, function () {
return function () {
console.log('Hello, UMD!')
}
})
```
4. **ES6 Modules(ESM)**
- **概述**:ES6 模块化是 JavaScript 原生的模块化标准,使用 `import` 和 `export` 语法。
- **特点**:支持静态分析,加载时可以进行优化,现代 JavaScript 标准。
- **使用场景**:现代前端开发(浏览器和 Node.js)。
```javascript
// 导出模块
export function greet() {
console.log('Hello, ESM!')
}
// 导入模块
import { greet } from './greet.js'
greet()
```
5. **SystemJS**
- **概述**:SystemJS 是一个支持多种模块规范(CommonJS、AMD 和 ESM)的模块加载器。
- **特点**:支持多种模块格式,动态加载模块。
- **使用场景**:需要跨模块加载器兼容的复杂应用。
```javascript
System.config({
map: {
greet: './greet.js',
},
})
System.import('greet').then((greet) => {
greet()
})
```
:::
## JS 如何捕获异常?有几种方式?
参考答案
::: details
1. **try...catch 语句**
```js
try {
// 可能会抛出异常的代码
throw new Error('Something went wrong!')
} catch (error) {
// 捕获并处理异常
console.error('Caught an error:', error.message)
}
```
2. **Promise 中的错误捕获(catch)**
```js
someAsyncFunction()
.then((result) => {
console.log(result)
})
.catch((error) => {
console.error('Async error caught:', error)
})
```
3. **window.onerror(全局错误处理)**
```js
window.onerror = function (message, source, lineno, colno, error) {
console.error(`Error occurred: ${message}`)
return true // 阻止默认错误处理
}
```
:::
## `0.1 + 0.2 === 0.3` 表达式返回什么?
参考答案
::: details
`0.1 + 0.2 === 0.3` 在 JavaScript 中会返回 **`false`**。
**原因:**
JavaScript 中的浮点数运算存在精度问题。由于计算机在内部表示浮点数时不能精确表示某些小数,导致 `0.1 + 0.2` 的结果并不是精确的 `0.3`,而是一个接近于 `0.3` 的小数。
具体来说,`0.1 + 0.2` 的计算结果是 `0.30000000000000004`,而不是 `0.3`。因此,当你用 `===`(严格相等)进行比较时,`0.30000000000000004` 和 `0.3` 不相等,结果为 `false`。
**解决方法:**
1. **四舍五入**:
```javascript
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON) // true
```
2. **自定义精度比较**:
将浮动值限制到一定的小数位,进行比较:
```javascript
console.log(Math.round((0.1 + 0.2) * 100) / 100 === 0.3) // true
```
:::
## 如何理解 JS 单线程?
参考答案
::: details
**什么是 JavaScript 单线程?**
JavaScript 是 **单线程** 的意思是它只有一个线程来执行代码,这意味着它一次只能执行一个任务。所有的 JavaScript 代码,默认情况下,都会按照顺序在同一个线程中依次执行。单线程的特性使得 JavaScript 相比多线程语言在处理并发时有一些限制,但它也有一套机制来处理异步操作,避免阻塞主线程。
**为什么是单线程?**
JavaScript 的设计目的是为了简化开发,尤其是在浏览器环境中。单线程可以避免多线程带来的复杂性,比如线程同步、资源竞争等问题。为了不让长时间的任务阻塞 UI 渲染,JavaScript 提供了异步编程的机制。
**如何处理并发任务?**
虽然 JavaScript 是单线程的,但它通过以下机制来实现并发任务的处理:
1. **事件循环(Event Loop)**:JavaScript 使用事件循环来管理异步任务。通过事件循环,JavaScript 可以在任务执行时不中断主线程的执行。异步任务(比如 `setTimeout`、`Promise`、`XHR` 等)会先进入 **消息队列(Event Queue)**,当主线程空闲时,再从队列中取出任务执行。
2. **Web APIs**:浏览器提供了 **Web APIs**(如 `setTimeout`、`fetch`、`DOM` 等)来处理一些异步操作。这些操作会被交给浏览器的 API 处理,处理完后通过事件循环机制将回调函数推送到消息队列,等待主线程执行。
3. **异步编程**:通过 **`setTimeout`**、**`Promise`**、**`async/await`** 等方式,JavaScript 可以非阻塞地处理 I/O 操作,避免卡住整个程序的执行。
:::
## 什么是 WebWorker 如何理解它?
参考答案
::: details
**Web Worker** 是一种浏览器提供的 API,允许你在一个独立的线程中执行 JavaScript 代码,**与主线程(UI 线程)分离**。Web Worker 可以处理计算密集型任务,如数据处理、文件解析等,这些任务通常会阻塞主线程,导致 UI 卡顿。通过 Web Worker,你可以将这些耗时操作移到后台线程,确保主线程始终保持响应状态。
**工作原理:**
1. **独立线程**:Web Worker 在一个与主线程(UI 线程)分离的线程中运行,主线程和 Worker 线程之间通过消息传递(postMessage)进行通信。
2. **主线程与 Worker 通信**:主线程可以通过 `postMessage()` 方法向 Worker 发送数据,Worker 完成计算后,通过 `postMessage()` 将结果返回给主线程。
3. **异步操作**:由于 Worker 在后台线程中运行,因此它的执行不会阻塞主线程,所有的计算任务都是异步执行的。
4. **线程间通信**:Worker 无法直接访问主线程的 DOM、`window` 或者 `document` 等对象,它只能通过 `postMessage()` 与主线程进行数据交换。返回的数据是通过事件机制传递的,使用 `onmessage` 监听数据的返回。
**Web Worker 的优势:**
- **性能提升**:Web Worker 可以让长时间的计算任务在后台线程中执行,避免 UI 阻塞,提升用户体验。
- **非阻塞性**:主线程可以继续处理用户交互和渲染,而不被复杂计算所阻塞。
- **多线程处理**:对于 CPU 密集型任务,Web Worker 可以将工作分配给多个 Worker,实现并行计算,提高性能。
**Web Worker 的应用场景:**
- **大数据处理**:例如,处理大量的数组计算、排序、数据筛选等任务。
- **图像处理**:例如,进行图像的处理和转换,而不影响 UI 渲染。
- **音视频处理**:例如,音视频的编码、解码等计算密集型操作。
- **异步任务**:一些需要后台执行的异步任务,可以通过 Worker 来处理。
**Web Worker 的局限性:**
- **无法操作 DOM**:Web Worker 在独立线程中运行,不能直接访问 DOM 和 `window`,只能通过消息传递来与主线程交换数据。
- **数据传递**:数据通过 `postMessage()` 传递时会发生深拷贝,因此传递大数据时可能会有性能开销。
- **浏览器支持**:大多数现代浏览器支持 Web Worker,但在旧版浏览器中可能不被支持。
1. **创建一个 Web Worker:**
```javascript
// main.js (主线程)
const worker = new Worker('worker.js') // 创建 Worker 实例
worker.postMessage('Hello, Worker!') // 向 Worker 发送消息
worker.onmessage = function (event) {
console.log('Worker says: ', event.data) // 接收 Worker 的响应
}
```
2. **Worker 文件(worker.js):**
```javascript
// worker.js (Worker 线程)
onmessage = function (event) {
console.log('Main thread says: ', event.data)
postMessage('Hello, Main Thread!') // 发送响应到主线程
}
```
:::
## JS 如何进行内存管理和垃圾回收?
参考答案
::: details
JavaScript 的内存管理是自动的,主要通过 **垃圾回收(GC)** 来实现。
**内存管理:**
1. JavaScript 使用 自动内存管理,开发者不需要手动分配和释放内存。
2. 内存通过 堆(用于存储对象和数组等动态分配的内存)和 栈(用于存储函数调用和局部变量)进行管理。
**常用的垃圾回收机制有:**
1. **标记-清除(Mark-and-Sweep)**:标记活动对象,清除未标记对象,释放内存。
2. **引用计数**:计算对象的引用次数,引用为 0 时回收。但会有循环引用的问题。
3. **生成式垃圾回收**:通过将内存分为年轻代和老年代,优化垃圾回收频率,减少内存碎片。
:::
## 如何检测 JS 内存泄漏?内存泄漏的场景有哪些?
参考答案
::: details
1. **使用浏览器开发者工具**:
- **Chrome DevTools** 中的 **Memory** 面板可以用来检测内存泄漏。可以查看 **Heap Snapshot** 和 **Allocation instrumentation on timeline**,分析对象分配、释放情况。
- **Heap Snapshot**:查看对象的分配情况,并通过比较不同时间点的快照来发现泄漏。
- **Timeline**:在页面交互过程中,查看内存的使用情况,发现持续增长的内存占用。
2. **通过 `performance.memory` API**:
- 在支持的浏览器中,可以通过 `performance.memory` API 获取当前的内存使用情况(如 JS 堆内存大小),来跟踪内存的变化。
```javascript
console.log(window.performance.memory)
```
3. **手动检测**:
- 通过创建和销毁对象,使用 `setInterval` 或 `setTimeout` 来检测是否有对象未被回收。
- 观察垃圾回收器是否清理不再使用的对象,如果内存不断增长,可能就是内存泄漏。
4. **第三方工具**:
- **Valgrind**、**Memory.js** 等工具可以帮助检测内存泄漏。
**内存泄漏的常见场景:**
1. **全局变量**:
- 意外的全局变量会导致对象无法被回收。
```javascript
function test() {
leakedVar = 'This is a global variable' // 未声明的变量成为全局变量
}
```
2. **未移除的事件监听器**:
- 如果事件监听器被绑定在 DOM 元素上,但没有在元素移除后正确移除,可能导致内存泄漏。
```javascript
const button = document.getElementById('myButton')
button.addEventListener('click', function () {
/* some logic */
})
// 如果没有 button.removeEventListener,按钮被移除后内存仍未释放
```
3. **闭包(Closures)**:
- 闭包会保持对外部函数变量的引用,如果闭包生命周期过长,会导致外部函数的变量无法释放。
```javascript
function createClosure() {
let largeObject = new Array(1000).fill('Some data')
return function () {
console.log(largeObject) // largeObject 被闭包引用,无法被 GC 回收
}
}
let closure = createClosure()
```
4. **DOM 引用**:
- 保留对已删除 DOM 元素的引用,导致内存泄漏。
```javascript
let div = document.createElement('div')
document.body.appendChild(div)
// 删除 DOM
document.body.removeChild(div)
// ❌ 如果还保留引用,GC 无法回收
let cache = div
// ✔️ 手动断开引用,GC 才能回收
div = null
cache = null
```
5. **定时器(setInterval/setTimeout)未清除**:
- 如果定时器没有清除,仍然会占用内存。
```javascript
let interval = setInterval(function () {
console.log('Running')
}, 1000)
// 如果没有 clearInterval(interval),定时器将一直运行,导致内存泄漏
```
6. **Web Workers 和后台线程**:
- 如果 Web Worker 或后台线程没有正确终止,可能会导致内存泄漏。
```javascript
const worker = new Worker('worker.js')
// 如果没有 worker.terminate(),worker 可能导致内存泄漏
```
:::
## 如何理解 WebAssembly?
参考答案
::: details
**WebAssembly(Wasm)** 是一种新的 Web 技术,它允许开发者将其他编程语言(如 C、C++、Rust 等)编译成高效的二进制代码,并在浏览器中运行。WebAssembly 旨在提供接近原生性能的 Web 体验,特别适用于高性能计算任务。
**关键点:**
1. **高效性**:WebAssembly 是一种二进制格式,比 JavaScript 的文本格式更紧凑,加载速度更快,执行速度更快,适用于 CPU 密集型任务,如图像处理、游戏开发和科学计算。
2. **与 JavaScript 协作**:WebAssembly 和 JavaScript 可以协同工作,JavaScript 用于 UI 操作和事件处理,WebAssembly 负责计算密集型任务。它们通过 **共享内存** 和 **消息传递** 进行通信。
3. **跨平台**:WebAssembly 是跨平台的,可以在所有支持 WebAssembly 的现代浏览器中运行,并且不需要针对不同操作系统和硬件做额外的修改。
4. **安全性**:WebAssembly 运行在沙盒环境中,不能直接访问操作系统资源,保证了 Web 应用的安全性。
**应用场景**:
- **游戏开发**:通过高效的计算,WebAssembly 可以让 Web 上的游戏运行得更流畅。
- **图像/视频处理**:利用 WebAssembly 进行高效的图像处理和视频编解码。
- **科学计算**:WebAssembly 能大大提升 JavaScript 在处理大数据和复杂计算时的性能。
:::
## JS V8 Nodejs Deno Bun 这几个,他们是什么关系?
参考答案
::: details
**1. V8**
- **V8** 是一个开源的 **JavaScript 引擎**,由 Google 开发,主要用于 Chrome 浏览器和 Node.js。
- V8 将 JavaScript 代码编译成机器代码并执行,从而提高 JavaScript 的执行效率。
- **作用**:V8 是 JavaScript 执行的“心脏”,负责解析和执行 JavaScript 代码。
- **关系**:V8 是 **Node.js** 和 **Deno** 的底层引擎。它本身不提供完整的 JavaScript 环境或库,只负责执行 JavaScript。
**2. Node.js**
- **Node.js** 是一个基于 V8 引擎的 **JavaScript 运行时环境**,使得 JavaScript 不仅可以在浏览器中运行,还可以在服务器端运行。
- 它为 JavaScript 提供了 I/O 操作、文件系统访问、网络请求等功能,这些功能通常由操作系统提供。
- **作用**:Node.js 使得 JavaScript 可以用于构建服务器端应用,支持事件驱动、非阻塞式 I/O 机制。
- **关系**:Node.js 使用 V8 作为其 JavaScript 引擎,除此之外,它还包含一些额外的 API(如 `fs`、`http`、`path` 等)来提供对文件系统、网络等资源的访问。
**3. Deno**
- **Deno** 是一个由 **Node.js** 的原始开发者 **Ryan Dahl** 创建的新的 JavaScript/TypeScript 运行时环境。
- 它同样使用 **V8 引擎**,但是与 Node.js 不同的是,Deno 内置了对 TypeScript 的支持,且具有现代化的安全特性(如权限控制)。
- **作用**:Deno 旨在修复 Node.js 中存在的一些设计问题,提供更简洁和安全的运行时环境。
- **关系**:Deno 使用 V8 作为 JavaScript 引擎,但它不是 Node.js 的直接继承者,而是对现有 JavaScript 运行时环境的一次重构,加入了许多新的功能和改进。
**4. Bun**
- **Bun** 是一个新兴的 **JavaScript/TypeScript 运行时**,其目标是提供更高效的性能,特别是在构建工具和服务器端应用中。
- Bun 是基于 **JavaScriptCore**(Safari 浏览器的 JavaScript 引擎)构建的,而不是 V8。
- **作用**:Bun 具有非常快速的执行速度,提供类似于 Node.js 的 API,同时它也是一个现代的构建工具(例如,能够快速打包、转译和运行 JavaScript/TypeScript 代码)。
- **关系**:Bun 不是基于 V8 引擎,它使用的是 **JavaScriptCore** 引擎,但它与 Node.js 和 Deno 类似,作为一个 JavaScript 运行时环境提供底层支持。
:::
## 有了解过WeakMap吗?WeakMap与Map的区别是什么?
参考答案
::: details
**1. 什么是WeakMap**
WeakMap 是 JavaScript 中的一种集合类型,它存储键值对,且键必须是对象,并且键是弱引用的。这意味着,如果键对象没有其他引用,它会被垃圾回收器回收,对应的键值对也会被自动删除。
**2. 与Map的区别**
**键的类型**
- **`Map`**:键可以是任意类型,包括基本数据类型(像字符串、数字等)和引用类型(如对象、函数)。
- **`WeakMap`**:键只能是对象,不能使用基本数据类型作为键。
**垃圾回收机制**
- **`Map`**:对键所引用的对象是强引用。只要 `Map` 存在,键引用的对象就不会被垃圾回收,即便其他地方无该对象的引用。
- **`WeakMap`**:对键所引用的对象是弱引用。若对象没有其他强引用,垃圾回收时对象会被回收,`WeakMap` 里对应的键值对也会自动移除。
**可遍历性**
- **`Map`**:是可迭代的,能使用 `for...of` 循环、`forEach` 方法等遍历其键值对。
- **`WeakMap`**:不可迭代,没有 `keys()`、`values()`、`entries()` 这些迭代方法,也不能用 `for...of` 或 `forEach` 遍历。
**方法和属性**
- **`Map`**:有 `size` 属性来获取键值对数量,还有 `set()`、`get()`、`has()`、`delete()`、`clear()` 等方法。
- **`WeakMap`**:只有 `set()`、`get()`、`has()`、`delete()` 方法,没有 `size` 属性和 `clear()` 方法。
**使用场景**
- **`Map`**:适用于需存储任意类型键值对,且要对这些键值对进行遍历和操作的场景,如缓存数据。
- **`WeakMap`**:常用于避免内存泄漏的场景,例如给对象添加私有数据,当对象被销毁时,`WeakMap` 里相关数据也会被清理。
:::
## 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?
参考答案
::: details
迭代协议
题目问怎么能让var [a,b] = {a:1,b:2} 成立,那么我们首先要运行一下,看看它是怎么个不成立法。
```JavaScript
const obj = {
a:'1',
b:'2',
}
const [a,b] = obj
```
运行之后打开控制台可以发现报错信息,它告诉我们obj这个对象是不可迭代的,那么我们想办法把obj变成可迭代的是不是就能解决这个问题,这要怎么做呢?想要搞明白这点我们需要先了解一下可迭代协议。
可迭代协议的概念( MDN )
`可迭代协议允许`JavaScript`对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置的可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object)。
要成为可迭代对象,该对象必须实现 @@iterator 方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:
[Symbol.iterator]
一个无参数的函数,其返回值为一个符合迭代器协议的对象。
当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。`
说人话就是,要想让obj成为一个可迭代的对象,就需要它实现 @@iterator 方法,具体表现为对象身上要有一个名为[Symbol.iterator] 的方法。而数组和Map则是一开始就有这个方法,所以它们是可迭代的。而对象身上则没有这个默认行为,所以不可迭代。真的是这样吗?我们创建一个数组,看看数组身上到底有没有[Symbol.iterator] 方法。
```JavaScript
const array = [1,2,3]
console.log(array)
```
点开原型查看

发现真的有一个Symbol.iterator()方法,该方法会返回一个迭代器对象。我们来调用一下
```JavaScript
const array = [1,2,3]
const iterator = array[Symbol.iterator]()
console.log(iterator)
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
```
打印iterator对象后发现在它的原型上有一个next()方法,调用next()方法,会得到一个对象value就是当前迭代的值,done则代表当前迭代器是否已经迭代完成。
数组 解构 的本质
```JavaScript
const array = [1,2,3]
var [a,b,c] = array
// 本质上是
const iterator = array[Symbol.iterator]()
var a = iterator.next().value
var b = iterator.next().value
var c = iterator.next().value
```
解决方法
到此为止我们可知,要想满足迭代协议需要对象身上有一个名为[Symbol.iterator]的方法。再使用for..of或者解构赋值的时候会隐式的调用这个方法,得到一个迭代对象,通过迭代对象的next方法判断当前是否完成迭代和具体迭代的值。
也就是说我们要在obj上添加[Symbol.iterator]方法并且完成next方法的逻辑
最终代码如下:
```javascript
const obj = {
a: '1',
b: '2',
[Symbol.iterator]() {
let index = 0
const keys = Object.keys(this)
return {
next() {
return {
value: obj[keys[index]],
done: index++ >= keys.length
}
}
}
}
}
const [a, b] = obj
```
当然,我们也可以用for...of去循环遍历这个对象,我看谁再说for...of不能遍历对象(doge)
```Javascript
for(let i of obj){
console.log(i)
}
// 1
// 2
```
:::
## postMessage 有哪些使用场景?
参考答案
::: details
**window.postMessage 定义**
`window.postMessage()`方法可以安全地实现跨源通信。`window.postMessage()` 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全
**用途**
可用于两个不同的Ifrom(不同源) 之间的通讯
语法
```JavaScript
otherWindow.postMessage(message, targetOrigin, [transfer]);
```
**参数说明**
- data
- 从其他 window 中传递过来的对象。
- origin
- 调用 `postMessage` 时消息发送方窗口的 **origin** . 这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。例如 “**** (隐含端口 443)”、“**** (隐含端口 80)”、“**
- source
- 对发送消息的**窗口**对象的引用; 您可以使用此来在具有不同origin的两个窗口之间建立双向通信。
**例子**
子框架传递信息
```JavaScript
```
父框架接收端
```JavaScript
```
这样即可以实现简单的框架跨域通信,但是会有一些安全问题
**安全问题**
如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。 这是一个完全万无一失的方式来避免安全问题。
如果您确实希望从其他网站接收message,请始终使用origin和source属性验证发件人的身份。 任何窗口(包括例如 **** 但是,验证身份后,您仍然应该始终验证接收到的消息的语法。 否则,您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。
- 当您使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是。恶意网站可以在您不知情的情况下更改窗口的位置,因此它可以拦截使用postMessage发送的数据。
示例
```JavaScript
/*
* A窗口的域名是,以下是A窗口的script标签下的代码:
*/
var popup = window.open(...popup details...);
// 如果弹出框没有被阻止且加载完成
// 这行语句没有发送信息出去,即使假设当前页面没有改变location(因为targetOrigin设置不对)
popup.postMessage("The user is 'bob' and the password is 'secret'",
"https://secure.example.net");
// 假设当前页面没有改变location,这条语句会成功添加message到发送队列中去(targetOrigin设置对了)
popup.postMessage("hello there!", "http://example.org");
function receiveMessage(event)
{
// 我们能相信信息的发送者吗? (也许这个发送者和我们最初打开的不是同一个页面).
if (event.origin !== "http://example.org")
return;
// event.source 是我们通过window.open打开的弹出页面 popup
// event.data 是 popup发送给当前页面的消息 "hi there yourself! the secret response is: rheeeeet!"
}
window.addEventListener("message", receiveMessage, false);
```
```JavaScript
/*
* 弹出页 popup 域名是,以下是script标签中的代码:
*/
//当A页面postMessage被调用后,这个function被addEventListener调用
function receiveMessage(event)
{
// 我们能信任信息来源吗?
if (event.origin !== "http://example.com:8080")
return;
// event.source 就当前弹出页的来源页面
// event.data 是 "hello there!"
// 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把event.source
// 作为回信的对象,并且把event.origin作为targetOrigin
event.source.postMessage("hi there yourself! the secret response " +
"is: rheeeeet!",
event.origin);
}
window.addEventListener("message", receiveMessage, false)
```
:::
================================================
FILE: docs/first-exam/TS.md
================================================
# Typescript 面试题
Typescript 已经全面普及,尤其大厂大型项目,前端熟悉 Typescript 是标配。
::: tip
如有疑问,可免费 [加群](/docs/services/group.md) 讨论咨询,也可参与 [1v1 面试咨询服务](/docs/services/1v1.md), 专业、系统、高效、全流程 准备前端面试
:::
## TS 优缺点,使用场景
参考答案
::: details
优点
- 静态类型,减少类型错误
- 有错误会在编译时提醒,而非运行时报错 —— 解释“编译时”和“运行时”
- 智能提示,提高开发效率
缺点
- 学习成本高
- 某些场景下,类型定义会过于混乱,可读性不好,如下代码
- 使用不当会变成 anyscript
```ts
type ModelFieldResolver = (
this: T,
...params: T[TKey] extends (...args: any) => any ? Parameters : never
) => T[TKey]
```
适用场景
- 大型项目,业务复杂,维护人员多
- 逻辑性比较强的代码,依赖类型更多
- 组内要有一个熟悉 TS 的架构人员,负责代码规范和质量
:::
PS. 虽然 TS 有很多问题,网上也有很多“弃用 TS”的说法,但目前 TS 仍然是最优解,而且各大前端框架都默认使用 TS 。
## TS 基础类型有哪些
参考答案
::: details
- boolean
- number
- string
- symbol
- bigint
- Enum 枚举
- Array 数组
- Tuple 元祖
- Object 对象
- undefined
- null
- any void never unknown
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/basic-types.html
:::
## 数组 Array 和元组 Tuple 的区别是什么
参考答案
::: details
数组元素只能有一种类型,元祖元素可以有多种类型。
```ts
// 数组,两种定义方式
const list1: number[] = [1, 2, 3]
const list2: Array = ['a', 'b', 'c']
// 元组
let x: [string, number] = ['x', 10]
```
:::
## 枚举 enum 是什么?有什么使用场景?
JS 中没有 enum 枚举,只学过 JS 你可能不知道 enum 。其实在 Java 和 C# 等高级语言中早就有了,TS 中也有。
参考答案
::: details
enum 枚举,一般用于表示有限的一些选项,例如使用 enum 定义 4 个方向
```ts
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
```
其他代码中,我们可以获取某一个方向,用于展示或存储。这样代码更具有可读性和维护行。
```ts
const d = Direction.Up
```
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/enums.html
:::
## keyof 和 typeof 有什么区别?
参考答案
::: details
`typeof` 是 JS 基础用法,用于获取类型,这个很简单。
`keyof` 是 TS 语法,用于获取所有 key 的类型,例如
```ts
interface Person {
name: string
age: number
location: string
}
type PersonType = keyof Person
// 等价于 type PersonType = 'name' | 'age' | 'location'
```
可以把代码拷贝到这里来练习 https://www.tslang.cn/play/index.html
:::
参考资料
::: details
- https://juejin.cn/post/7023238396931735583
- https://juejin.cn/post/7096869746481561608
:::
## any void never unknown 有什么区别
参考答案
::: details
主要区别:
- `any` 任意类型(不进行类型检查)
- `void` 没有任何类型,和 `any` 相反
- `never` 永不存在的值的类型
- `unknown` 未知类型(一个更安全的 any)
代码示例
```ts
function fn(): void {} // void 一般定义函数返回值
// 返回 never 的函数,必须存在无法达到的终点
function error(message: string): never {
throw new Error(message)
}
function infiniteLoop(): never {
while (true) {}
}
// unknown 比直接使用 any 更安全
const a: any = 'abc'
console.log(a.toUpperCase()) // 不会报错,但不安全
const b: unknown = 'abc'
// console.log( b.toUpperCase() ) // 会报错!!!
console.log((b as string).toUpperCase()) // 使用 as 转换类型,意思是告诉 TS 编译器:“我知道 b 的类型,我对安全负责”
```
PS:但现在 unknown 用的比 any 少很多,因为麻烦
:::
## unknown 和 any 区别
参考答案
::: details
`unknown` 是更安全的 `any` ,如下代码
```js
const a: any = 'x'
a.toString() // 不报错
const b: unknown = 'y'
// b.toString() // 报错
;(b as string).toString() // 不报错
```
:::
## TS 访问修饰符 public protected private 有什么作用
参考答案
::: details
- public 公开的,谁都能用 (默认)
- protected 受保护的,只有自己和子类可以访问
- private 私有的,仅自己可以访问
这些规则很难用语法去具体描述,看代码示例
```ts
class Person {
name: string = ''
protected age: number = 0
private girlfriend = '小丽'
// public protected private 也可以修饰方法、getter 等
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
class Employee extends Person {
constructor(name: string, age: number) {
super(name, age)
}
getInfo() {
console.log(this.name)
console.log(this.age)
// console.log(this.girlfriend) // 这里会报错,private 属性不能在子类中访问
}
}
const zhangsan = new Employee('张三', 20)
console.log(zhangsan.name)
// console.log(zhangsan.age) // 这里会报错,protected 属性不能在子类对象中访问,只能在子类中访问
```
:::
追问:`#` 和 `private` 有什么区别呢?
::: details
`#` 在 TS 中可定义私有属性
```ts
class Person {
#salary: number
constructor(
private name: string,
salary: number
) {
this.#salary = salary
}
}
const p = new Person('xxx', 5000)
// const n = p.name // 报错
const n = (p as any).name // 可以通过“投机取巧”获取到
console.log('name', n)
// const s = p.#salary // 报错
// const s = (p as any).#salary // 报错
```
区别:
- `#` 属性,不能在参数中定义
- `private` 属性,可通过 `as any` 强制获取
- `#` 属性,更私密
:::
## type 和 interface 共同和区别,如何选择
type 和 interface 有很多相同之处,很多人因此而产生“选择困难症”,这也是 TS 热议的话题。
共同点
::: details
- 都能描述一个对象结构
- 都能被 class 实现
- 都能被扩展
```ts
// 接口
interface User {
name: string
age: number
getName: () => string
}
// 自定义类型
type UserType = {
name: string
age: number
getName: () => string
}
// class UserClass implements User {
class UserClass implements UserType {
name = 'x'
age = 20
getName() {
return this.name
}
}
```
:::
区别
::: details
- type 可以声明基础类型
- type 有联合类型和交差类型
- type 可以被 `typeof` 赋值
```ts
// type 基础类型
type name = string
type list = Array
// type 联合类型
type info = string | number
type T1 = { name: string }
type T2 = { age: number }
// interface T2 { age: number } // 联合,还可以是 interface ,乱吧...
type T3 = T1 | T2
const a: T3 = { name: 'x' }
type T4 = T1 & T2
const b: T4 = { age: 20, name: 'x' }
// typeof 获取
type T5 = typeof b
//【补充】还有个 keyof ,它和 typeof 完全不同,它是获取 key 类型的
type K1 = keyof T5
const k: K1 = 'name'
```
:::
如何选择?
::: details
根据社区的使用习惯,推荐使用方式
- 能用 interface 就尽量用 interface
- 除非必须用 type 的时候才用 type
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/interfaces.html
:::
PS. 其实你混淆 type 和 interface 不是你的问题,这是 TS 设计的问题,或者说 TS 设计初衷和后来演变带来的副作用。
## 什么是泛型,如何使用它?
只学过 JS 的同学不知道泛型,其实它早就是 C# 和 Java 中的重要概念了。初学泛型可能会比较迷惑,需要多些代码多练习。
泛型的定义
::: details
泛型 Generics 即通用类型,可以灵活的定义类型而无需写死。
```ts
const list: Array = ['a', 'b']
const numbers: Array = [10, 20]
interface User {
name: string
age: number
}
const userList: Array = [{ name: 'x', age: 20 }]
```
:::
泛型的使用
::: details
1. 用于函数
```ts
// Type 一般可简写为 T
function fn(arg: Type): Type {
return arg
}
const x1 = fn('xxx')
// 可以有多个泛型,名称自己定义
function fn(a: T, b: K) {
console.log(a, b)
}
fn('x', 10)
```
2. 用于 class
```ts
class SomeClass {
name: T
constructor(name: T) {
this.name = name
}
getName(): T {
return this.name
}
}
const s1 = new SomeClass('xx')
```
3. 用于 type
```ts
function fn(arg: T): T {
return arg
}
const myFn: (arg: U) => U = fn // U T 随便定义
```
4. 用于 interface
```ts
// interface F1 {
// (arg: T): T;
// }
interface F1 {
(arg: T): T
}
function fn(arg: T): T {
return arg
}
const myFn: F1 = fn
```
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/generics.html
:::
## 什么是交叉类型和联合类型
### 交叉类型 `T1 & T2`
交叉类型是将多个类型合并为一个类型,包含了所需的所有类型的特性。例如 `T1 & T2 & T3`
代码示例
::: details
```ts
interface U1 {
name: string
city: string
}
interface U2 {
name: string
age: number
}
type UserType1 = U1 & U2
const userA: UserType1 = { name: 'x', age: 20, city: 'beijing' }
// 可在 userA 获取所有属性,相当于“并集”
userA.name
userA.age
userA.city
```
:::
注意事项
::: details
1. 两个类型的相同属性,如果类型不同(冲突了),则该属性是 `never` 类型
```ts
// 如上代码
// U1 name:string ,U2 name: number
// 则 UserType1 name 是 never
```
2. 基础类型没办法交叉,会返回 `never`
```ts
type T = string & number // never
```
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/advanced-types.html
:::
### 联合类型 `T1 | T2`
一种“或”的关系。格式如 `T1 | T2 | T3`。代码示例如下
::: details
```ts
interface U1 {
name: string
city: string
}
interface U2 {
name: string
age: number
}
function fn(): U1 | U2 {
return {
name: 'x',
age: 20,
}
}
```
:::
注意事项
::: details
基础类型可以联合
```ts
type T = string | number
const a: T = 'x'
const b: T = 100
```
但如果未赋值的情况下,联合类型无法使用 string 或 number 的方法
```ts
function fn(x: string | number) {
console.log(x.length) // 报错
}
```
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/advanced-types.html
:::
## 是否用过工具类型
TS 工具类型有 `Partial` `Required` `Omit` `ReadOnly` 等,熟练使用 TS 的人都会熟悉这些工具类型。
参考答案
::: details
`Partial` 属性设置为可选
```ts
interface User {
name: string
age: number
}
type User1 = Partial // 属性全部可选,类似 `?`
const u: User1 = {}
```
`Require` 属性设置为必选 (和 Partial 相反)
`Pick` 挑选部分属性
```ts
interface User {
name: string
age: number
city: string
}
type User1 = Pick // 只选择两个属性
const u: User1 = { name: 'x', age: 20 }
```
`Omit` 剔除部分属性(和 Pick 相反)
`ReadOnly` 属性设置为只读
相当于为每个属性都设置一遍 `readonly`
```ts
interface User {
name: string
age: number
}
type User1 = Readonly
const u: User1 = { name: 'x', age: 20 }
// u.name = 'y' // 报错
```
:::
## TS 这些符号 `?` `?.` `??` `!` `_` `&` `|` `#` 分别什么意思
参考答案
::: details
`?` 可选属性,可选参数
```ts
interface User {
name: string
age?: number
}
const u: User = { name: 'xx' } // age 可写 可不写
function fn(a: number, b?: number) {
console.log(a, b)
}
fn(10) // 第二个参数可不传
```
`?.` 可选链:有则获取,没有则返回 undefined ,但不报错。
```ts
const user: any = {
info: {
city: '北京',
},
}
// const c = user && user.info && user.info.city
const c = user?.info?.city
console.log(c)
```
`??` 空值合并运算符:当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。
```ts
const user: any = {
// name: '张三'
index: 0,
}
// const n1 = user.name ?? '暂无姓名'
const n2 = user.name || '暂无姓名' // 某些情况可用 || 代替
console.log('name', n2)
const i1 = user.index ?? '暂无 index'
const i2 = user.index || '暂无 index' // 当是 0 (或 false 空字符串等)时,就不能直接用 || 代替
console.log('index', i1)
```
`!` 非空断言操作符:忽略 undefined null ,自己把控风险
```ts
function fn(a?: string) {
return a!.length // 加 ! 表示忽略 undefined 情况
}
```
`_` 数字分隔符:分割数字,增加可读性
```ts
const million = 1_000_000
const phone = 173_1777_7777
// 编译出 js 就是普通数字
```
其他的本文都有讲解
- `&` 交叉类型
- `|` 联合类型
- `#` 私有属性
:::
## 什么是抽象类 abstract class
抽象类是 C# 和 Java 的常见语法,TS 也有,但日常前端开发使用并不多。
参考答案
::: details
抽象类,不能直接被实例化,必须派生一个子类才能使用。
```ts
abstract class Animal {
abstract makeSound(): void
move(): void {
console.log('roaming the earch...')
}
}
// const a = new Animal() // 直接实例化,报错
class Dog extends Animal {
// 必须要实现 Animal 中的抽象方法,否则报错
makeSound() {
console.log('wang wang')
}
}
const d = new Dog()
d.makeSound()
d.move()
```
:::
参考资料
::: details
- https://www.tslang.cn/docs/handbook/classes.html 其中搜索 `abstract class`
:::
## 如何扩展 window 属性,如何定义第三方模块的类型
参考答案
::: details
```ts
declare interface Window {
test: string
}
window.test = 'aa'
console.log(window.test)
```
:::
## 是否有过真实的 Typescript 开发经验,讲一下你的使用体验
开放性问题,需要结合你实际开发经验来总结。可以从以下几个方面考虑
::: details
- 在 Vue React 或其他框架使用时遇到的障碍?
- 在打包构建时,有没有遇到 TS 语法问题而打包失败?
- 有没有用很多 `any` ?如何避免 `any` 泛滥?
:::
参考资料
::: details
- https://juejin.cn/post/6929793926979125255
:::
================================================
FILE: docs/hr-exam/behavioural-test.md
================================================
# 行为面试技巧
HR 行为面试,考察工作态度和价值观。
::: tip
- HR 权利很大,可以拒绝你入职。所以和 HR 聊天要态度温婉。
- 无论你真实想法如何,HR 面试时要表现出一切都是为了公司和工作。工作好了,我才能好。
- 注意倾听,HR 可能会“挖坑”(个人缺点、如何评价前领导等),不要啥实话都往外说。
:::
::: tip
如有疑问,可免费 [加群](/docs/services/group.md) 讨论咨询,也可参与 [1v1 面试咨询服务](/docs/services/1v1.md), 专业、系统、高效、全流程 准备前端面试
:::
## 个人介绍
::: tip
这个问题在任何面试环节都可能会被问到,提前准备。
:::
使用如下模板,用自然舒缓的语气 1min 之内表达完,时间不要太长。
1. 个人履历,近几年的教育/工作经历
2. 擅长的业务领域(如医疗、3D、地图等),做过的主要项目
3. 擅长的技术栈 —— 程序员,最终还是要说到技术上
注意事项
- 不要太短,不要两三句话说完,要尽量全面的表达出自己
- 不要太啰嗦,不要深入细节,对方可能听不懂(这才面试刚开始)
- 要有条理性,不要想一个说一个,逻辑很混乱
## 离职原因?
常规说法:想换一个更大的平台,继续提升自己的能力。
如被裁员:实话实说即可,现在这行情被裁员也很正常。
注意事项
- 不要谈钱,不要说为了涨工资而离职
- 不要谈任何前公司的坏话,例如 xxx 项目/领导 不好而离职
## 空窗期为何这么久?
有些同学被裁员,然后迟迟未找到工作,空窗期可能几个月甚至半年多。
推荐表达:一开始对自己要求比较高,想找一个理想的工作,一直没有合适的机会。
注意事项
- 不要说“学习了一段时间”,暴露自己技术能力不好
- 不要说“休息了一段时间”,我们都自愿加班,根本不需要休息
- 不要说“出去玩了”,老板和 HR 不喜欢爱玩的员工
## 你为何选择我们公司?
推荐表达方式
- 提前熟悉公司背景、主营业务、JD 招聘信息、负责的项目
- 说出其中的 xxx 项目/技术 比较吸引你,或者 xxx 福利比较吸引你
- 再说出自己的 xxx 技能比较符合公司的招聘要求,所以就投了简历
注意,千万不能说:因为你们公司招聘啊!—— 这种无脑回答
## 你对这份工作有什么期许?
推荐表达方式
- 希望通过这个工作,参与项目,团队分享等,能让自己在 xxx 技术领域,提升更高一个层级
- 自己技术提升了,才能更好的服务于公司的项目/产品,承担更重要的职责
要点是:自己的进步是为了公司服务的,这是 HR 喜欢的价值观。
## 你喜欢什么样的团队和领导?
推荐表达方式
- 喜欢一个有技术氛围的团队,喜欢分享技术的领导,大家可以一起成长进步
- 技术氛围好,才能更好的保证代码质量和产品稳定性
同理:谈技术进步,也是为了公司产品服务的,这是 HR 喜欢的价值观。
## 未来 3-5 年的规划是什么?
参考上文“你对这份工作有什么期许?”的回答方式:
`技术进步 + 承担更重要的职责 + 为公司项目服务`
## 如何看待加班?
前两年我们一般回答:加班就像借钱,救急不救穷 —— 即,可以偶尔加班,但不能长久加班。
但是,近两年风向早就变了,加班成了常态,而且有的会比较加班工时。
所以现在再回复这个问题,就得换一种说法:认可加班。
其实换位思考一下,如果我们也严格执行 8 小工作制,那你的工资还会这么高吗?你是否愿意每日工作 8 小时,同时降薪 40% ?
## 你的优点是什么?
我们的文化比较内敛,大家都过于谦虚,不擅长宣传自己,尤其是程序员行业。
都会觉得自己没有啥优点,如果不提前准备,被问到这个问题可能就懵了。
优点,就是指你觉得自己比较好的部分,并不是说一定比别人优秀、甚至是专家。不是的,大部分都是普通人。
推荐表达方式
- 优先选择**硬技能**,自己擅长的技术领域,业务领域,能更好的用于工作中。
- 如果实在找不到擅长的技术领域,可以说一些**软技能**:认真,准时,容易沟通,为人和善友好,工作有激情,英语好...
遇到这个问题你就想办法夸自己,不要过于谦虚 —— 能表达自己的优点,这本身也是个优点。
## 你的缺点是什么?
注意,这是个坑!不要傻乎乎的真把自己的缺点说出来,暴露给别人看。
正确的方式是:说缺点,也要说自己的补救措施,最终体现自己是一个爱学习、要求进步的人。
推荐的表达方式
- 我觉得自己目前在 xxx 方面还有所欠缺
- 但我已经开始通过 xxx 学习这方面的知识了,计划 xxx 天以后即可完成
## 是否和前领导有过冲突?如何解决的?
准备一个工作中具体的例子,使用 `STAR` 模型讲解,例如:
- Situation 背景:记得在 xxx 项目中,我和领导有了不一致的意见,我认为应该先 xxx ,他认为应该先 yyy
- Task 目标/任务:但我们都是为了同一个目标,能把项目尽快上线,达到客户的要求
- Action 行动/解决方案:我们找来项目组核心成员,以及客户的核心成员,一起开会讨论,列出当前所有任务,划分优先级,重新制定时间计划,并且得到客户的认可
- Result 结果:最后项目成功上线,且客户很满意我们的沟通方式和做事效率
注意事项
- 工作中的冲突一般都是:任务太多,资源(人,时间)不够
- 冲突要完全是客观的,完全和工作相关的,一定是对事不对人的
- 不要说前领导/前公司的坏话,大家都是为了把工作做好
## 是否和同事/项目成员有过冲突?如何解决的?
参考上一题。
上一题中,和领导有冲突需要找客户沟通确认。而这一题,你和同事有了冲突,就需要找领导沟通确认。其他都是一样的。
## 是否和客户/甲方有过冲突?如何解决的?
参考上一题。
和客户发生了冲突,首先要第一时间和你领导沟通,说明情况。再和领导一起找客户沟通。
所以,这些问题都离不开 `同事 领导 客户` 这几个常见角色。
做 C 端产品的可能没有明确的甲方/客户,那和你发生冲突的可能是 PM 产品经理,沟通方式是一样的。
## 如果 PM 给你的项目加需求,你该如何应对?
在项目管理中有个专业术语叫“需求蔓延”,英文 Scope creep / requirement creep
意思就是本来定好了需求、开发计划,但是 PM 今天加个按钮,明天加个图标,后天加段文字...
遇到这种问题的常见解决方式
- 先和自己领导沟通
- 如果领导同意,且自己评估工作量不大,那就加上 —— 不要过于较真,HR 也不喜欢过于死板的人
- 如果领导同意,但自己评估工作量有点大,那就和领导说清楚(领导不一定熟悉每一个开发细节)
- 领导有可能直接不同意
- 如果确定不接这个需求,如何回绝 PM 呢?
- 让他发邮件,抄送他领导,以及项目组成员,走正式的需求变更流程
- 一般此时 PM 就退缩了,不会为了这个小需求来发正式的邮件的
- 最后,这样做是为了项目能稳定、按预期完成,否则需求蔓延太多,项目工期、质量都不可控
PS. 其实这样的 PM 好对付,怕就怕在中小公司有这样的老板,摊上了也没办法...
## 如果你项目的 deadline 很紧张,你该如何做?
准备一个工作中具体的例子,使用 `STAR` 模型讲解,例如:
- Situation 背景:在 xxx 公司时,有一个 xxx 项目,遇到过 deadline 很紧张的情况
- Task 任务:客户/老板下令必须在 xxx 时间之前上线,我们要按照这个时间往前推
- Action 行动/解决方案:
- 和领导沟通,让他帮忙协同其他人力资源(如测试人员),确保所有人都能按计划进行
- 和项目组开会,明确需求范围,列出功能,划分优先级,明确分工,制定开发计划
- 大家在一起集中加班开发,即时沟通,即时调整,快速开发完成,并提交测试
- Result 结果:虽然大家很累、进度优点紧张,但项目最后按计划上线,我们团队也因此更加团结高效
注意事项
- 所有的冲突,解决方案都会涉及 `领导` `沟通` `优先级` `计划` 这几个关键词
- 要提到加班,但不能只有加班。需要的是:有管理能力 + 加班
## 介绍一个你最近做过的项目
推荐的表达方式
- 项目背景和功能介绍:这是一个 xxx 项目,它服务于 xxx 。它主要包含 a b c 功能/模块。
- 技术栈:说出 2-3 个主要的即可,和 HR 不要说太多技术细节,能体现出技术即可
- 个人的角色和成绩:我是这个项目中的核心前端开发人员,我主要负责 x y z 模块的开发。
- 个人成长:通过开发这个项目,让我掌握了能独立承担项目/模块的能力,希望在未来的工作中继续发挥这方面的价值。
PS. 通过介绍项目,来体现自己当前的能力
## 说一件最让你有成就感的事情
准备一个工作中具体的例子,使用 `STAR` 模型讲解,例如:
- Situation 背景:介绍项目背景,要让 HR 能听懂
- Task 任务:要完成 xxx 目标,而且有很大难度 —— 没难度哪儿来的成就感?
- Action 行动/解决方案:通过 xxx 方式,最终解决了这个问题
- Result 结果:自己得到 xxx 提升,学会了 xxx 能力
注意事项
- 一定是和工作相关的,非工作的不要说
- 说的是一件事,但最后要总结为自己的能力,能做好未来的工作
## 说一件最让你感觉失败的事情
参考上一题。
虽然做失败了,但让我学到了 xxx ,最终还是要总结为自己能力的提升。
## 请用三个词概括自己
可选的词:`计划性` `执行力` `热情` `积极` `创造性` `极客精神` `时间观念强` `善于沟通` `乐于分享` `组织能力` `管理能力` `专注`
## 你的同事/领导如何评价你?
我看很多同学在简历中写“自我评价”,你就可以把自我评价搬到这里。例如
- 工作积极热情
- 善于团队合作
- 热爱技术
如果有可能,尽量在说每一条的时候举一个例子证明一下。
HR 天天听这些 `积极` `热情` `热爱` 这些词,耳朵早就听出茧子了,你不说点具体的例子,他们可能都听不进去。
## 你业余有什么兴趣爱好?
说几个程序员常见的爱好即可,例如玩游戏、看电影、听歌等。
不要说太影响工作和加班的爱好,如徒步、骑行、长途旅行等。
## 如何主持一次会议?
综合考察你的沟通和组织能力,这也是程序员最欠缺的能力。
- 明确会议主题、 todos 和核心参与人
- 联系核心参与人,协同他们的时间,确定会议时间
- 发送邮件给全体参会人员,抄送相关领导
- 制定会议议程,主持会议,保证讨论话题的方向(不要蔓延到其他话题),做会议记录
- 会议结束,整体会议记录和决议,发送邮件
PS. 最关键的是:又开始有结束,知道开会干啥,知道开完会有了什么结论。
================================================
FILE: docs/hr-exam/salary.md
================================================
# 谈薪技巧
HR 会把薪资尽量压低,以节省公司成本。而我们需要通过谈判,多争取一些薪资,当然要在合理范围之内。
::: tip
- 谈钱必伤感情,不要觉得不好意思,都是为自己争取利益,公平谈判。
- 你担心 HR 拒绝你,其实 HR 也担心你不来(面试几轮,找个合适的候选人也不容易)
:::
::: tip
如有疑问,可免费 [加群](/docs/services/group.md) 讨论咨询,也可参与 [1v1 面试咨询服务](/docs/services/1v1.md), 专业、系统、高效、全流程 准备前端面试
:::
## 大厂校招有统一工资标准
大厂会统一秋招、春招,只要面试通过会有统一的工资标准,大约年薪 200k 这个级别
- 不同公司不一样,例如字节平均薪资就高一点
- 不同岗位不一样,算法、大数据库会高一点
- 不同学历不一样,研究生会高一点
这是刚毕业的薪资水平,你未来如何涨薪,就看自己的能力和运气了。
一般来说,跳槽涨薪 vs 公司内部涨薪,前者要快很多。但目前就业行情不好,也要谨慎跳槽,千万不要裸辞。
## 参考你当前的薪资
跳槽,一般会在你当前薪资基础上涨幅 10-20% 。除非你当前薪资特别低,例如你在二线城市工作,整体薪资水平低。
PS. 当前就业行情不好,涨幅可能不会很大。
## 参考公司规模
一线城市的大公司薪资水品最高。公司规模越大,薪资水平相对会越高。初创小公司最“穷”且不稳定。
如果你刚毕业,根据自己的能力,尽量去大规模的公司,别相信什么“小公司锻炼能力”。
## 参考市场行情
前些年我见过从某度跳槽到某滴涨薪 50% 的人,这两年就出现了降薪跳槽的人,以及大量待业人员。
个人是斗不过环境的,具体来说就是:不要裸辞、不要拒绝外包、抓住任何工作机会。
## 参考公司福利,但要认清福利的价值
如果是大公司,公司福利几乎不用看,基本都不错,而且都是统一标准的。
如果是中小公司,且 HR 说有 xxx 福利,为了压低一点薪资。此时你问清楚并且算清楚这些福利的具体价值,能抵多少工资?
不要不好意思,谈钱本来就是伤感情的事儿,大家都是挣去自己的利益,这很正常。
## 不要透露底牌,自己说出具体的期望薪资
这是一个非常重要的技巧!!!
和 HR 谈薪时,HR 一般会直接问:你能接受的期望薪资是多少? —— HR 在问你的底牌
此时你如果说出来一个具体数,如 20K ,亮了底牌,你大概率就输了。
正确的回答方式应该是这样
- 首先自己提前做好调研,查一下当前合理的薪资范围是多少(例如 20-40k),根据城市、公司规模、岗位等
- 然后和 HR 谈薪时答复:我已经查到这个岗位的薪资是 20-40k ,不知道咱们公司能给到多少?
你要把太极打回去,让 HR 去亮底牌,然后再自己做判断。
## 多要现金,还是多要期权?
公司一般会倾向于:多给你期权,少给你现金。所以你应该:多要现金,少要期权。
当然,如果总包比较多的话,也不可能全要现金,公司没有那么多钱。合理比例即可。
知乎、脉脉那些总包 100w 200w 的,真假先不说,即便是真的,也大部分都是期权。
================================================
FILE: docs/markdown-examples.md
================================================
# Markdown Extension Examples
This page demonstrates some of the built-in markdown extensions provided by VitePress.
## Syntax Highlighting
VitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:
**Input**
````md
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
````
**Output**
```js{4}
export default {
data () {
return {
msg: 'Highlighted!'
}
}
}
```
## Custom Containers
**Input**
```md
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
```
**Output**
::: info
This is an info box.
:::
::: tip
This is a tip.
:::
::: warning
This is a warning.
:::
::: danger
This is a dangerous warning.
:::
::: details
This is a details block.
:::
## More
Check out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).
================================================
FILE: docs/second-exam/HarmonyOS-application-development.md
================================================
# 鸿蒙应用开发
## 鸿蒙中地图功能如何实现,申请流程是什么样的
::: details
1. 主要通过 集成 Map Kit 的功能来实现
2. Map Kit 功能很强大,比如有
1. [创建地图](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-creation-V5):呈现内容包括建筑、道路、水系等。
2. [地图交互](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-interaction-V5):控制地图的交互手势和交互按钮。
3. [在地图上绘制](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-drawing-V5):添加位置标记、覆盖物以及各种形状等。
4. [位置搜索](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-location-services-V5):多种查询 Poi 信息的能力。
5. [路径规划](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-navi-V5):提供驾车、步行、骑行路径规划能力。
6. [静态图](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-static-diagram-V5):获取一张地图图片。
7. [地图 Picker](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-advanced-controls-V5):提供地点详情展示控件、地点选取控件、区划选择控件。
8. [通过 Petal 地图应用实现导航等能力](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-petalmaps-V5):查看位置详情、查看路径规划、发起导航、发起内容搜索。
9. [地图计算工具](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/map-calculation-tool-V5):华为地图涉及的 2 种坐标系及其使用区域和转换
3. 在编码之前需要
1. 完成证书的申请和公钥指纹的一些配置
2. 还要在 AGC 平台上开通地图服务应用
3. 代码中使用 项目的 client_id
4. 最后开始编码
:::
## 一多开发是如何实现的
::: details
1. 一多开发是一次开发多端部署
2. 主要分成三个核心部分
1. 工程级一多
2. 界面级一多
3. 能力级一多
3. 工程级一多主要指的是使用华为鸿蒙推荐的三层架构来搭建项目,比如
1. 第一层,最底层是 common-公共能力层,用于存放公共基础能力集合(如工具库、公共配置等),一般是使用 HSP 包(动态共享包),这样它被项目中多个模块引入的话,也只会保留一个备份。
2. 第二层,是 features-基础特性层,用于存放基础特性集合(如应用中相对独立的各个功能的 UI 及业务逻辑实现等)
3. 顶层是,products-产品定制层,用于针对不同设备形态进行功能和特性集成
4. 界面级一多指的是一套代码可以适配不同尺寸、形态的设备,主要通过以下这些技术来实现
1. 自适应布局 等比拉伸缩放等等相关技术
2. 响应式布局 通过断点、媒体查询、栅格布局来实现
5. 能力级一多主要指的是不同硬件设备支持能力不一样,如蓝牙、摄像头、传感器等等。这些主要通过判断当前设置是否支持该能力来决定是否调用相关的 api 功能。如利用编辑器工具的智能提示、和代码中使用的 caniuse 或者 try-catch 进行判断使用。
:::
## 三层架构
::: details
1. 第一层,最底层是 common-公共能力层,用于存放公共基础能力集合(如工具库、公共配置等),一般是使用 HSP 包(动态共享包),这样它被项目中多个模块引入的话,也只会保留一个备份。
2. 第二层,是 features-基础特性层,用于存放基础特性集合(如应用中相对独立的各个功能的 UI 及业务逻辑实现等)
3. 顶层是,products-产品定制层,用于针对不同设备形态进行功能和特性集成
:::
## 录音有做过吗?avrecoder 有几种状态?
::: details
录音可以通过 AVRecorder 和 AudioCapturer 来实现。两者区别主要在支持录制声音的格式不同和控制录音文件的细小粒度不同上。AVRecorder 会简单一些,AudioCapturer 会复杂一些-还可以搭配 ai 语音功能使用
AVRecorder 主要有以下这些状态:
类型说明'idle'闲置状态。'prepared'参数设置完成'started'正在录制。'paused'录制暂停。'stopped'录制停止。'released'录制资源释放。'error'错误状态。

:::
## AVRecord 的录音步骤
::: details
1. 创建 AVRecorder 实例,实例创建完成进入 idle 状态。
2. 设置业务需要的监听事件,监听状态变化及错误上报。
3. 配置音频录制参数,调用 prepare()接口,此时进入 prepared 状态。
4. 开始录制,调用 start()接口,此时进入 started 状态。
:::
## 图片上传有做过吗?图片处理,旋转、缩放、图片保存有做过吗?
::: details
做过相册图片的上传(如果是沙箱内的图片只需要 1 个步骤即可,直接上传),流程主要有 3 个步骤,基于 photoAccessHelper 、CoreFileKit、NetworkKit 来实现的
1. photoAccessHelper 用来实现选择要上传的相册的图片
2. CoreFileKit 将相册图片拷贝到沙箱目录
3. NetworkKit 负责将沙箱目录内的图片上传到服务器上
图片处理,旋转、缩放、图片保存主要基于 Image Kit 来实现。它提供有
- 图片解码
- 指将所支持格式的存档图片解码成统一的 PixelMap,以便在应用或系统中进行图片显示或图片处理。
- PixelMap
- 指图片解码后无压缩的位图,用于图片显示或图片处理。
- 图片处理
- 指对 PixelMap 进行相关的操作,如旋转、缩放、设置透明度、获取图片信息、读写像素数据等。
- 图片编码
- 指将 PixelMap 编码成不同格式的存档图片,用于后续处理,如保存、传输等。
其中压缩图片是通过 一个 ImageKit 的 packing 函数,传入压缩比例(0-100)来是实现的。值越小体积越小
:::
## 视频有做过吗?
::: details
1. 如果是普通的视频播放直接使用 Video 组件来播放即可。功能相对弱一些
2. 如果是对视频播放进行神帝的一些处理,如流媒体、本地资源解析、媒体资源解封装、视频解码和自定义渲染的这些功能,可以使用 AVPlayer 来实现。
3. 如果类似做一个编辑视频的软件,那么就需要使用到对应的 CAPI 接口来实现了(调用底层 c++的能力)
:::
## 同事发给你代码,你怎么知道它的 bundlename
::: details
一般直接看 AppScope 中的字段就行
:::
## 鸿蒙如何和网页端通信?
::: details
1. 如果是应用的话,使用 web 组件和对应的 controller 的一些接口,如 [runJavaScript()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#runjavascript)和[registerJavaScriptProxy](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#registerjavascriptproxy)
2. 如果是元服务的话,使用 AtomicServiceWeb 来实现,因为 2025 年 1 月 22 日后不支持使用 web。还有 AtomicServiceWeb 没有了 web 中的如 [runJavaScript()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#runjavascript)和[registerJavaScriptProxy](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#registerjavascriptproxy)接口,但是它一样可以通过页面的 url 进行参数的传递和鸿蒙端提供了 js sdk,也可以很方便的让 h5 端调用鸿蒙端的功能
:::
## 跨域是怎么处理的?
::: details
> 跨域存在于不同源的浏览器和服务器的网络通信中,因为鸿蒙端嵌套了 web 组件,理解成就是一个浏览器,因此也会存在跨域
为了提高安全性,ArkWeb 内核不允许 file 协议或者 resource 协议访问 URL 上下文中来自跨域的请求。因此,在使用 Web 组件加载本地离线
资源的时候,Web 组件会拦截 file 协议和 resource 协议的跨域访问。
主要有两种解决方案
1. 将本地资源替换成网络资源,也就是 file 协议访问的是本地的资源,我们将本地资源放在网络上,通过 http 请求的方式来加载,然后在后端设置 cors 跨域即可。同时,开发者需利用 Web 组件的[onInterceptRequest](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-components-web-V5#oninterceptrequest9)方法,对本地资源进行拦截和相应的替换
2. 通过[setPathAllowingUniversalAccess 白名单](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#setpathallowinguniversalaccess12)设置一个路径列表。当使用 file 协议访问该列表中的资源时,允许进行跨域访问本地文件
:::
## 录音过程中息屏怎么处理?
::: details
可以通过申请[长时任务](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/continuous-task-V5),实现在后台长时间运行。长时任务支持的类型,包含数据传输、音视频播放、录制、定位导航、蓝牙相关、多设备互联、WLAN 相关、音视频通话和计算任务
开发步骤如下:
1. 需要申请 ohos.permission.KEEP_BACKGROUND_RUNNING 权限
2. 声明后台模式类型(录音等)
3. 通过 @ohos.resourceschedule.backgroundTaskManager 和@ohos.app.ability.wantAgent 进行编码处理
:::
## 有做过华为支付吗?
::: details
需要企业资质、需要在 AGC 平台上开通服务。

1. 商户客户端请求商户服务器创建商品订单。
2. 商户服务器按照商户模型调用 Payment Kit 服务端[直连商户预下单](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-prepay-V5)或[平台类商户/服务商预下单](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-agent-prepay-V5)接口。
3. 华为支付服务端返回预支付 ID(prepayId)。
4. 商户服务端组建订单信息参数[orderStr](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-model-V5#section159202591414)返回给商户客户端。
5. 商户客户端调用[requestPayment](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-paymentservice-V5#section192192415554)接口调起 Payment Kit 支付收银台。
6. Payment Kit 客户端展示收银台。
7. 用户通过收银台完成支付,Payment Kit 客户端会收到支付结果信息并请求 Payment Kit 服务端处理支付。
8. Payment Kit 服务端成功受理支付订单并异步处理支付。
9. Payment Kit 服务端将支付结果返回给 Payment Kit 客户端。
10. Payment Kit 客户端展示支付结果页。
11. 用户关闭支付结果页后 Payment Kit 客户端会返回支付状态给商户客户端。
12. 支付处理完成后,Payment Kit 服务端会调用回调接口返回支付结果信息给商户服务端。
13. 商户服务端收到支付结果回调响应后,使用[SM2 验签方式](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/payment-rest-overview-V5#section17670192215175)对支付结果进行验签。
:::
## 说一下多线程
::: details
[参考](https://juejin.cn/post/7435302345448259622)

:::
## HarmonyOS 中的生命周期
::: details
页面生命周期
1. onpageshow:页面每次显示时触发,包括路由过程、应用进入前台等场景。例如,用户从后台切换应用到前台,或者通过路由跳转到该页面时,此方法会被调用
2. onpagehide:页面每次隐藏时触发,包括路由过程、应用进入后台等场景。比如用户按下主页键将应用切换到后台,或者通过路由跳转到其他页面时,该页面的 onpagehide 方法会被执行
3. onbackpress:当用户点击返回按钮时触发。如果返回值为 true,表示页面自己处理返回逻辑,不进行页面路由;返回 false 则表示使用默认的路由返回逻辑,不设置返回值时按照 false 处理
4. abouttoappear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其 build () 函数之前执行。在该函数中可以修改变量,更改将在后续执行 build () 函数中生效
5. abouttodisappear:在自定义组件析构销毁之前执行。在此函数中不允许改变状态变量,特别是 @link 变量的修改可能会导致应用程序行为不稳定
组件生命周期独有的
1. abouttoappear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其 build () 函数之前执行。在该函数中可以修改变量,更改将在后续执行 build () 函数中生效
2. abouttodisappear:在自定义组件析构销毁之前执行。在此函数中不允许改变状态变量,特别是 @link 变量的修改可能会导致应用程序行为不稳定
UIAbility 生命周期
1. create 状态:在应用加载过程中,UIAbility 实例创建完成时触发,系统会调用 oncreate () 回调。可以在该回调中进行页面初始化操作,例如变量定义、资源加载等,用于后续的 UI 展示
2. windowstagecreate 状态:UIAbility 实例创建完成之后,在进入 foreground 之前,系统会创建一个 windowstage。windowstage 创建完成后会进入 onwindowstagecreate () 回调,可以在该回调中设置 UI 加载、设置 windowstage 的事件订阅,如获焦 / 失焦、可见 / 不可见等事件
3. foreground 状态:当 UIAbility 实例切换至前台时触发,对应于 onforeground () 回调。在 onforeground () 中可以申请系统需要的资源,或者重新申请在 onbackground 中释放的资源.
4. background 状态:当 UIAbility 实例切换至后台时触发,对应于 onbackground () 回调。在该回调中可以释放 UI 界面不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等.
5. windowstagedestroy 状态:在 UIAbility 实例销毁之前,会先进入 onwindowstagedestroy 回调,可以在该回调中释放 UI 界面资源
6. destroy 状态:在 UIAbility 实例销毁时触发,可以在 ondestroy () 回调中进行系统资源的释放、数据的保存等操作
:::
## 用 Entry 和 Navigation 装饰的页面有哪些区别
::: details
1. @Entry 装饰的页面是应用的入口页面,通常用于展示应用的初始界面,而 Navigation 组件是一个导航容器,挂载在单个页面下,支持跨模块的动态路由。
2. @Entry 页面具有通用的生命周期方法,而 Navigation 组件里的页面不执行 onPageShow、onPageHide 等生命周期回调。
:::
## HarmonyOS 中里面有几种包,分别有什么作用
::: details
HarmonyOS 中有三种类型的包:HAP(HarmonyOS Ability Package)、HAR(Harmony Archive)、HSP(Harmony Shared Package)。
1. HAP 是应用安装和运行的基本单元,分为 entry 和 feature 两种类型。
2. HAR 是静态共享包,用于代码和资源的共享。
3. HSP 是动态共享包,用于应用内共享代码和资源。
:::
## 简单介绍一下 Stage 模型
::: details
1. Stage 模型是 HarmonyOS 应用开发的基础架构,它提供了面向对象的开发方式,规范化了进程创建的方式,并提供组件化开发机制。
2. Stage 模型的组件天生具备分布式迁移和协同的能力,支持多设备形态和多窗口形态,重新定义了应用能力边界。
:::
## HarmonyOS 中的动画
::: details
HarmonyOS 提供了多种动画能力,包括属性动画、显式动画、转场动画、路径动画和粒子动画。
:::
## 如何进行路由页面传参
::: details
在 HarmonyOS 中,可以通过 router.pushUrl 方法跳转到目标页面,并携带参数。在进入被分享页面时,通过 router.getParams()来获取
传递的数据。此外,还可以使用 LocalStorage 等在页面间共享状态。
:::
## ArkTS 和 TS 的区别有哪些区别
::: details
ArkTS 是 HarmonyOS 优选的主力应用开发语言,它保持了 TypeScript 的基本风格,同时通过规范定义强化开发期静态检查和分析,提升程序执行稳定性和性能。ArkTS 与 TS 的主要区别在于 ArkTS 是静态类型的,而 TS 支持动态类型。ArkTS 在编译时进行类型检查,有助于在代码运行前发现和修复错误。
1. **[强制使用静态类型](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#强制使用静态类型)** **ArkTS 中禁止使用 any 类型。**
2. [禁止在运行时变更对象布局](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#禁止在运行时变更对象布局)
- 向对象中添加新的属性或方法。
- 从对象中删除已有的属性或方法。
- 将任意类型的值赋值给对象属性。
3. [限制运算符的语义](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/typescript-to-arkts-migration-guide-V5#限制运算符的语义)
```HTML
// 一元运算符`+`只能作用于数值类型:
let t = +42; // 合法运算
let s = +'42'; // 编译时错误
```
:::
## 常见装饰器
::: details
- [@State](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-state-V5):@State 装饰的变量拥有其所属组件的状态,可以作为其子组件单向和双向同步的数据源。当其数值改变时,会引起相关组件的渲染刷新。
- [@Prop](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-prop-V5):@Prop 装饰的变量可以和父组件建立单向同步关系,@Prop 装饰的变量是可变的,但修改不会同步回父组件。深拷贝。
- [@Link](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-link-V5):@Link 装饰的变量可以和父组件建立双向同步关系,子组件中@Link 装饰变量的修改会同步给父组件中建立双向数据绑定的数据源,父组件的更新也会同步给@Link 装饰的变量。
- [@Provide/@Consume](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-provide-and-consume-V5):@Provide/@Consume 装饰的变量用于跨组件层级(多层组件)同步状态变量,可以不需要通过参数命名机制传递,通过 alias(别名)或者属性名绑定。
- [@Observed](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-observed-and-objectlink-V5):@Observed 装饰 class,需要观察多层嵌套场景的 class 需要被@Observed 装饰。单独使用@Observed 没有任何作用,需要和@ObjectLink、@Prop 联用。
- [@ObjectLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-observed-and-objectlink-V5):@ObjectLink 装饰的变量接收@Observed 装饰的 class 的实例,应用于观察多层嵌套场景,和父组件的数据源构建双向同步。
- [AppStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5)是应用程序中的一个特殊的单例[LocalStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-localstorage-V5)对象,是应用级的数据库,和进程绑定,通过[@StorageProp](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5#storageprop)和[@StorageLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-appstorage-V5#storagelink)装饰器可以和组件联动。
- AppStorage 是应用状态的“中枢”,将需要与组件(UI)交互的数据存入 AppStorage,比如持久化数据[PersistentStorage](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-persiststorage-V5)和环境变量[Environment](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-environment-V5)。UI 再通过 AppStorage 提供的装饰器或者 API 接口,访问这些数据。
- 框架还提供了 LocalStorage,AppStorage 是 LocalStorage 特殊的单例。LocalStorage 是应用程序声明的应用状态的内存“数据库”,通常用于页面级的状态共享,通过[@LocalStorageProp](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-localstorage-V5#localstorageprop)和[@LocalStorageLink](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-localstorage-V5#localstoragelink)装饰器可以和 UI 联动。
:::
## 鸿蒙的 router 和 Navigation 的对比
::: details
1. router 最多页面栈为 32 个,Navigation 无限制
2. Navigation 支持一多开发,Auto 模式自适应单栏跟双栏显示
3. Navigation 支持获取指定页面参数
4. Navigation 清理指定路由
5. Navigation 支持路由拦截
:::
## 能力对比
::: details
| 业务场景 | Navigation | Router |
| :-------------------------------------------- | :-------------------------------------- | :------------------------------------------ |
| **一多能力** | **支持,Auto 模式自适应单栏跟双栏显示** | **不支持** |
| 跳转指定页面 | pushPath & pushDestination | pushUrl & pushNameRoute |
| 跳转 HSP 中页面 | 支持 | 支持 |
| 跳转 HAR 中页面 | 支持 | 支持 |
| 跳转传参 | 支持 | 支持 |
| **获取指定页面参数** | **支持** | **不支持** |
| 传参类型 | 传参为对象形式 | 传参为对象形式,对象中暂不支持方法变量 |
| 跳转结果回调 | 支持 | 支持 |
| 跳转单例页面 | 支持 | 支持 |
| 页面返回 | 支持 | 支持 |
| 页面返回传参 | 支持 | 支持 |
| 返回指定路由 | 支持 | 支持 |
| 页面返回弹窗 | 支持,通过路由拦截实现 | showAlertBeforeBackPage |
| 路由替换 | replacePath & replacePathByName | replaceUrl & replaceNameRoute |
| 路由栈清理 | clear | clear |
| **清理指定路由** | **removeByIndexes & removeByName** | 不支持 |
| 转场动画 | 支持 | 支持 |
| 自定义转场动画 | 支持 | 支持,动画类型受限 |
| 屏蔽转场动画 | 支持全局和单次 | 支持 设置 pageTransition 方法 duration 为 0 |
| geometryTransition 共享元素动画 | 支持(NavDestination 之间共享) | 不支持 |
| 页面生命周期监听 | UIObserver.on('navDestinationUpdate') | UIObserver.on('routerPageUpdate') |
| 获取页面栈对象 | 支持 | 不支持 |
| **路由拦截** | 支持通过 setInercption 做路由拦截 | 不支持 |
| 路由栈信息查询 | 支持 | getState() & getLength() |
| 路由栈 move 操作 | moveToTop & moveIndexToTop | 不支持 |
| 沉浸式页面 | 支持 | 不支持,需通过 window 配置 |
| 设置页面标题栏(titlebar)和工具栏(toolbar) | 支持 | 不支持 |
| 模态嵌套路由 | 支持 | 不支持 |
:::
## 页面下拉刷新和页面上拉加载
::: details
1. 下拉刷新可以使用[Refresh](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-refresh-V5)组件,它提供了 onStateChange 和 onRefreshing 事件 用来实现下拉刷新的业务
2. [List](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-list-V5#onreachend)、Scroll、Grid、WaterFall 等组件都提供了**上拉加载更多**事件,比如 List 组件的 onReachEnd 事件就是
:::
## 响应式布局
::: details
> [链接](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/responsive-layout-V5#栅格布局)
1. 断点
1. 在 在 UIAbility 的[onWindowStageCreate](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/uiability-lifecycle-V5)生命周期回调中监听 窗口尺寸变化事件,获取到当前窗口大小
2. 因为窗口大小单位是 px,需要调用 px2vp 函数转成 vp
3. 然后存到 AppStorage 中
4. 最后页面 使用 AppStorage 即可
2. 媒体查询
1. 主要通过 mediaquery 结合 断点来使用
3. 栅格布局
1. 通过 GridRow 和 GridCol 来实现
2. 一列分成了 12 份, 结合栅格组件默认提供 xs、sm、md、lg 四个断点
:::
## 断点续传
::: details
> 鸿蒙发送网络请求有两套方案
>
> 1. Request , 我们使用的 axios 就是 基于它封装的
> 2. RCP ,Remote Communication Kit(远场通信服务)是华为提供的 HTTP 发起数据请求的 NAPI 封装 目前新项目再推动它
> 3. [远场通信场景](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-scenario-V5)
> 1. [获取服务器资源](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-getserverresources-V5)
> 2. [发送数据到服务器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-senddatatoserver-V5)
> 3. [断点续传](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-httpresume-V5)
> 4. [双向证书校验](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-certificateverification-V5)
> 5. [拦截器](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-interceptor-V5)
> 6. [使用自定义证书校验](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-validation-V5)
> 7. [上传下载文件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-updownload-V5)
> 8. [设置 TLS 版本号和加密套件](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-settls-V5)
> 9. [获取服务器资源 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-getserverresources-c-V5)
> 10. [发送数据到服务器 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-senddatatoserver-c-V5)
> 11. [断点续传 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-httpresume-c-V5)
> 12. [双向证书校验 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-certificateverification-c-V5)
> 13. [拦截器 (C/C++)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-interceptor-c-V5)
> 14. [证书锁定](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-validationlock-V5)
> 15. [响应校验](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-responsecheck-V5)
> 16. [读写超时](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-readtimeout-V5)
> 17. [请求暂停和恢复](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-pause-resume-V5)
> 18. [同步读写流](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/remote-communication-asyncreadwrite-V5)
:::
### [断点续传](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/re
::: details
mote-communication-httpresume-V5)
1. 利用了远场通信 RemoteCommunicationKit
2. 发送网络请求,利用[TransferRange](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/remote-communication-rcp-V5#section838945575618)的 from 和 to 属性 进行截取下载内容,拼接到文件上即可
:::
## 双向证书校验
::: details
> 用于验证服务端和客户端之间的身份和数据完整性,确保通信的安全性。
1. 导入远场通信模块和文件读写模块
1. ```HTML
import { rcp } from '@kit.RemoteCommunicationKit';
import { fileIo } from '@kit.CoreFileKit';
```
2. 使用文件读写模块 读取存在客户端的证书
1. ```HTML
// 读取
fileIo.read(file.fd
// 存到字符串 content中
// 将读取的数据转换为字符串
let content = String.fromChar
```
3. 然后调用给远场通信的 configuration 方法设置到 security.certificate.content 属性中
1. ```HTML
request.configuration = {
security: {
certificate: {
content: content,
```
:::
## 项目优化
::: details
- **图片懒加载**:列表里的图片滑到可见区域再加载,减少内存占用。
- **数据缓存**:用`Preferences`或数据库缓存首页数据,下次启动先展示缓存再刷新。
- **减少布局嵌套**:用@Builder 代替自定义组件,多用线性布局,少用 flex 等弹性布局
- **线程管理**:把 JSON 解析、图片解码丢到`Worker`线程,防止主线程卡顿。
- **内存泄漏排查**:用 DevEco Studio 的 Profiler 工具,发现有个页面退出后监听器没注销,赶紧加了`onPageHide`里的释放逻辑。
:::
## forEach 和 LazyForEach
::: details
1. forEach 会把数据全部渲染出来
2. LazyForEach 只会渲染可视区域
:::
## LazyForEach 如何实现更新
::: details
- **数据源绑定**:`LazyForEach` 需要与实现了 `IDataSource` 接口的数据源(如 `LazyDataSource`)绑定。当数据源发生变化(增、删、改)时,框架会自动触发更新。
- **观察者模式**:数据源通过 `DataChangeListener` 通知 `LazyForEach` 数据变更。只有实际变化的项会触发局部更新,而非重新渲染整个列表。
:::
## Class 和 interface 的区别
::: details
1. Interface 只能定义类型,class 可以定义类型和保护功能实现
2. interface 可以同时继承多个接口,class 只能同时继承一个父类
3. 工作中两个都用,比如用 class 来封装了一些工具库 avplayer、首选项、全屏-沉浸式、axios 等
:::
## AVPlayer 的播放步骤
::: details
1. 创建实例 createAVPlayer(),AVPlayer 初始化 idle 状态。
2. 设置业务需要的监听事件
| 事件类型 | 说明 |
| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| stateChange | 必要事件,监听播放器的 state 属性改变。 |
| error | 必要事件,监听播放器的错误信息。 |
| durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
| timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
| seekDone | 响应 API 调用,监听 seek()请求完成情况。当使用 seek()跳转到指定播放位置后,如果 seek 操作成功,将上报该事件。 |
| speedDone | 响应 API 调用,监听 setSpeed()请求完成情况。当使用 setSpeed()设置播放倍速后,如果 setSpeed 操作成功,将上报该事件。 |
| volumeChange | 响应 API 调用,监听 setVolume()请求完成情况。当使用 setVolume()调节播放音量后,如果 setVolume 操作成功,将上报该事件。 |
| bufferingUpdate | 用于网络播放,监听网络播放缓冲信息,用于上报缓冲百分比以及缓存播放进度。 |
| audioInterrupt | 监听音频焦点切换信息,搭配属性 audioInterruptMode 使用。如果当前设备存在多个音频正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
3. 设置资源:设置属性 url,AVPlayer 进入 initialized 状态。
4. 准备播放:调用 prepare(),AVPlayer 进入 prepared 状态,此时可以获取 duration,设置音量。
5. 音频播控:播放 play(),暂停 pause(),跳转 seek(),停止 stop() 等操作。
6. (可选)更换资源:调用 reset()重置资源,AVPlayer 重新进入 idle 状态,允许更换资源 url。
7. 退出播放:调用 release()销毁实例,AVPlayer 进入 released 状态,退出播放。
:::
## 手动签名和自动签名的区别
::: details
:::
### **核心区别总结**
::: details
| **对比维度** | **自动签名** | **手动签名** |
| ---------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
| **适用场景** | 单设备调试(单真机可用) | 多设备调试、断网环境调试 |
| **签名证书管理** | 由 DevEco Studio 自动生成签名证书并绑定当前设备 UDID | 需在**AGC**控制台申请调试证书\*\*\*\*、注册调试设备 UDID、配置调试 Profile |
| **安装限制** | 仅允许当前绑定的设备安装 | 支持注册的所有调试设备安装 |
| **权限支持** | **不支持**受限开放权限(如健康服务) | 支持受限权限(需通过 AGC 审核并提交场景说明) |
| **发布用途** | **禁止用于发布** | 可生成与发布版本一致的签名包(需替换为正式证书) |
| **受限服务依赖** | 无法使用部分依赖签名的开放能力(如 Health Kit) | 支持所有开放能力 |
| **公钥指纹管理** | 自动生成调试指纹,需在发布前手动更新为发布指纹 | 需手动维护调试和发布的指纹 |
:::
## webview 的性能优化(怎么加快 webview 的响应速度)
::: details
- 可以通过[prepareForPageLoad()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#prepareforpageload10)来预解析或者预连接将要加载的页面
- 能够预测到 Web 组件将要加载的页面或者即将要跳转的页面。可以通过[prefetchPage()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5#prefetchpage10)来预加载即将要加载页面
- 可以通过 prefetchResource()预获取将要加载页面中的 post 请求。在页面加载结束时,可以通过 clearPrefetchedResource()清除后续不再使用的预获取资源缓存
- 预编译生成编译缓存 可以通过 precompileJavaScript()在页面加载前提前生成脚本文件的编译缓存。
:::
================================================
FILE: docs/second-exam/engineering.md
================================================
# 前端工程化
前端工程化通过自动化工具和标准化流程,提升开发效率、代码质量和可维护性。其核心目标是优化开发、构建、测试和部署流程,减少人工干预和重复劳动,便于项目扩展和团队协作。
常见的工具,如Vite和Webpack,提供高效的构建和打包能力,显著提升开发效率并丰富前端生态。这些工具的广泛应用使前端开发更加高效,且成为近年来面试中的热门话题。
::: tip
如有疑问,可免费 [加群](/docs/services/group.md) 讨论咨询,也可参与 [1v1 面试咨询服务](/docs/services/1v1.md), 专业、系统、高效、全流程 准备前端面试
:::
## Vite为什么更快?
::: details 参考答案
Vite 相比传统构建工具(如 Webpack)更快🚀,主要得益于以下几个核心特性:
- 基于原生 ES 模块(ESM):Vite 利用浏览器原生的 ES 模块,在开发模式下`按需加载`模块,避免了整体打包,从而减少了启动时间。它通过只编译实际修改的文件,提升了开发过程中的反馈速度。
- 高效的热模块替换(HMR):Vite 在开发模式下利用原生 ES 模块实现模块级的热更新。当文件发生变化时,Vite 只会重新加载发生变化的模块,而不是重新打包整个应用,极大提高了热更新的速度。
- 使用 esbuild 进行快速编译:Vite 默认使用 esbuild 作为编译工具,相比传统的 JavaScript 编译工具(如 Babel、Terser),esbuild 提供显著的性能提升,能够快速完成代码转换和压缩,从而加速开发和构建过程。
- 现代 JavaScript 特性支持:Vite 在生产环境中使用 Rollup 构建,支持优秀的树摇和代码拆分,有效减小构建体积。同时,Vite 利用现代浏览器特性(如动态导入、ES2015+ 模块),减少了 polyfill 的使用,提升了加载速度。
- 预构建和缓存:Vite 在开发时会预构建常用依赖(如 Vue、React),并将其转换为浏览器可执行的格式,避免每次启动时重新编译。同时,Vite 会缓存这些预构建的依赖,并在启动时复用缓存,从而加快启动速度。
:::
## vite中如何使用环境变量?
::: details 参考答案
根据当前的代码环境变化的变量就叫做**环境变量**。比如,在生产环境和开发环境将BASE_URL设置成不同的值,用来请求不同的环境的接口。
Vite内置了 `dotenv` 这个第三方库, dotenv会自动读取 `.env` 文件, dotenv 从你的 `环境目录` 中的下列文件加载额外的环境变量:
> .env # 所有情况下都会加载
> .env.[mode] # 只在指定模式下加载
默认情况下
- `npm run dev` 会加载 `.env` 和 `.env.development` 内的配置
- `npm run build` 会加载 `.env` 和 `.env.production` 内的配置
- `mode` 可以通过命令行 `--mode` 选项来重写。
环境变量需以 VITE\_ 前缀定义,且通过 `import.meta.env` 访问。
示例:
.env.development:
```js
VITE_API_URL = 'http://localhost:3000'
```
在代码中使用:
```js
console.log(import.meta.env.VITE_API_URL) // http://localhost:3000
```
> 参考博文:[vite中环境变量的使用与配置](https://juejin.cn/post/7172012247852515335)
:::
## vite如何实现根据不同环境(qa、dev、prod)加载不同的配置文件?
::: details 参考答案
在 Vite 中,根据不同环境设置不同配置的方式,类似于 Webpack 时代的配置方法,但更加简化。Vite 使用 `defineConfig` 函数,通过判断 `command` 和 `mode` 来加载不同的配置。
- **通过 `defineConfig` 动态配置:**
Vite 提供的 `defineConfig` 函数可以根据 `command` 来区分开发环境( `serve` )和生产环境( `build` ),并返回不同的配置。
```javascript
import { defineConfig } from 'vite'
import viteBaseConfig from './vite.base.config'
import viteDevConfig from './vite.dev.config'
import viteProdConfig from './vite.prod.config'
export default defineConfig(({ command, mode, ssrBuild }) => {
if (command === 'serve') {
// 开发环境独有配置
return {
...viteBaseConfig,
...viteDevConfig,
}
} else {
// 生产环境独有配置
return {
...viteBaseConfig,
...viteProdConfig,
}
}
})
```
- **创建不同的配置文件**
`vite.base.config.ts` :基础配置,适用于所有环境。
```javascript
import {
defineConfig
} from "vite";
export default defineConfig({
// 基础配置->使用所有场景
return {
plugins: [
vue()
],
}
});
```
`vite.dev.config.ts` :开发环境配置。
```javascript
import { defineConfig } from 'vite'
export default defineConfig({
// 开发环境专有配置
})
```
`vite.prod.config.ts` :生产环境配置。
```javascript
import { defineConfig } from 'vite'
export default defineConfig({
// 生产环境专有配置
})
```
> 参考博文:[vite指定配置文件及其在多环境下的配置集成方案](https://juejin.cn/post/7172009616967942175)
:::
## 简述Vite的依赖预加载机制。
::: details 参考答案
Vite 的依赖预构建机制通过在开发模式下提前处理常用依赖(如 Vue、React 等),将这些依赖转换为浏览器可以直接执行的格式。这避免了每次启动时重新编译这些依赖,显著提升了启动速度。预构建的依赖被缓存,并在后续启动时复用缓存,进一步加速了开发过程中的构建和启动时间。
具体来说,它的工作原理如下:
- **依赖识别和路径补全**: Vite 会首先识别项目中需要的依赖,并对非绝对路径或相对路径的引用进行路径补全。比如,`Vue` 的加载路径会变为 `node_modules/.vite/deps/Vue.js?v=1484ebe8`,这一路径显示了 Vite 在 `node_modules/.vite/deps` 文件夹下存放了经过预处理的依赖文件。
- **转换成 ES 模块**: 一些第三方包(特别是遵循 CommonJS 规范的包)在浏览器中无法直接使用。为了应对这种情况,Vite 会使用 **esbuild** 工具将这些依赖转换为符合 ES 模块规范的代码。转换后的代码会被存放在 `node_modules/.vite/deps` 文件夹下,这样浏览器就能直接识别并加载这些依赖。
- **统一集成 ES 模块**: Vite 会对每个包的不同模块进行统一集成,将各个分散的模块(如不同的 ES 函数或组件)合并成一个或几个文件。这不仅减少了浏览器发起多个请求的次数,还能够加快页面加载速度。
> 参考博文:[vite的基础使用及其依赖预加载机制](https://juejin.cn/post/7172007612379054093#heading-3)、[手写vite让你深刻了解Vite的文件加载原理](https://juejin.cn/post/7178803290820804667)
:::
## vite中如何加载、处理静态资源?
::: details 参考答案
🎯 **静态资源目录(public 目录)**:
- 静态资源可以放在 `public` 目录下,这些文件不会经过构建处理,直接按原样复制到输出目录。在开发时可以通过 `/` 路径直接访问,如 `/icon.png`。
- `public` 目录可通过 `vite.config.js` 中的 `publicDir` 配置项修改。
🎯 **资源引入**:
- **图片、字体、视频**:通过 `import` 引入,Vite 会自动将其处理为 URL 并生成带哈希值的文件名。在开发时,引用会是根路径(如 `/img.png`),在生产构建后会是如 `/assets/img.2d8efhg.png` 的路径。
- **CSS、JS**:CSS 会被自动注入到页面中,JS 按模块处理。
🎯 **强制作为 URL 引入**:通过 `?url` 后缀可以显式强制将某些资源作为 URL 引入。
```js
import imgUrl from './img.png?url'
```
🎯 **强制作为原始内容引入**:通过 `?raw` 后缀将文件内容作为字符串引入。
🎯 `new URL()` :通过 `import.meta.url` 可以动态构建资源的 URL,这对于一些动态路径很有用。
```js
const imgUrl = new URL('./img.png', import.meta.url).href
document.getElementById('hero-img').src = imgUrl
```
> 参考博文:[vite中静态资源(css、img、svg等)的加载机制及其相关配](https://juejin.cn/post/7173467405522305055)
:::
## 如何在Vite项目中引入CSS预处理器?
::: details 参考答案
在 Vite 中使用 CSS 预处理器(如 Sass、Less)是非常简单的,Vite 默认支持这些预处理器,我们只需要安装相应的依赖即可。
安装依赖:
```js
npm install sass--save - dev
```
在 Vue 组件中使用:
```vue
```
此外,我们可以通过在vite的 `preprocessorOptions` 中进行配置,使用CSS 预处理器的一些强大功能。
对于 Less,假如我们需要在项目中全局使用某些变量,我们可以在 `vite.config.js` 中配置 `globalVars` ,使得变量在所有文件中无需单独引入:
```javascript
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
css: {
preprocessorOptions: {
less: {
globalVars: {
blue: '#1CC0FF', // 定义全局变量
},
},
},
},
})
```
一旦配置了全局变量,我们就可以在任何 Vue 组件中直接使用它,无需再次引入:
```vue
```
> 参考博文:[vite中如何更优雅的使用css](https://juejin.cn/post/7175366648659411000)、[Vite中预处理器(如less)的配置](https://juejin.cn/post/7177549666291515447)、[使用postcss完善vite项目中的css配置](https://juejin.cn/post/7178454300572516409)
:::
## vite中可做的项目优化有哪些?
::: details 参考答案
1️⃣ 启用 Gzip/Brotli 压缩
使用 `vite-plugin-compression` 插件开启 Gzip 或 Brotli 压缩,可以有效减小传输的文件体积,提升加载速度。
安装依赖:
```javascript
npm install vite - plugin - compression--save - dev
```
配置示例:
```javascript
import compression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
compression({
algorithm: 'gzip', // 或 'brotli' 压缩
threshold: 10240, // 文件大于 10KB 时启用压缩
}),
],
})
```
> 参考博文:[vite打包优化vite-plugin-compression的使用](https://juejin.cn/post/7222901994840244279)
2️⃣ 代码分割
- 🎯 路由分割
使用动态导入实现按需加载,减小初始包的体积,提高页面加载速度。
```javascript
const module = import('./module.js') // 动态导入
```
或者在路由中使用懒加载:
```javascript
const MyComponent = () => import('./MyComponent.vue')
```
- 🎯 手动控制分包
在 Vite 中,你可以通过配置 Rollup 的 `manualChunks` 选项来手动控制如何分割代码。这个策略适用于想要将特定的依赖或模块提取成单独的 chunk 文件。
```javascript
import { defineConfig } from 'vite'
export default defineConfig({
build: {
minify: false,
// 在这里配置打包时的rollup配置
rollupOptions: {
manualChunks: (id) => {
if (id.includes('node_modules')) {
return 'vendor'
}
},
},
},
})
```
> 参考博文:[Vite性能优化之分包策略](https://juejin.cn/post/7177982374259949624)
3️⃣ 图片优化
使用 `vite-plugin-imagemin` 插件对项目中的图片进行压缩,减少图片体积,提升加载速度。
```javascript
npm install vite - plugin - imagemin--save - dev
```
```javascript
export default defineConfig({
plugins: [
ViteImagemin({
gifsicle: {
optimizationLevel: 3,
},
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 85,
},
pngquant: {
quality: [0.65, 0.9],
},
}),
],
})
```
4️⃣ 依赖优化
配置 Vite 的 `optimizeDeps` 选项,提前预构建常用依赖,减少开发环境下的启动时间。
```javascript
export default defineConfig({
optimizeDeps: {
include: ['lodash', 'vue', 'react'], // 预构建依赖
},
})
```
> 参考博文:[vite的基础使用及其依赖预加载机制](https://juejin.cn/post/7172007612379054093#heading-3)
:::
## 简述vite插件开发流程?
::: details 参考答案
Vite 插件开发基于 Rollup 插件系统,因此其生命周期和钩子与 Rollup 插件非常相似。以下是开发流程和关键步骤:
1️⃣ **理解插件生命周期**
Vite 插件有一系列生命周期钩子,每个钩子对应不同的功能需求,主要钩子包括:
- **config**:用于修改 Vite 配置,通常在构建或开发过程中使用。
- **configureServer**:用于修改开发服务器的行为,如自定义请求处理。
- **transform**:对文件内容进行转换,适用于文件类型转换或代码处理。
- **buildStart** 和 **buildEnd**:在构建过程开始和结束时触发,适用于日志记录或优化操作。
插件开发的核心是根据具体需求,在合适的生命周期钩子中实现业务逻辑。
2️⃣ **插件基本结构**
Vite 插件的基本结构如下:
```javascript
export default function myVitePlugin() {
return {
name: 'vite-plugin-example', // 插件名称
config(config) {
// 修改 Vite 配置
},
configureServer(server) {
// 修改开发服务器行为
},
transform(src, id) {
// 对文件内容进行转换
},
}
}
```
插件对象必须包含一个 `name` 属性,用于标识插件,还可以根据需求实现其他钩子。
3️⃣ **插件开发**
在插件开发过程中,根据需求实现不同的钩子逻辑。例如,假设我们需要创建一个插件来处理自定义文件类型并将其转换为 JavaScript:
```javascript
const fileRegex = /\.(my-file-ext)$/
export default function transformFilePlugin() {
return {
name: 'vite-plugin-transform-file',
transform(src, id) {
if (fileRegex.test(id)) {
return {
code: compileFileToJS(src), // 将文件内容转换为 JavaScript
map: null, // 可以返回 source map
}
}
},
}
}
```
- **transform**:此钩子对符合 `fileRegex` 正则表达式的文件(`.my-file-ext`)进行转换,并返回转换后的 JavaScript 代码。
4️⃣ **插件使用**
插件开发完成后,可以在 Vite 配置中使用:
```javascript
import transformFilePlugin from 'vite-plugin-transform-file'
export default {
plugins: [transformFilePlugin()],
}
```
5️⃣ **发布插件**
开发完成后,插件可以通过 npm 发布,或者将其托管在 GitHub 上,方便团队或社区使用。
> 参考博文:[https://juejin.cn/post/7270528132167417915](https://juejin.cn/post/7270528132167417915)
:::
## 如何在Vite中配置代理?
::: details 参考答案
在 Vite 中配置代理可以通过 `server.proxy` 选项来实现。以下是一个示例配置:
```javascript
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
// 代理 /api 请求到目标服务器
'/api': {
target: 'http://localhost:5000', // 目标服务器地址
changeOrigin: true, // 修改请求头中的 Origin 字段为目标服务器的 origin
secure: false, // 是否允许 HTTPS 请求
rewrite: (path) => path.replace(/^\/api/, ''), // 重写请求路径,将 /api 替换为空
},
// 代理某些静态资源请求
'/assets': {
target: 'http://cdn-server.com', // 目标是静态资源服务器
changeOrigin: true,
rewrite: (path) => path.replace(/^\/assets/, '/static'), // 将 /assets 路径重写为 /static
},
},
},
})
```
:::
## Vite如何集成TypeScript?如何配置?
::: details 参考方案
Vite 对 TypeScript 提供了开箱即用的支持,无需额外安装插件。
我们创建一个 `index.html` 文件并引入 `main.ts` 文件:
```javascript
```
在 `main.ts` 中,可以写入一些 TypeScript 代码:
```javascript
let tip: string = "这是一个vite项目,使用了ts语法";
console.log('tip: ', tip);
```
运行 `vite` 后,可以看到控制台输出内容,表明 Vite 天生支持 TypeScript。
在 Vite 项目中,虽然默认支持 TypeScript,但 Vite 本身不会阻止编译时出现 TypeScript 错误。为了更严格的类型检查和错误提示,我们需要配置 TypeScript。
- 添加 TypeScript 配置(如果没有)
通过以下命令生成 `tsconfig.json` 配置文件
```plain
npx tsc --init
```
创建好 `tsconfig.json` 后,Vite 会根据该配置文件来编译 TypeScript。
- 强化 TypeScript 错误提示
Vite 默认不会阻止编译时的 TypeScript 错误。如果我们想要在开发时严格检查 TypeScript 错误并阻止编译,可以使用 `vite-plugin-checker` 插件。
```javascript
npm i vite - plugin - checker--save - dev
```
然后在 `vite.config.ts` 中引入并配置该插件:
```typescript
// vite.config.ts
import checker from 'vite-plugin-checker'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [checker({ typescript: true })],
})
```
这样,任何 TypeScript 语法错误都会在控制台显示,并阻止编译。
- 打包时进行 TypeScript 检查
虽然 Vite 只会执行 `.ts` 文件的转译,而不会执行类型检查,但我们可以通过以下方式确保在打包时进行 TypeScript 类型检查。
修改 `package.json` 配置
```json
{
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build"
}
}
```
`tsc --noEmit` 会执行类型检查,但不会生成编译后的文件。如果存在类型错误,打包过程会被阻止。
- TypeScript 智能提示
Vite 默认为 `import.meta.env` 提供了类型定义,但是对于自定义的 `.env` 文件,TypeScript 的智能提示默认不生效。为了实现智能提示,可以在 `src` 目录下创建一个 `env.d.ts` 文件:
```typescript
///
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_APP_HAHA: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
```
> 参考博文:https://juejin.cn/post/7177210200330829885
:::
## 什么是 Webpack?它的作用是什么?
参考答案
::: details
Webpack 是一个开源的 **前端静态模块打包工具**,主要用于将现代 JavaScript 应用中的各种资源(代码、样式、图片等)转换为优化的静态文件。它是现代前端开发的核心工具之一,尤其在复杂项目中扮演着关键角色。
**Webpack 的核心作用**
1. **模块化支持**
- **解决问题**:将代码拆分为多个模块(文件),管理依赖关系。
- **支持语法**:
- ES Modules ( `import/export` )
- CommonJS ( `require/module.exports` )
- AMD 等模块化方案。
```javascript
// 模块化开发
import Header from './components/Header.js'
import styles from './styles/main.css'
```
2. **资源整合**
- **处理非 JS 文件**:将 CSS、图片、字体、JSON 等资源视为模块,统一管理。
```javascript
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg)$/,
type: 'asset/resource',
},
],
},
}
```
3. **代码优化**
- **功能**:
- **Tree Shaking**:删除未使用的代码。
- **代码分割(Code Splitting)**:按需加载代码,减少首屏体积。
- **压缩**:减小文件体积,提升加载速度。
```javascript
// 动态导入实现按需加载
button.addEventListener('click', () => {
import('./module.js').then((module) => module.run())
})
```
4. **开发工具集成**
- **功能**:
- **热更新(HMR)**:实时预览代码修改效果。
- **Source Map**:调试时映射压缩代码到源代码。
- **本地服务器**:快速启动开发环境。
```javascript
devServer: {
hot: true, // 启用热更新
open: true, // 自动打开浏览器
},
devtool: 'source-map', // 生成 Source Map
```
5. **生态扩展**
- **Loader**:处理特定类型文件(如 `.scss` → `.css` )。
- **Plugin**:优化构建流程(如生成 HTML、压缩代码)。
```javascript
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin(),
],
```
**Webpack 的工作流程**
1. **入口(Entry)**:从指定文件(如 `index.js`)开始分析依赖。
2. **依赖图(Dependency Graph)**:递归构建模块间的依赖关系。
3. **加载器(Loaders)**:转换非 JS 资源(如编译 Sass、处理图片)。
4. **插件(Plugins)**:在构建生命周期中执行优化任务。
5. **输出(Output)**:生成优化后的静态文件(如 `bundle.js`)。
**与其他工具对比**
| **工具** | **定位** | **与 Webpack 的区别** |
|----------------|-----------------------------|-------------------------------------------|
| Gulp/Grunt | 任务运行器(Task Runner) | 处理文件流,但无模块化支持 |
| Rollup | 库打包工具 | 更适合库开发,Tree Shaking 更激进 |
| Vite | 新一代构建工具 | 基于原生 ESM,开发环境更快,生产依赖 Rollup |
**适用场景**
- **单页应用(SPA)**:如 React、Vue、Angular 项目。
- **复杂前端工程**:多页面、微前端架构。
- **静态网站生成**:结合 Markdown、模板引擎使用。
Webpack 通过 **模块化整合**、**代码优化** 和 **开发效率提升**,解决了前端工程中资源管理混乱、性能瓶颈和开发体验差的问题。它不仅是打包工具,更是现代前端工程化的基础设施。
:::
## 如何使用 Webpack 配置多环境的不同构建配置?
参考答案
::: details
在 Webpack 中配置多环境(如开发环境、测试环境、生产环境)的构建配置,可以通过 **环境变量注入** 和 **配置合并** 的方式实现。
**步骤 1:安装依赖工具**
```bash
npm install webpack-merge cross-env --save-dev
```
- **webpack-merge**:用于合并基础配置和环境专属配置。
- **cross-env**:跨平台设置环境变量(兼容 Windows 和 macOS/Linux)。
**步骤 2:创建配置文件结构**
```
project/
├── config/
│ ├── webpack.common.js # 公共配置
│ ├── webpack.dev.js # 开发环境配置
│ └── webpack.prod.js # 生产环境配置
├── src/
│ └── ... # 项目源码
└── package.json
```
**步骤 3:编写公共配置 ( `webpack.common.js` )**
```javascript
// config/webpack.common.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../dist'),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
}
```
**步骤 4:编写环境专属配置**
开发环境 ( `webpack.dev.js` )
```javascript
// config/webpack.dev.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const webpack = require('webpack')
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
devServer: {
hot: true,
open: true,
port: 3000,
},
plugins: [
// 注入环境变量(可在代码中通过 process.env.API_URL 访问)
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify('https://dev.api.com'),
'process.env.NODE_ENV': JSON.stringify('development'),
}),
],
})
```
生产环境 ( `webpack.prod.js` )
```javascript
// config/webpack.prod.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const webpack = require('webpack')
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
optimization: {
minimizer: [
'...', // 保留默认的 JS 压缩配置
new CssMinimizerPlugin(),
],
},
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify('https://prod.api.com'),
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
})
```
**步骤 5:配置 `package.json` 脚本**
```json
{
"scripts": {
"start": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js",
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
"build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js"
}
}
```
**步骤 6:在代码中使用环境变量**
```javascript
// src/index.js
console.log('当前环境:', process.env.NODE_ENV)
console.log('API 地址:', process.env.API_URL)
// 根据不同环境执行不同逻辑
if (process.env.NODE_ENV === 'development') {
console.log('这是开发环境')
} else {
console.log('这是生产环境')
}
```
**步骤 7:运行命令**
```bash
# 启动开发服务器(热更新)
npm run start
# 构建开发环境产物
npm run build:dev
# 构建生产环境产物
npm run build:prod
```
**扩展:支持更多环境(如测试环境)**
1. 创建 `webpack.stage.js`
```javascript
// config/webpack.stage.js
const { merge } = require('webpack-merge')
const common = require('./webpack.common.js')
const webpack = require('webpack')
module.exports = merge(common, {
mode: 'production',
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify('https://stage.api.com'),
'process.env.NODE_ENV': JSON.stringify('staging'),
}),
],
})
```
2. 添加 `package.json` 脚本
```json
{
"scripts": {
"build:stage": "cross-env NODE_ENV=staging webpack --config config/webpack.stage.js"
}
}
```
| **配置项** | **开发环境** | **生产环境** | **测试环境** |
| ------------ | --------------------- | ---------------------- | ----------------------- |
| `mode` | `development` | `production` | `production` |
| `devtool` | `eval-source-map` | `source-map` | `source-map` |
| `devServer` | ✅ 启用 | ❌ 不启用 | ❌ 不启用 |
| **代码压缩** | ❌ 不压缩 | ✅ CSS/JS 压缩 | ✅ CSS/JS 压缩 |
| **环境变量** | `API_URL=dev.api.com` | `API_URL=prod.api.com` | `API_URL=stage.api.com` |
:::
## Webpack 的核心概念有哪些?请简单解释。
参考答案
::: details
Webpack 的核心概念是理解其工作原理和配置的基础,以下是它们的简要解释:
**1. 入口(Entry)**
- **作用**:定义 Webpack **构建依赖图的起点**,通常为项目的主文件(如 `index.js`)。
```javascript
entry: './src/index.js', // 单入口
entry: {
app: './src/app.js',
admin: './src/admin.js'
}, // 多入口
```
**2. 出口(Output)**
- **作用**:指定打包后的资源**输出位置和命名规则**。
```javascript
output: {
filename: '[name].bundle.js', // 输出文件名([name] 为入口名称)
path: path.resolve(__dirname, 'dist'), // 输出目录(绝对路径)
clean: true, // 自动清理旧文件(Webpack 5+)
}
```
**3. 加载器(Loaders)**
- **作用**:让 Webpack **处理非 JavaScript 文件**(如 CSS、图片、字体等),将其转换为有效模块。
```javascript
module: {
rules: [{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}, // 处理 CSS
{
test: /\.(png|svg)$/,
type: 'asset/resource'
}, // 处理图片(Webpack 5+)
],
}
```
**4. 插件(Plugins)**
- **作用**:扩展 Webpack 功能,干预**整个构建流程**(如生成 HTML、压缩代码、提取 CSS)。
```javascript
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}), // 生成 HTML
new MiniCssExtractPlugin(), // 提取 CSS 为独立文件
]
```
**5. 模式(Mode)**
- **作用**:预设优化策略,区分**开发环境**(`development`)和**生产环境**(`production`)。
```javascript
mode: 'production', // 启用代码压缩、Tree Shaking 等优化
```
**6. 模块(Modules)**
- **作用**:Webpack 将每个文件视为**模块**(如 JS、CSS、图片),通过依赖关系构建依赖图。
- **特点**:支持 ESM、CommonJS、AMD 等模块化语法。
**7. 代码分割(Code Splitting)**
- **作用**:将代码拆分为多个文件(chunks),实现**按需加载**或**并行加载**,优化性能。
- **实现方式**:
- 动态导入(`import()`)
- 配置 `optimization.splitChunks`
**8. Tree Shaking**
- **作用**:通过静态分析**移除未使用的代码**,减小打包体积。
- **前提**:使用 ES Module(`import/export`),并启用生产模式(`mode: 'production'`)。
:::
## 如何在 Webpack 中实现 CSS 和 Sass 的处理?
参考答案
::: details
在 Webpack 中处理 CSS 和 Sass(SCSS)需要配置相应的加载器(loaders)和插件(plugins)。
**1. 安装所需依赖**
```bash
npm install --save-dev \
style-loader \
css-loader \
sass-loader \
sass \
postcss-loader \
autoprefixer \
mini-css-extract-plugin \
css-minimizer-webpack-plugin
```
- **核心依赖**:
- `style-loader`:将 CSS 注入 DOM。
- `css-loader`:解析 CSS 文件中的 `@import` 和 `url()`。
- `sass-loader`:将 Sass/SCSS 编译为 CSS。
- `sass`:Sass 编译器(Dart Sass 实现)。
- **可选工具**:
- `postcss-loader` 和 `autoprefixer`:自动添加浏览器前缀。
- `mini-css-extract-plugin`:提取 CSS 为独立文件(生产环境推荐)。
- `css-minimizer-webpack-plugin`:压缩 CSS(生产环境推荐)。
**2. 基础 Webpack 配置**
在 `webpack.config.js` 中添加以下规则和插件:
**配置 CSS 和 SCSS 处理**
```javascript
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
module: {
rules: [
// 处理 CSS 文件
{
test: /\.css$/,
use: [
// 开发环境用 style-loader,生产环境用 MiniCssExtractPlugin.loader
process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader', // 可选:添加浏览器前缀
],
},
// 处理 SCSS/Sass 文件
{
test: /\.(scss|sass)$/,
use: [
process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader', // 可选:添加浏览器前缀
'sass-loader',
],
},
],
},
plugins: [
// 提取 CSS 为独立文件(生产环境)
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
optimization: {
minimizer: [
// 压缩 CSS(生产环境)
new CssMinimizerPlugin(),
],
},
}
```
**3. 配置 PostCSS(可选)**
创建 `postcss.config.js` 文件以启用 `autoprefixer` :
```javascript
module.exports = {
plugins: [
require('autoprefixer')({
// 指定浏览器兼容范围
overrideBrowserslist: ['last 2 versions', '>1%', 'not dead'],
}),
],
}
```
通过配置 `css-loader` 、 `sass-loader` 和 `MiniCssExtractPlugin` ,Webpack 可以高效处理 CSS 和 Sass。关键点包括:
1. 加载器顺序:从右到左(如 `[sass-loader, css-loader, style-loader]`)。
2. 生产环境提取 CSS:使用 `MiniCssExtractPlugin`。
3. 浏览器兼容性:通过 `postcss-loader` 和 `autoprefixer` 自动处理。
:::
## Webpack 中的入口和出口是什么?
参考答案
::: details
在 Webpack 中,**入口(Entry)** 和 **出口(Output)** 是配置文件中的核心概念,决定了打包的起点和终点。它们共同定义了 Webpack 如何处理代码以及最终生成的资源。
1. **入口(Entry)**
入口是 Webpack 构建依赖图的起点,它告诉 Webpack:**“从哪个文件开始分析代码的依赖关系?”**
**作用**
- 指定应用程序的起始文件。
- 根据入口文件递归构建依赖关系树。
- 支持单入口(单页面应用)或多入口(多页面应用)。
**配置方式**
在 `webpack.config.js` 中通过 `entry` 属性配置:
```javascript
module.exports = {
entry: './src/index.js', // 单入口(默认配置)
// 多入口(多页面应用)
entry: {
home: './src/home.js',
about: './src/about.js',
},
}
```
**默认行为**
- 如果未手动配置 `entry`,Webpack 默认使用 `./src/index.js` 作为入口。
2. **出口(Output)**
出口是 Webpack 打包后的资源输出位置,它告诉 Webpack:**“打包后的文件放在哪里?如何命名?”**
**作用**
- 定义打包文件的输出目录和命名规则。
- 处理静态资源的路径(如 CSS、图片等)。
**配置方式**
在 `webpack.config.js` 中通过 `output` 属性配置:
```javascript
const path = require('path')
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'), // 输出目录(必须为绝对路径)
filename: 'bundle.js', // 单入口输出文件名
// 多入口时,使用占位符确保唯一性
filename: '[name].[contenthash].js',
clean: true, // 自动清理旧文件(Webpack 5+)
},
}
```
**常用占位符**
| 占位符 | 说明 |
|---------------------|-------------------------------|
| `[name]` | 入口名称(如多入口的 `home` ) |
| `[hash]` | 根据构建生成的唯一哈希值 |
| `[contenthash]` | 根据文件内容生成的哈希值 |
| `[chunkhash]` | 根据代码块生成的哈希值 |
:::
## Webpack 中的 Loaders 和 Plugins 有什么区别
参考答案
::: details
在 Webpack 中,**Loaders(加载器)** 和 **Plugins(插件)** 是构建流程中的两大核心概念,它们的作用和职责有明显区别。
**1. 核心区别总结**
| **特性** | **Loaders** | **Plugins** |
|----------------|---------------------------------|------------------------------------|
| **主要作用** | **转换文件内容**(如转译、预处理) | **扩展构建流程**(优化、资源管理、注入环境变量等) |
| **执行时机** | 在模块加载时(文件转换为模块时) | 在整个构建生命周期(从初始化到输出)的各个阶段 |
| **配置方式** | 通过 `module.rules` 数组配置 | 通过 `plugins` 数组配置(需要 `new` 实例化) |
| **典型场景** | 处理 JS/CSS/图片等文件转译 | 生成 HTML、压缩代码、提取 CSS 等全局操作 |
| **依赖关系** | 针对特定文件类型(如 `.scss` ) | 不依赖文件类型,可干预整个构建流程 |
**2. Loaders 的作用与使用**
**核心功能**
- 将非 JavaScript 文件(如 CSS、图片、字体等)**转换为 Webpack 能处理的模块**。
- 对代码进行预处理(如 Babel 转译、Sass 编译)。
**配置示例**
```javascript
// webpack.config.js
module.exports = {
module: {
rules: [
// 处理 CSS 文件
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
// 处理 TypeScript 文件
{
test: /\.tsx?$/,
use: 'ts-loader',
},
// 处理图片文件
{
test: /\.(png|jpg|gif)$/,
type: 'asset/resource', // Webpack 5 内置方式(替代 file-loader)
},
],
},
}
```
**常见 Loaders**
- `babel-loader`: 将 ES6+ 代码转译为 ES5。
- `css-loader`: 解析 CSS 中的 `@import` 和 `url()`。
- `sass-loader`: 将 Sass/SCSS 编译为 CSS。
- `file-loader`: 处理文件(如图片)的导入路径。
**3. Plugins 的作用与使用**
**核心功能**
- 扩展 Webpack 的能力,干预构建流程的**任意阶段**。
- 执行更复杂的任务,如代码压缩、资源优化、环境变量注入等。
**配置示例**
```javascript
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
// 自动生成 HTML 文件,并注入打包后的资源
new HtmlWebpackPlugin({
template: './src/index.html',
}),
// 提取 CSS 为独立文件
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
],
}
```
**常见 Plugins**
- `HtmlWebpackPlugin`: 生成 HTML 文件并自动引入打包后的资源。
- `MiniCssExtractPlugin`: 将 CSS 提取为独立文件(替代 `style-loader`)。
- `CleanWebpackPlugin`: 清理构建目录(Webpack 5 中可用 `output.clean: true` 替代)。
- `DefinePlugin`: 注入全局常量(如 `process.env.NODE_ENV`)。
**4. 执行流程对比**
**Loaders 的执行流程**
```plaintext
文件资源 (如 .scss) → 匹配 Loader 规则 → 按顺序应用 Loaders → 转换为 JS 模块
```
- **顺序关键**:Loaders 从右到左(或从下到上)执行。
例如: `use: ['style-loader', 'css-loader', 'sass-loader']` 的执行顺序为:
`sass-loader` → `css-loader` → `style-loader` 。
**Plugins 的执行流程**
```plaintext
初始化 → 读取配置 → 创建 Compiler → 挂载 Plugins → 编译模块 → 优化 → 输出
```
- **生命周期钩子**:Plugins 通过监听 Webpack 的[生命周期钩子](https://webpack.js.org/api/compiler-hooks/)(如 `emit`、`done`)干预构建流程。
**5. 协作示例**
一个同时使用 Loaders 和 Plugins 的典型场景:
```javascript
// webpack.config.js
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
module: {
rules: [
{
test: /\.scss$/,
// Loaders 处理链:sass → css → MiniCssExtractPlugin
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
plugins: [
// Plugin:提取 CSS 为文件
new MiniCssExtractPlugin(),
// Plugin:生成 HTML
new HtmlWebpackPlugin(),
],
}
```
:::
## Webpack中, 如何实现按需加载?
参考答案
::: details
在 Webpack 中实现按需加载(代码分割/懒加载)的核心思路是 **将代码拆分为独立 chunk,在需要时动态加载**。
**一、基础方法:动态导入(Dynamic Import)**
通过 `import()` 语法实现按需加载,Webpack 会自动将其拆分为独立 chunk。
**1. 代码中使用动态导入**
```javascript
// 示例:点击按钮后加载模块
document.getElementById('btn').addEventListener('click', async () => {
const module = await import('./module.js')
module.doSomething()
})
```
**2. 配置 Webpack**
确保 `webpack.config.js` 的 `output` 配置中包含 `chunkFilename` :
```javascript
module.exports = {
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].[contenthash].chunk.js', // 动态导入的 chunk 命名规则
path: path.resolve(__dirname, 'dist'),
publicPath: '/', // 确保 chunk 的公共路径正确
},
}
```
**二、框架集成:React/Vue 路由级按需加载**
结合前端框架的路由系统实现组件级懒加载。
**React 示例**
```javascript
import React, { Suspense, lazy } from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
const Home = lazy(() => import('./routes/Home'))
const About = lazy(() => import('./routes/About'))
function App() {
return (
Loading...
}>
{' '}
{' '}
{' '}
{' '}
{' '}
)
}
```
**Vue 示例**
```javascript
const routes = [
{
path: '/',
component: () => import('./views/Home.vue'),
},
{
path: '/about',
component: () => import('./views/About.vue'),
},
]
```
**三、优化配置:代码分割策略**
通过 `SplitChunksPlugin` 优化公共代码提取。
**Webpack 配置**
```javascript
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块进行分割(包括异步和非异步)
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors', // 提取 node_modules 代码为 vendors 块
priority: 10, // 优先级
reuseExistingChunk: true,
},
common: {
minChunks: 2, // 被至少两个 chunk 引用的代码
name: 'common',
priority: 5,
reuseExistingChunk: true,
},
},
},
},
}
```
**四、Babel 配置(如需支持旧浏览器)**
安装 Babel 插件解析动态导入语法:
```bash
npm install @babel/plugin-syntax-dynamic-import --save-dev
```
在 `.babelrc` 或 `babel.config.json` 中添加插件:
```json
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}
```
**五、预加载与预取(可选优化)**
通过注释提示浏览器提前加载资源(需结合框架使用)。
**React 示例**
```javascript
const About = lazy(
() =>
import(
/* webpackPrefetch: true */ // 预取(空闲时加载)
/* webpackPreload: true */ // 预加载(与父 chunk 并行加载)
'./routes/About'
)
)
```
**六、验证效果**
1. **构建产物分析**:
- 运行 `npx webpack --profile --json=stats.json` 生成构建报告。
- 使用 [Webpack Bundle Analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) 可视化分析 chunk 分布。
2. **网络请求验证**:
- 打开浏览器开发者工具,观察触发动态导入时是否加载新 chunk。
:::
## 什么是 Tree Shaking?如何在 Webpack 中启用它?
参考答案
::: details
**Tree Shaking(摇树优化)** 是一种在打包过程中 **移除 JavaScript 项目中未使用代码(Dead Code)** 的优化技术。它的名字形象地比喻为“摇动树以掉落枯叶”,即通过静态代码分析,识别并删除未被引用的模块或函数,从而减小最终打包体积。
**Tree Shaking 的工作原理**
1. **基于 ES Module(ESM)的静态结构**
ESM 的 `import/export` 是静态声明(代码执行前可确定依赖关系),而 CommonJS 的 `require` 是动态的。只有 ESM 能被 Tree Shaking 分析。
2. **标记未使用的导出**
打包工具(如 Webpack)通过分析代码,标记未被任何模块导入的导出。
3. **压缩阶段删除**
结合代码压缩工具(如 Terser)删除这些标记的未使用代码。
**在 Webpack 中启用 Tree Shaking 的步骤**
**1. 使用 ES Module 语法**
确保项目代码使用 `import/export` ,而非 CommonJS 的 `require` 。
```javascript
// ✅ 正确:ESM 导出
export function add(a, b) {
return a + b
}
export function subtract(a, b) {
return a - b
}
// ✅ 正确:ESM 导入
import { add } from './math'
// ❌ 错误:CommonJS 导出
module.exports = {
add,
subtract,
}
```
**2. 配置 Webpack 的 `mode` 为 `production` **
在 `webpack.config.js` 中设置 `mode: 'production'` ,这会自动启用 Tree Shaking 和代码压缩。
```javascript
module.exports = {
mode: 'production', // 启用生产模式优化
// ...
}
```
**3. 禁用模块转换(Babel 配置)**
确保 Babel 不会将 ESM 转换为 CommonJS。在 `.babelrc` 或 `babel.config.json` 中设置:
```json
{
"presets": [
["@babel/preset-env", { "modules": false }] // 保留 ESM 语法
]
}
```
**4. 标记副作用文件(可选)**
在 `package.json` 中声明哪些文件有副作用(如全局 CSS、Polyfill),避免被错误删除:
```json
{
"sideEffects": [
"**/*.css", // CSS 文件有副作用(影响样式)
"src/polyfill.js" // Polyfill 有副作用
]
}
```
若项目无副作用文件,直接设为 `false` :
```json
{
"sideEffects": false
}
```
**5. 显式配置 `optimization.usedExports` **
在 `webpack.config.js` 中启用 `usedExports` ,让 Webpack 标记未使用的导出:
```javascript
module.exports = {
optimization: {
usedExports: true, // 标记未使用的导出
minimize: true, // 启用压缩(删除未使用代码)
},
}
```
**验证 Tree Shaking 是否生效**
**方法 1:检查打包后的代码**
若未使用的函数(如 `subtract` )被删除,说明 Tree Shaking 生效:
```javascript
// 打包前 math.js
export function add(a, b) {
return a + b
}
export function subtract(a, b) {
return a - b
}
// 打包后(仅保留 add)
function add(a, b) {
return a + b
}
```
**方法 2:使用分析工具**
通过 [Webpack Bundle Analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) 可视化分析打包结果:
```bash
npm install --save-dev webpack-bundle-analyzer
```
配置 `webpack.config.js` :
```javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [new BundleAnalyzerPlugin()],
}
```
运行构建后,浏览器将自动打开分析页面,检查未使用的模块是否被移除。
| **步骤** | **关键配置** | **作用** |
| -------------------- | ------------------------------------ | ---------------------------- |
| 使用 ESM 语法 | `import/export` | 提供静态分析基础 |
| 设置生产模式 | `mode: 'production'` | 自动启用 Tree Shaking 和压缩 |
| 配置 Babel | `"modules": false` | 保留 ESM 结构 |
| 标记副作用文件 | `package.json` 的 `sideEffects` 字段 | 防止误删有副作用的文件 |
| 显式启用 usedExports | `optimization.usedExports: true` | 标记未使用的导出 |
:::
================================================
FILE: docs/second-exam/mini-program.md
================================================
# 小程序
小程序是很容易入门和掌握的技术栈,如果你技术栈偏窄,可以考虑补充一下小程序的知识。
::: tip
如有疑问,可免费 [加群](/docs/services/group.md) 讨论咨询,也可参与 [1v1 面试咨询服务](/docs/services/1v1.md), 专业、系统、高效、全流程 准备前端面试
:::
## 小程序双线程架构
参考答案
::: details
**1. 架构组成**
**(1)逻辑层(Service)**
- **运行环境**:独立的 JavaScript 线程(如 JavaScriptCore 或 V8 引擎)。
- **职责**:
- 处理业务逻辑(数据请求、事件响应、状态管理)。
- 调用小程序 API(如网络请求、本地存储)。
- 通过 `setData` 向渲染层传递数据。
- **特点**:
- **无法直接操作 DOM**:与渲染层隔离,避免恶意脚本攻击。
- **单例运行**:全局状态统一管理(如 App 和 Page 对象)。
**(2)渲染层(View)**
- **运行环境**:WebView 线程(每个页面独立实例)。
- **职责**:
- 解析 WXML/WXSS,渲染页面结构。
- 处理用户交互事件(点击、滑动),触发逻辑层响应。
- **特点**:
- **数据驱动更新**:根据逻辑层传递的数据动态渲染。
- **轻量化**:不执行复杂逻辑,保障渲染流畅性。
**(3)系统层(Native)**
- **作用**:作为逻辑层与渲染层的通信桥梁,提供原生能力支持。
- **核心功能**:
- **JSBridge**:序列化传递数据(JSON 格式)。
- **安全管控**:拦截非法操作(如直接访问 DOM)。
- **原生 API**:调用摄像头、地理位置等硬件功能。

:::
## 直接修改 this.data 为何不会触发视图更新?
参考答案
::: details
小程序中直接修改 `this.data` 不会触发视图更新的原因如下:
**1. 数据更新机制的设计**
小程序采用 **显式更新** 策略,只有通过 `this.setData()` 方法修改数据时,才会触发以下流程:
- **数据变更通知**:将修改的数据标记为“脏数据”(需更新)。
- **通信到渲染层**:通过 JSBridge 将数据序列化后传递到 WebView 线程。
- **视图差异化更新**:渲染层对比新旧数据差异,仅更新变化的 DOM 节点。
直接修改 `this.data` 仅改变逻辑层的数据,但 **未触发上述流程**,因此渲染层无法感知数据变化。
**2. 双线程架构的限制**
小程序的逻辑层(Service)与渲染层(View)运行在独立线程中:
- **逻辑层**:通过 JavaScriptCore 或 V8 引擎运行。
- **渲染层**:在 WebView 中解析 WXML/WXSS。
两者通过 **异步通信**(JSBridge)传递数据。
直接修改 `this.data` 不会触发系统层的数据传递,导致渲染层无法同步更新。
**3. 性能优化考量**
若每次数据修改都自动触发更新:
- **频繁通信开销**:高频数据变更(如循环中修改数据)会导致线程间通信阻塞。
- **不必要的渲染**:中间状态的数据变更可能引发多次无效渲染。
通过 `this.setData()` 的 **批量合并更新机制**,可优化性能:
```javascript
// 合并多次更新,仅触发一次通信和渲染
this.setData({ a: 1 })
this.setData({ b: 2 })
// 等效于
this.setData({ a: 1, b: 2 })
```
**4. 数据一致性与安全性**
- **脏数据风险**:直接修改 `this.data` 可能导致逻辑层与渲染层数据不一致。
- **状态管理规范**:强制使用 `this.setData()` 确保数据变更可追踪,符合单向数据流原则。
:::
## setData 底层做了哪些性能优化处理?
参考答案
::: details
**1. 核心优化机制**
**(1) 数据通信优化**
- **差异化更新(Diff 算法)**
对比新旧数据树,仅序列化并传输变化的部分。例如:
```javascript
// 旧数据:{ a: 1, list: [{ id: 1 }, { id: 2 }] }
this.setData({ 'list[1].id': 3 })
// 实际传输:{ 'list[1].id': 3 }(而非整个 list 数组)
```
**优化效果**:减少 60%~80% 的数据传输量。
- **序列化过滤**
自动过滤 `undefined`、`Function`、`Symbol` 等不可序列化数据,避免无效通信。
**(2) 更新调度优化**
- **批量合并(Batching)**
同一事件循环内的多次 `setData` 调用合并为一次更新:
```javascript
this.setData({ a: 1 })
this.setData({ b: 2 })
// 合并为 { a: 1, b: 2 },触发单次通信
```
**优化场景**:高频操作(如动画帧更新、滚动事件)。
- **异步队列与优先级调度**
用户交互触发的更新优先级高于数据请求,优先保障交互流畅性。
**(3) 渲染层优化**
- **虚拟 DOM 对比(Virtual DOM Diff)**
生成最小化的 DOM 操作指令,避免全量渲染:
- **WXS 脚本加速**
在渲染层直接处理轻量逻辑(如数据格式化),减少逻辑层通信:
```wxml
function formatPrice(price) { return '¥' + price; }
module.exports = { formatPrice };
{{utils.formatPrice(100)}}
```
**(4) 通信协议优化**
- **二进制传输(如 Protocol Buffers)**
替代 JSON 序列化,体积减少 30%~50%,解析速度提升 2~5 倍。
- **通道复用与流量控制**
复用 JSBridge 通道,避免频繁建立连接,满负荷时自动排队。
:::
## this.setData({ list: largeDataArray }) 有问题吗?
参考答案
::: details
在小程序开发中,使用 `this.setData({ list: largeDataArray })` 传递一个大型数据数组(尤其是包含成千上万条数据时)**确实存在明显的性能问题**。
**1. 核心问题分析**
**(1) 数据传输瓶颈**
- **JSBridge 序列化开销**:数据需从逻辑层(Service)序列化为 JSON 字符串,通过 JSBridge 传递到渲染层(View),数据量越大,序列化和传输时间越长。
- **典型耗时**:传输 10,000 条数据(每条 100B)约耗时 **100~300ms**(中低端手机更久)。
**(2) 渲染性能问题**
- **DOM 节点爆炸**:渲染层需解析数据并生成大量 DOM 节点,导致:
- **内存占用高**:每个 DOM 节点消耗 0.1~1KB 内存,10,000 条数据可能占用 **1~10MB**。
- **渲染卡顿**:首次渲染或滚动时出现明显卡顿(帧率低于 30fps)。
**(3) 频繁 GC(垃圾回收)**
- **内存抖动**:频繁创建和销毁大型临时对象,触发 JavaScript 引擎垃圾回收,导致间歇性卡顿。
**2. 优化方案**
**(1) 分页加载(懒加载)**
- **实现方式**:
```javascript
Page({
data: { list: [], page: 0 },
onReachBottom() {
// 滚动到底部加载下一页
this.loadNextPage()
},
loadNextPage() {
const nextPageData = fetchData(this.data.page + 1)
this.setData({
list: this.data.list.concat(nextPageData),
page: this.data.page + 1,
})
},
})
```
- **优点**:减少单次传输数据量,避免内存峰值。
**(2) 虚拟列表(按需渲染)**
- **原理**:仅渲染可视区域内的元素。
- **实现库**:使用 `wx-component` 或第三方库(如 `recycle-view`)。
```xml
{{item.text}}
```
- **优点**:渲染 100 万条数据时,内存占用仅 **1~2MB**。
**(3) 纯数据字段(Pure Data)**
- **适用场景**:需要存储数据但无需渲染的字段。
```javascript
Component({
options: { pureDataPattern: /^_/ },
data: {
_fullList: largeDataArray, // 不触发渲染
visibleList: largeDataArray.slice(0, 20),
},
})
```
**(4) 数据压缩**
- **精简字段**:
```javascript
// 原始数据
const rawData = [{ id: 1, title: '...', desc: '...' /* 10+ 字段 */ }]
// 优化后
const optimizedData = rawData.map(({ id, title }) => ({ id, title }))
this.setData({ list: optimizedData })
```
- **压缩率**:减少 50%~80% 数据体积。
**(5) WebWorker 计算**
```javascript
// 在 Worker 中处理数据
const worker = wx.createWorker('workers/data-handler.js')
worker.postMessage({ action: 'filter', data: largeDataArray })
worker.onMessage((res) => {
this.setData({ list: res.filteredData })
})
```
**(6) 原生组件替代**
- 使用 `