封装的业务组件
滑动验证码校验
身份证正反面拍照框
统一报错组件
统一弹窗组件
script 标签中 defer 和 async 的区别?
script:会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。async script:解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。
下图清晰地展示了三种 script 的过程:
总结
-
使用
defer:- 当脚本之间有依赖关系时。
- 需要脚本在DOM完全加载后执行时。
- 提高页面加载速度,同时保证脚本按顺序执行。
-
使用
async:-
当脚本之间没有依赖关系时。
-
脚本不需要操作DOM元素时。
-
加载第三方脚本时,避免阻塞页面渲染。
const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // 其他配置项... plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', // 你的HTML模板文件路径 scriptLoading: 'defer' // 设置script标签的defer属性 }) ] };
-
webpack文件压缩
使用 compression-webpack-plugin 进行 Gzip 压缩
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
// 其他配置项...
plugins: [
new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /.js$|.css$|.html$/,
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
})
]
};
使用 compression-webpack-plugin 进行 Brotli 压缩
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
// 其他配置项...
plugins: [
new CompressionPlugin({
filename: '[path][base].br', // 压缩后的文件名
algorithm: 'brotliCompress', // 使用Brotli压缩算法
test: /.js$|.css$|.html$/, // 匹配需要压缩的文件
threshold: 10240, // 只有文件大于10KB才会被压缩
minRatio: 0.8, // 压缩后的文件大小与原始文件大小的比率,小于该值才会被压缩
deleteOriginalAssets: false // 是否删除原始文件
})
]
};
nginx配置
server {
listen 80;
server_name example.com;
location / {
root /path/to/dist;
try_files $uri $uri/ /index.html;
# 启用Gzip压缩
gzip on;
gzip_types text/plain text/css application/javascript application/json application/xml text/xml application/xml+rss text/javascript;
gzip_vary on;
# 提供Gzip文件
location ~* .(js|css|html)$ {
add_header Content-Encoding gzip;
add_header Content-Type $mime_type;
try_files $uri.gz $uri =404;
}
# 启用Brotli压缩
brotli on;
brotli_types text/plain text/css application/javascript application/json application/xml text/xml application/xml+rss text/javascript;
brotli_static on;
# 提供Brotli文件
location ~* .(js|css|html)$ {
add_header Content-Encoding br;
add_header Content-Type $mime_type;
try_files $uri.br $uri =404;
}
}
}
对比总结
| 特性 | Gzip | Brotli |
|---|---|---|
| 压缩效率 | 相对较低 | 相对较高 |
| 性能 | 压缩和解压缩速度快 | 压缩和解压缩速度较慢 |
| CPU 负担 | 较小 | 较大 |
| 文件扩展名 | .gz | .br |
| 支持情况 | 几乎所有现代浏览器 | 大多数现代浏览器 |
| 适用场景 | 资源有限、广泛支持 | 高压缩率、资源充足 |
从用户在浏览器地址栏输入 URL 到请求返回并显示页面
1. 输入 URL
用户在浏览器地址栏输入 URL 并按下回车键。
2. DNS 解析
浏览器需要将输入的域名(如 www.example.com)转换为对应的 IP 地址。
-
检查浏览器缓存:
- 浏览器首先检查其缓存中是否已经存在该域名的 IP 地址。
- 如果存在且未过期,则直接使用缓存的 IP 地址。
-
检查操作系统缓存:
- 如果浏览器缓存中没有,浏览器会检查操作系统的 DNS 缓存。
-
检查路由器缓存:
- 如果操作系统缓存中也没有,请求会发送到路由器,路由器会检查其缓存。
-
联系 DNS 服务器:
- 如果所有缓存中都没有,浏览器会向本地 DNS 服务器(如 ISP 提供的 DNS 服务器)发送 DNS 查询请求。
-
递归查询:
- 本地 DNS 服务器会递归地查询根 DNS 服务器、顶级域 DNS 服务器(如
.com服务器)和权威 DNS 服务器,直到找到目标域名的 IP 地址。
- 本地 DNS 服务器会递归地查询根 DNS 服务器、顶级域 DNS 服务器(如
-
返回 IP 地址:
- DNS 服务器将 IP 地址返回给浏览器,并缓存该结果以备后续使用。
3. 建立 TCP 连接
浏览器使用 TCP 协议与目标服务器建立连接。
-
三次握手:
- 第一次握手:浏览器向服务器发送一个 SYN(同步)数据包。
- 第二次握手:服务器收到 SYN 数据包后,发送一个 SYN-ACK(同步确认)数据包给浏览器。
- 第三次握手:浏览器收到 SYN-ACK 数据包后,发送一个 ACK(确认)数据包给服务器。
- 三次握手完成后,TCP 连接建立成功。
-
保持连接:
- 建立连接后,浏览器和服务器可以开始传输数据。
4. 发送 HTTP 请求
浏览器通过建立的 TCP 连接向服务器发送 HTTP 请求。
-
构建请求:
- 浏览器构建一个 HTTP 请求,通常包括请求行(如
GET /index.html HTTP/1.1)、请求头(如Host: www.example.com)和请求体(对于 POST 请求)。
- 浏览器构建一个 HTTP 请求,通常包括请求行(如
-
发送请求:
- 浏览器将构建好的 HTTP 请求通过 TCP 连接发送给服务器。
5. 服务器处理请求
服务器接收到请求后,处理并生成响应。
-
解析请求:
- 服务器解析 HTTP 请求,确定请求的资源路径和其他信息。
-
查找资源:
- 服务器根据请求路径查找相应的文件或执行相应的处理逻辑(如动态生成内容)。
-
生成响应:
- 服务器生成 HTTP 响应,通常包括响应行(如
HTTP/1.1 200 OK)、响应头(如Content-Type: text/html)和响应体(如 HTML 内容)。
- 服务器生成 HTTP 响应,通常包括响应行(如
-
发送响应:
- 服务器将生成的 HTTP 响应通过 TCP 连接发送给浏览器。
6. 浏览器接收响应
浏览器接收到服务器的响应后,开始解析和渲染页面。
-
解析响应:
- 浏览器解析 HTTP 响应,提取响应头和响应体。
-
构建 DOM 树:
- 浏览器使用 HTML 解析器将响应体中的 HTML 代码解析成 DOM 树。
-
构建 CSSOM 树:
- 浏览器使用 CSS 解析器将 CSS 代码解析成 CSSOM 树。
-
生成渲染树:
- 浏览器将 DOM 树和 CSSOM 树合并成渲染树,确定每个元素的样式和布局。
-
布局(Reflow) :
- 浏览器根据渲染树计算每个元素的精确位置和大小。
-
绘制(Repaint) :
- 浏览器将渲染树中的每个节点绘制到屏幕上,形成最终的页面。
7. 加载和渲染资源
页面加载过程中,浏览器会继续请求和加载其他资源(如 CSS、JavaScript、图像等)。
-
并行请求:
- 浏览器会并行请求页面中引用的所有资源,以加快加载速度。
-
缓存:
- 浏览器会缓存静态资源(如 CSS、JavaScript、图像),以减少后续请求。
-
执行 JavaScript:
- 浏览器会执行页面中的 JavaScript 代码,动态修改页面内容或添加交互功能。
8. 页面显示完成
所有资源加载和渲染完成后,页面最终显示在用户面前。
总结
从用户在浏览器地址栏输入 URL 到页面显示完成,整个过程涉及以下几个关键步骤:
- DNS 解析:将域名转换为 IP 地址。
- TCP 连接:建立与服务器的 TCP 连接。
- HTTP 请求:浏览器向服务器发送 HTTP 请求。
- 服务器处理:服务器处理请求并生成响应。
- HTTP 响应:服务器将响应发送给浏览器。
- 页面解析和渲染:浏览器解析响应并渲染页面。
- 资源加载:浏览器加载和渲染其他资源。
- 页面显示:页面最终显示在用户面前。
通过这些步骤,浏览器能够高效地从服务器获取资源并呈现给用户。
前端性能优化是一个多方面的任务,涉及代码分割、资源压缩、缓存策略等多个方面。Webpack 是一个强大的构建工具,可以帮助你实现这些优化。以下是一些常见的 Webpack 配置优化策略:
1. 代码分割(Code Splitting)
代码分割可以将代码拆分成多个小块,按需加载,减少初始加载时间。
-
使用
SplitChunksPlugin-
Webpack 内置的
SplitChunksPlugin可以帮助你自动分割代码。 -
示例:
javascriptmodule.exports = { optimization: { splitChunks: { chunks: 'all', // 对所有模块都进行代码分割 cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, name: 'vendors', chunks: 'all', }, }, }, }, };
-
2. 压缩资源
压缩 JavaScript 和 CSS 文件可以显著减少文件大小。
-
使用
TerserPlugin压缩 JavaScript-
Webpack 默认使用
TerserPlugin压缩 JavaScript。 -
示例:
javascriptconst TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [new TerserPlugin()], }, };
-
-
使用
CssMinimizerPlugin压缩 CSS-
使用
css-minimizer-webpack-plugin压缩 CSS。 -
示例:
javascriptconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin(), new CssMinimizerPlugin(), ], }, };
-
3. 使用 Tree Shaking
Tree Shaking 可以移除未使用的代码,减少最终打包文件的大小。
-
确保使用 ES6 模块语法
-
Tree Shaking 依赖于 ES6 模块语法(
import和export)。 -
示例:
javascriptimport { someFunction } from './module'; someFunction();
-
4. 使用缓存
通过配置缓存,可以避免重复加载未更改的资源。
-
设置文件名哈希
-
使用文件内容的哈希值作为文件名的一部分,确保文件内容变化时文件名也变化。
-
示例:
javascriptmodule.exports = { output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', }, };
-
5. 使用 CDN
将常用的库(如 React、Vue 等)通过 CDN 加载,减少打包文件的大小。
-
配置 externals
-
使用
externals配置项将某些模块排除在打包文件之外。 -
示例:
javascriptmodule.exports = { externals: { react: 'React', 'react-dom': 'ReactDOM', }, };
-
6. 图片优化
优化图片可以显著减少文件大小,提高加载速度。
-
使用
image-webpack-loader-
使用
image-webpack-loader压缩图片。 -
示例:
javascriptmodule.exports = { module: { rules: [ { test: /.(png|jpe?g|gif|svg)$/i, use: [ { loader: 'file-loader', options: { name: '[path][name].[ext]', }, }, { loader: 'image-webpack-loader', options: { mozjpeg: { progressive: true, quality: 65, }, optipng: { enabled: false, }, pngquant: { quality: [0.65, 0.90], speed: 4, }, gifsicle: { interlaced: false, }, webp: { quality: 75, }, }, }, ], }, ], }, };
-
7. 使用 MiniCssExtractPlugin
将 CSS 提取到单独的文件中,减少初始加载时间。
-
配置
MiniCssExtractPlugin-
使用
mini-css-extract-plugin提取 CSS。 -
示例:
javascriptconst MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { module: { rules: [ { test: /.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', chunkFilename: '[id].[contenthash].css', }), ], };
-
8. 使用 BundleAnalyzerPlugin
分析打包后的文件,找出体积较大的模块。
-
配置
BundleAnalyzerPlugin-
使用
webpack-bundle-analyzer分析打包文件。 -
示例:
javascriptconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin(), ], };
-
9. 使用 HMR(Hot Module Replacement)
在开发环境中使用 HMR 可以提高开发效率,减少重新加载时间。
-
配置 HMR
-
在开发配置中启用 HMR。
-
示例:
javascriptconst webpack = require('webpack'); module.exports = { devServer: { hot: true, }, plugins: [ new webpack.HotModuleReplacementPlugin(), ], };
-
10. 使用 PurgeCSSPlugin
移除未使用的 CSS,减少 CSS 文件大小。
-
配置
PurgeCSSPlugin-
使用
purgecss-webpack-plugin移除未使用的 CSS。 -
示例:
javascriptconst PurgeCSSPlugin = require('purgecss-webpack-plugin'); const glob = require('glob'); const path = require('path'); const PATHS = { src: path.join(__dirname, 'src'), }; module.exports = { plugins: [ new PurgeCSSPlugin({ paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }), }), ], };
-
Vue 2 和 Vue 3 在多个方面有显著区别,包括响应式系统、组件结构、性能优化、API 设计等方面。以下是 Vue 2 与 Vue 3 的核心差异总结:
🧩 一、响应式系统的实现
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式机制 | Object.defineProperty | Proxy + ref / reactive |
| 数组响应性 | 需要特殊处理(如 this.$set) | 完全自动响应 |
| 对象新增属性响应性 | 不响应,需使用 $set | 自动响应 |
| 性能 | 相对较低(每个属性都需要定义 getter/setter) | 更高效(基于 Proxy) |
🧱 二、API 风格
| 特性 | Vue 2 (Options API) | Vue 3 (Composition API) |
|---|---|---|
| 组件逻辑组织方式 | 按选项分块(data, methods, computed 等) | 使用 setup() 或 <script setup> 组织逻辑 |
| 可复用逻辑 | mixins(易命名冲突) | custom hooks(函数封装,高内聚低耦合) |
| 默认语法 | Options API | 支持 Options API 和 Composition API |
| 推荐新项目使用 | ❌ 已逐步淘汰 | ✅ 推荐使用 Composition API |
⚙️ 三、生命周期钩子
| Vue 2 钩子名 | Vue 3 钩子名(setup() 中) |
|---|---|
beforeCreate | ❌ 移除(用 setup() 替代) |
created | ❌ 移除(用 setup() 替代) |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
activated | onActivated |
deactivated | onDeactivated |
errorCaptured | onErrorCaptured |
Vue 3 还支持在
<script setup>中直接导入并调用这些钩子。
📦 四、组件通信和 props
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| props 类型校验 | 使用 props: {} | 同样支持,但可配合 TypeScript 更强类型检查 |
| emit 事件 | this.$emit('event') | defineEmits()(在 <script setup> 中) |
| 插槽 | 使用 this.$slots | 支持更灵活的插槽 API |
v-model | 单向绑定,默认为 .sync | 支持多 v-model,可自定义修饰符 |
📁 五、模块化与工具链
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 构建工具 | Webpack / Babel | Vite 原生支持(更快的开发服务器) |
| TypeScript 支持 | 有限 | 完全支持(TypeScript First) |
| 包体积 | 较大 | 更小(Tree-shaking 更彻底) |
| 模板编译器 | runtime-only 和 runtime-with-compiler | 支持编译时优化(block tree) |
| 虚拟 DOM 实现 | 传统 Diff 算法 | Block Tree 架构,提升 diff 性能 |
🧠 六、全局状态管理
| Vue 2 | Vue 3 |
|---|---|
| Vuex 3 | Vuex 4(兼容 Composition API) |
可使用 provide/inject | ✅ 同样支持 |
| 推荐使用 | Pinia(Vue 3 推荐状态管理库) |
🧪 七、模板语法增强
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| Fragment(多根节点) | ❌ 不支持 | ✅ 支持 |
| Teleport(传送组件) | ❌ 不支持 | ✅ 支持(类似 React Portals) |
| Suspense(异步依赖加载) | ❌ 不支持 | ✅ 支持 |
| 自定义渲染器 | ✅ 支持 | ✅ 更加模块化 |
🧩 八、组合式 API(Composition API)
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 是否支持 Composition API | ❌ 不支持 | ✅ 完全支持 |
是否支持 <script setup> | ❌ 不支持 | ✅ 完全支持(推荐写法) |
是否支持 ref / reactive | ❌ 不支持 | ✅ 完全支持 |
是否支持 watchEffect / watch | ❌ 不支持 | ✅ 完全支持 |
总结对比
| 对比项 | watchEffect | watch |
|---|---|---|
| 是否自动追踪依赖 | ✅ 是 | ❌ 否 |
| 是否能获取新/旧值 | ❌ 否 | ✅ 是 |
| 是否需要手动指定监听对象 | ❌ 否 | ✅ 是 |
| 是否适合监听对象/数组 | ✅ 可以(但不推荐深层监听) | ✅ 推荐配合 { deep: true } |
| 是否适合执行清理逻辑 | ✅ 支持 onInvalidate | ✅ 支持 |
| 使用场景 | 快速监听多个依赖 | 精确监听某个值或多个值 |
🔥 九、性能优化
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 编译优化 | 无 | ✅ Block Tree 编译优化 |
| 渲染速度 | 一般 | ✅ 更快(diff 算法优化) |
| 包体积 | 较大 | ✅ 更小(Tree-shaking 更彻底) |
| SSR 支持 | ✅ 支持 | ✅ 更高效(Hydration 支持) |
📦 十、模块导出方式
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 默认构建方式 | IIFE / UMD | ESM(默认使用 ES Module) |
| 是否支持按需导入 | ❌ 不支持 | ✅ 支持按需引入(如 ref, computed) |
💡 十一、其他重要改进
| 功能 | Vue 2 | Vue 3 |
|---|---|---|
| 开发体验 | 使用 Vue Devtools 支持 | ✅ 支持更好 |
| 生态更新 | 社区活跃,但已进入维护模式 | ✅ 主流框架,持续更新 |
| TypeScript 支持 | 有限 | ✅ 完全支持 |
| JSX 支持 | ✅ 支持 | ✅ 支持 |
| 多版本共存 | ✅ 支持 | ✅ 支持 |
✅ 十二、示例对比:同一个组件的不同写法
Vue 2(Options API)
jsexport default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
Vue 3(Composition API + <script setup>)
vue<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
📌 十三、总结对比表
| 对比项 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy + ref/reactive |
| API 风格 | Options API | 支持 Options + Composition API |
| 生命周期钩子 | beforeCreate, created, mounted 等 | onBeforeMount, onMounted 等 |
| 支持 TypeScript | ✅ 支持 | ✅ 更友好 |
支持 <script setup> | ❌ 不支持 | ✅ 强烈推荐 |
| 支持多 v-model | ❌ 不支持 | ✅ 支持 |
| 支持 Fragment、Teleport、Suspense | ❌ 不支持 | ✅ 支持 |
| 构建工具 | Webpack / Babel | ✅ 支持 Vite |
| 包大小 | 较大 | ✅ 更小 |
| 社区生态 | 成熟 | ✅ 新一代生态(Pinia、Vite、Vue Router 4) |
✅ 最终建议
| 场景 | 推荐版本 |
|---|---|
| 新项目 | ✅ Vue 3(推荐搭配 <script setup>) |
| 老项目维护 | ✅ Vue 2(仍可继续使用) |
| 需要高性能 | ✅ Vue 3 |
| 需要 TypeScript 支持 | ✅ Vue 3 |
| 需要使用 Vite | ✅ Vue 3 |
| 需要使用 Pinia | ✅ Vue 3 |
| 需要使用 Composition API | ✅ Vue 3 |
闭包(Closure)定义
闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。 闭包 = 函数 + 函数能够访问的自由变量。
闭包是指能够访问并记住其词法作用域(lexical scope),即使该函数在其作用域外执行。
闭包的本质是:
一个函数在定义时所处的环境(作用域)与它自身结合,形成闭包。
自由变量(Free Variable)
自由变量是指:
- 函数中使用的变量;
- 它既不是函数的参数,也不是函数内部定义的局部变量;
- 它来自函数外部的作用域。
例如:
javascriptfunction outer() {
const x = 10;
return function inner(y) {
return x + y; // `x` 是 inner 函数的自由变量
};
}
const closureFunc = outer();
console.log(closureFunc(5)); // 输出 15
在这个例子中:
inner是一个闭包;- 它捕获了外部函数
outer中定义的变量x,即自由变量; - 即使
outer已经执行完毕,x依然被保留在内存中,因为inner引用了它。
闭包的组成
闭包 = 函数 + 该函数能够访问的自由变量环境
上面的例子中:
- 函数:
inner - 自由变量环境:包含
x: 10
闭包的常见用途
| 使用场景 | 示例说明 |
|---|---|
| 数据封装 | 利用闭包实现私有变量和方法 |
| 柯里化 / 偏函数应用 | 创建带有部分参数绑定的新函数 |
| 回调函数 | 在异步编程中保留上下文状态 |
| 模块模式 | 实现模块化的结构,避免全局污染 |
注意事项
- 内存泄漏风险:如果闭包引用了外部变量,这些变量不会被垃圾回收器回收,可能导致内存占用过高。
- 性能考虑:频繁创建闭包可能会带来额外的性能开销。 Promise 是 JavaScript 中处理异步操作的一种方式,它提供了比传统的回调函数和事件更强大和灵活的异步编程模型。以下是 Promise 的原理及其在面试中常被问到的关键点:
1. Promise 状态
Promise 对象有三种状态:
- pending(进行中) :初始状态,既不是成功也不是失败。
- fulfilled(已成功) :表示操作成功完成。
- rejected(已失败) :表示操作失败。
一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,这个状态就不可逆,并且会触发相应的回调函数。
2. Promise 构造函数
Promise 是通过 new Promise() 创建的,构造函数接受一个函数作为参数,这个函数又接受两个参数 resolve 和 reject,分别用于将 Promise 的状态变为 fulfilled 或 rejected。
javascript
const promise = new Promise((resolve, reject) => {
// 异步操作
if (/* 成功条件 */) {
resolve(value); // 成功时调用 resolve
} else {
reject(error); // 失败时调用 reject
}
});
3. then 和 catch 方法
- then(onFulfilled, onRejected) :用于注册当 Promise 被解决(无论是 fulfilled 还是 rejected)时的回调函数。
- catch(onRejected) :用于捕获 Promise 链中的错误,相当于
then(null, onRejected)。
Promise 支持链式调用,因为 then 和 catch 方法都会返回一个新的 Promise 对象。
4. Promise 链式调用
Promise 的核心优势之一是支持链式调用。每个 then 返回一个新的 Promise,使得可以继续在其后添加新的 then 或 catch。
javascript
promise
.then(result => {
return result * 2; // 返回值会传递给下一个 then
})
.then(doubleResult => {
console.log(doubleResult);
})
.catch(error => {
console.error(error); // 捕获前面的错误
});
5. 错误传播
如果在 Promise 链中的任何一个环节发生错误,并且没有被当前的 catch 捕获,错误会一直向后传播,直到被某个 catch 捕获或者导致未处理的 Promise rejection。
6. Promise.all、Promise.race、Promise.resolve、Promise.reject
- Promise.all(iterable) :等待所有 Promise 完成,如果其中任意一个 Promise 被拒绝,则立即拒绝。
- Promise.race(iterable) :只要有一个 Promise 解决或拒绝,就立即以相同的结果结束。
- Promise.resolve(value) :返回一个以给定值解析的 Promise。
- Promise.reject(reason) :返回一个以给定原因拒绝的 Promise。
7. 微任务队列
Promise 的回调函数(如 then 和 catch)会被放入微任务队列中,确保它们在当前脚本执行完成后尽快执行,但会在任何宏任务之前执行。这保证了 Promise 回调具有更高的优先级。
8. 实现原理简述
内部实现上,Promise 包含以下几个部分:
- 状态管理:跟踪 Promise 的状态(pending、fulfilled、rejected)。
- 值存储:保存 Promise 解决后的结果或拒绝的原因。
- 回调队列:当 Promise 的状态发生变化时,执行相应的回调函数。
- 链式调用支持:通过返回新的 Promise 来支持链式调用。
9. 常见问题与陷阱
- 忘记 catch 错误:未处理的 Promise rejection 可能会导致程序崩溃。
- 嵌套 Promise:过度嵌套可能导致代码难以理解和维护。
- Promise 链中断:如果没有正确返回值或抛出错误,可能会导致后续的
then不执行。
10. 手动实现简易 Promise
虽然完整的 Promise/A+ 规范较为复杂,但在面试中通常只需要实现一个简化版的 Promise,支持基本的 then 和链式调用即可。在 JavaScript 中,事件循环(Event Loop)是理解异步编程的核心机制。以下是面试中常见的关于事件循环的知识点总结:
1. JavaScript 是单线程语言
JavaScript 最初设计为单线程语言,意味着它只有一个主线程来执行代码。为了避免阻塞,JS 引入了 事件循环机制 来处理异步操作。
2. 调用栈(Call Stack)
- JS 引擎用来管理函数调用的数据结构。
- 每当一个函数被调用,它会被压入调用栈;执行完成后弹出。
javascript
function foo() {
console.log("foo");
}
function bar() {
foo();
}
bar(); // call stack: bar -> foo
3. Web APIs(宿主环境提供的功能)
浏览器提供的一些异步功能,例如:
setTimeoutsetIntervalfetchDOM 事件
这些任务由浏览器执行,完成后将回调放入对应的队列中。
4. 回调队列(Callback Queue)
-
当 Web API 完成任务后,其回调函数会被放入相应的队列中等待执行。
-
包括:
-
宏任务队列(Macro Task Queue)
- 如:
setTimeout,setInterval,I/O,UI 渲染,script标签加载等。
- 如:
-
微任务队列(Micro Task Queue)
- 如:
Promise.then/catch/finally,queueMicrotask,MutationObserver
- 如:
-
5. 事件循环流程图解
简化版流程如下:
+----------------------+
| Call Stack |
+----------+-----------+
|
v
+----------------------+
| Web APIs |
+----------+-----------+
|
v
+----------------------+ +------------------------+
| Callback Queues | --> | Event Loop (检查队列) |
+----------+-----------+ +------------------------+
|
+------v-------+
| Micro Task Q | <---- 优先级更高
+--------------+
| Macro Task Q |
+--------------+
6. 微任务 vs 宏任务
| 类型 | 示例 | 特点 |
|---|---|---|
| 微任务 | Promise.then, queueMicrotask | 优先级高,在当前宏任务结束后立即清空微任务队列 |
| 宏任务 | setTimeout, setInterval | 正常排队,每次事件循环处理一个宏任务 |
示例说明:
javascript
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise then");
});
console.log("End");
// 输出顺序:
// Start
// End
// Promise then
// setTimeout
7. 事件循环的完整流程
- 执行全局同步代码(宏任务)。
- 清空所有微任务队列中的任务。
- 渲染页面(如果需要)。
- 等待下一个宏任务到来并重复上述流程。
8. 常见面试题解析
✅ 题目1:
javascript
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('C');
});
console.log('D');
// 输出:A D C B
✅ 题目2:
javascript
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('then');
}).then(() => {
console.log('then again');
});
console.log('end');
// 输出:start end then then again setTimeout
9. 实际应用与优化
- 使用
Promise.then()替代嵌套的setTimeout提高性能。 - 避免在微任务中执行耗时操作,否则会阻塞后续渲染或用户交互。
- 可以使用
queueMicrotask(fn)或MutationObserver进行更细粒度的异步控制。
10. 扩展知识点(进阶面试)
- Node.js 的事件循环和浏览器的区别
process.nextTick()在 Node 中优先于Promise.thenrequestIdleCallback和requestAnimationFrame的使用场景 在 JavaScript 面试中,深拷贝(Deep Copy)与浅拷贝(Shallow Copy) 是常被问到的基础知识点。它们涉及对象、数组等引用类型的数据复制行为。
🧠 一、基本概念
1. 浅拷贝(Shallow Copy)
- 仅复制对象的第一层属性。
- 如果属性是引用类型(如对象或数组),则复制的是其 引用地址。
- 修改嵌套对象的值会影响原对象。
常见实现方式:
javascript
// Object.assign
const obj2 = Object.assign({}, obj1);
// 扩展运算符
const obj2 = { ...obj1 };
// 数组 slice
const arr2 = arr1.slice();
示例:
javascript
const user = {
name: 'Tom',
info: { age: 20 }
};
const copy = Object.assign({}, user);
copy.info.age = 30;
console.log(user.info.age); // 输出 30,说明引用共享了
2. 深拷贝(Deep Copy)
- 递归复制对象的所有层级属性。
- 原始对象和拷贝对象完全独立,互不影响。
常见实现方式:
✅ 简单但有限的方法:
javascript
JSON.parse(JSON.stringify(obj))
⚠️ 缺点:不能处理函数、undefined、Symbol、循环引用等。
✅ 递归实现(基础版):
javascript
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const copy = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
✅ 使用第三方库(推荐):
lodash的cloneDeepstructuredClone()(现代浏览器支持)
📌 二、面试常见问题解析
Q1:为什么 JSON.parse(JSON.stringify(obj)) 不是真正的深拷贝?
-
无法复制以下内容:
- 函数(function)
- Symbol 类型
- undefined
- 循环引用(如
obj.self = obj) - Date 对象会被转为字符串
Q2:如何判断两个对象是否相等?
===只比较引用。- 要判断内容是否相同,需手动遍历或使用工具库(如
lodash.isEqual)。
Q3:如何处理循环引用?
可以使用一个 WeakMap 来记录已克隆的对象:
javascript
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj);
const copy = Array.isArray(obj) ? [] : {};
map.set(obj, copy);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key], map);
}
}
return copy;
}
📊 三、对比总结
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制层级 | 第一层 | 所有层级 |
| 引用共享 | 是 | 否 |
| 性能 | 快 | 相对慢 |
| 实现复杂度 | 简单 | 复杂(需考虑循环引用等) |
| 常用方法 | Object.assign, 扩展运算符 | JSON.parse/stringify, 递归, lodash |
💡 四、实际应用场景
| 场景 | 应该使用 |
|---|---|
| 数据展示 | 浅拷贝即可 |
| 表单数据编辑(需还原) | 深拷贝 |
| Vuex 中的状态快照备份 | 深拷贝 |
| 配置项复制 | 根据需求选择 |
在 JavaScript 中,原型(Prototype)与原型链(Prototype Chain) 是理解对象继承机制的核心概念。以下是面试中常被问到的原型和原型链知识点总结:
一、原型(Prototype)
1. 每个函数都有一个 prototype 属性
- 函数的
prototype是一个对象,用于构建实例对象的原型。 - 实例对象会继承构造函数
prototype上的方法和属性。
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, ${this.name}`);
};
const p1 = new Person('Tom');
p1.sayHello(); // Hello, Tom
2. 每个对象都有一个 __proto__ 属性
__proto__是对象的一个内部属性,指向其构造函数的prototype。- 它是实现继承的关键。
javascript
console.log(p1.__proto__ === Person.prototype); // true
二、原型链(Prototype Chain)
1. 原型链的本质
- 当访问一个对象的属性或方法时,如果该对象自身没有这个属性,JavaScript 引擎会沿着
__proto__查找其原型对象。 - 如果原型对象也没有,继续向上查找,直到
Object.prototype或null为止。
2. 示例说明
javascript
function Animal() {}
Animal.prototype.eat = function() {
console.log('Animal is eating.');
};
function Dog() {}
Dog.prototype = Object.create(Animal.prototype); // 继承 Animal
Dog.prototype.bark = function() {
console.log('Dog is barking.');
};
const dog = new Dog();
dog.eat(); // Animal is eating.
dog.bark(); // Dog is barking.
三、构造函数、实例、原型之间的关系图
实例对象
↓ __proto__
构造函数.prototype
↓ __proto__
父级构造函数.prototype
↓ __proto__
Object.prototype
↓ __proto__
null
四、常见面试题解析
✅ 题目1:什么是原型链?为什么需要它?
答: 原型链是 JavaScript 实现继承的方式。每个对象都有一个 __proto__ 属性,指向其构造函数的 prototype,从而形成一条链式结构。通过原型链,子对象可以访问父对象上的属性和方法。
✅ 题目2:如何判断一个对象是否属于某个构造函数的实例?
答: 使用 instanceof 运算符。
javascript
function Person() {}
const p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Object); // true
✅ 题目3:如何修改原型对象?
答: 可以通过直接赋值给 构造函数.prototype 或使用 Object.setPrototypeOf() 修改原型。
javascript
Person.prototype.walk = function() {
console.log(`${this.name} is walking.`);
};
⚠️ 注意:不要直接重写整个原型对象,否则会破坏原有的原型链。
✅ 题目4:constructor 属性的作用是什么?
答: constructor 是原型对象上的默认属性,指向对应的构造函数。
javascript
console.log(Person.prototype.constructor === Person); // true
✅ 题目5:如何实现类的继承?
答: 使用 Object.create() 或 class extends(ES6+)
ES5 方式:
javascript
function Parent() {}
Parent.prototype.parentMethod = function() {};
function Child() {}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.childMethod = function() {};
ES6 方式:
javascript
class Parent {}
class Child extends Parent {}
五、原型链常见陷阱
| 问题 | 描述 |
|---|---|
| 共享引用类型数据 | 多个实例共享原型中的引用类型(如数组),修改一个会影响其他实例。 |
忘记绑定 this | 在原型方法中使用 this 时,如果作为回调传入其他上下文,可能会出错。 |
| 滥用原型链导致性能下降 | 过长的原型链会增加属性查找时间。 |
六、扩展知识点(进阶面试)
| 知识点 | 说明 |
|---|---|
Object.getPrototypeOf(obj) | 获取对象的原型 |
Object.setPrototypeOf(obj, prototype) | 设置对象的原型 |
isPrototypeOf() | 判断一个对象是否存在于另一个对象的原型链上 |
hasOwnProperty() | 判断属性是否为对象自身的属性 |