1. scss 与 less 的区别,scss 对比与 less 的优势有哪些?
SCSS 与 Less 的区别
1. 实现方式
- Less:基于,在客户端处理,引入
less.js即可处理。 - SCSS:基于 Ruby,在服务器端处理。
2. 语法差异
-
代码块与结尾:
- SCSS:使用大括号
{}来定义代码块,且语句必须以分号;结尾。 - Less:语法更接近 CSS,使用大括号
{}定义代码块,语句结尾不强制要求分号。
- SCSS:使用大括号
-
变量定义:
- SCSS:使用
$符号定义变量,例如$color: #fff;。 - Less:使用
@符号定义变量,例如@color: #fff;。
- SCSS:使用
-
Mixin(混合) :
- SCSS:使用
@mixin关键字定义样式,使用@include将混合样式应用到选择器中。 - Less:使用
@mixin关键字定义多个 CSS 规则,使用@apply选择器应用混合后的样式。
- SCSS:使用
-
继承:
- SCSS:使用
@extend关键字扩展样式。 - Less:通过
extend关键字扩展样式。
- SCSS:使用
-
注释:
- SCSS:注释只能使用
/* */的方式。 - Less:可以使用
//或者/* */的方式。
- SCSS:注释只能使用
-
导入其他文件:
- SCSS:可以使用
@import指令导入其他文件,文件后缀可以是.scss或.sass。 - Less:使用
import关键字导入.less文件。
- SCSS:可以使用
3. 特性支持差异
-
编程能力:
- SCSS:可编程能力比较强,支持函数列表、对象、判断、循环等。
- Less:编程能力弱,不直接支持对象、循环、判断等,只有
when判断。
-
变量作用域:
- SCSS:变量作用域更灵活。
- Less:作用域相对固定。
SCSS 对比 Less 的优势
- 用户与学习资源:使用 SCSS 的用户更多,更容易找到会用 SCSS 的开发者,也更容易找到相关的学习资源。
- 功能丰富度:SCSS 可编程能力强,支持函数、列表、对象、判断、循环等,相比 Less 有更多的功能。
- 库资源:有丰富的 Sass 库,如 Compass、Bourbon 等。
1. 数组去重对象,将重复的 id 进行一个去重
方法一:使用 Map 对象
Map 对象可以存储键值对,并且键是唯一的。我们可以利用这个特性,以对象的 id 作为键,对象本身作为值,将数组中的对象存储到 Map 中,最后将 Map 的值转换为数组。
javascript
const arr = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' }
];
function removeDuplicatesById(arr) {
const map = new Map();
arr.forEach(item => {
if (!map.has(item.id)) {
map.set(item.id, item);
}
});
return Array.from(map.values());
}
const uniqueArr = removeDuplicatesById(arr);
console.log(uniqueArr);
方法二:使用 reduce 方法
reduce 方法可以对数组中的每个元素执行一个提供的函数,并将其结果汇总为单个值。我们可以在 reduce 方法中检查当前元素的 id 是否已经存在于结果数组中,如果不存在则添加到结果数组中。
javascript
const arr = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' }
];
function removeDuplicatesById(arr) {
return arr.reduce((acc, current) => {
const existing = acc.find(item => item.id === current.id);
if (!existing) {
return [...acc, current];
}
return acc;
}, []);
}
const uniqueArr = removeDuplicatesById(arr);
console.log(uniqueArr);
方法三:使用 filter 方法
filter 方法会创建一个新数组,其包含通过所提供函数实现的测试的所有元素。我们可以在 filter 方法中检查当前元素的 id 在数组中第一次出现的索引是否等于当前索引,如果相等则保留该元素。
javascript
const arr = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' }
];
function removeDuplicatesById(arr) {
return arr.filter((item, index, self) => {
return index === self.findIndex(t => t.id === item.id);
});
}
const uniqueArr = removeDuplicatesById(arr);
console.log(uniqueArr);
以上三种方法都可以实现根据对象的 id 属性对数组进行去重,你可以根据实际情况选择合适的方法。其中,使用 Map 对象的方法性能相对较好,因为 Map 的查找操作时间复杂度为 O(1)。
webpack 构建打包优化,你在项目中都做了什么
1. 压缩代码
压缩代码可以减小文件体积,加快加载速度。可以使用 terser-webpack-plugin 来压缩 JavaScript 代码,使用 optimize-css-assets-webpack-plugin 来压缩 CSS 代码。
javascript
const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 移除 console 语句
},
},
}),
new OptimizeCSSAssetsPlugin({}),
],
},
};
2. 分割代码
使用 splitChunks 配置项将代码分割成多个文件,实现按需加载,减少首屏加载时间。
javascript
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 对所有类型的 chunk 进行分割
minSize: 30000, // 生成 chunk 的最小体积(以 bytes 为单位)
maxSize: 0,
minChunks: 1, // 分割前必须共享模块的最小 chunks 数
maxAsyncRequests: 5, // 按需加载时的最大并行请求数
maxInitialRequests: 3, // 入口点的最大并行请求数
automaticNameDelimiter: '~',
cacheGroups: {
vendors: {
test: /[\/]node_modules[\/]/,
priority: -10,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
3. 缓存
使用 cache 配置项开启 Webpack 缓存,避免重复构建。
javascript
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
},
};
4. 懒加载
对于一些不急需加载的模块,可以使用懒加载的方式,在需要的时候再进行加载。在 Vue 或 React 中都有相应的实现方式。
Vue 中的懒加载示例
javascript
const Home = () => import('./views/Home.vue');
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
];
React 中的懒加载示例
jsx
const Home = React.lazy(() => import('./views/Home'));
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Home />
</React.Suspense>
);
}
5. 排除不必要的文件
使用 exclude 配置项排除不需要处理的文件,减少 Webpack 的处理范围。
javascript
module.exports = {
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/, // 排除 node_modules 目录下的文件
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
};
6. 使用 DllPlugin 和 DllReferencePlugin
对于一些不经常变化的第三方库,可以使用 DllPlugin 提前打包,然后使用 DllReferencePlugin 在主项目中引用。
生成 DLL 文件
javascript
// webpack.dll.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendors: ['react', 'react-dom'],
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, 'dll'),
library: '[name]_[hash]',
},
plugins: [
new webpack.DllPlugin({
name: '[name]_[hash]',
path: path.resolve(__dirname, 'dll/[name].manifest.json'),
}),
],
};
在主项目中引用 DLL 文件
javascript
// webpack.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
// ...其他配置
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dll/vendors.manifest.json'),
}),
],
};
通过以上这些优化策略,可以显著提高 Webpack 的构建打包性能,提升项目的加载速度和用户体验。
1. 单点登录 SSO,你主要做了什么,怎么样实现了单点登录
单点登录(SSO)允许用户使用一组凭据(如用户名和密码)登录到多个相关的应用系统,而无需为每个系统单独登录。在前端实现单点登录,通常可以采用以下步骤和方法:
1. 选择合适的认证协议
常见的单点登录认证协议有 OAuth 2.0、OpenID Connect 等。这里以 OpenID Connect 为例,它是基于 OAuth 2.0 的身份验证协议,提供了用户身份信息。
2. 前端实现步骤
步骤 1:重定向到认证服务器
当用户访问前端应用时,前端应用检查本地是否有有效的令牌(token)。如果没有,则将用户重定向到认证服务器的登录页面。
javascript
// 前端代码示例,重定向到认证服务器
const authServerUrl = 'https://example-auth-server.com/auth';
const clientId = 'your-client-id';
const redirectUri = 'https://your-app.com/callback';
const scope = 'openid profile email';
const state = generateRandomState(); // 生成随机的 state 参数,防止 CSRF 攻击
const authUrl = `${authServerUrl}?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code&state=${state}`;
window.location.href = authUrl;
function generateRandomState() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let state = '';
for (let i = 0; i < 16; i++) {
state += characters.charAt(Math.floor(Math.random() * characters.length));
}
return state;
}
步骤 2:处理认证回调
用户在认证服务器登录成功后,认证服务器会将用户重定向回前端应用的回调页面,并附带一个授权码(code)。前端应用需要从 URL 中提取授权码,并将其发送到后端服务器进行交换令牌。
javascript
// 前端代码示例,处理认证回调
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// 验证 state 参数,防止 CSRF 攻击
if (state === localStorage.getItem('state')) {
// 发送授权码到后端服务器进行交换令牌
fetch('https://your-backend-server.com/exchange-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
})
.then(response => response.json())
.then(data => {
// 保存令牌到本地存储
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('id_token', data.id_token);
// 重定向到应用的主页
window.location.href = '/';
})
.catch(error => {
console.error('Token exchange failed:', error);
});
} else {
console.error('Invalid state parameter');
}
步骤 3:使用令牌进行请求
前端应用在后续的请求中,需要将令牌添加到请求头中,以证明用户的身份。
javascript
// 前端代码示例,使用令牌进行请求
const accessToken = localStorage.getItem('access_token');
fetch('https://your-api-server.com/data', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
.then(response => response.json())
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Request failed:', error);
});
步骤 4:处理令牌过期
当令牌过期时,前端应用需要刷新令牌或重新引导用户登录。可以在请求返回 401 状态码时,进行相应的处理。
javascript
// 前端代码示例,处理令牌过期
fetch('https://your-api-server.com/data', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
.then(response => {
if (response.status === 401) {
// 令牌过期,刷新令牌或重新登录
refreshToken();
}
return response.json();
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Request failed:', error);
});
function refreshToken() {
// 发送刷新令牌请求到后端服务器
const refreshToken = localStorage.getItem('refresh_token');
fetch('https://your-backend-server.com/refresh-token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh_token: refreshToken })
})
.then(response => response.json())
.then(data => {
// 更新令牌
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
// 重新发起请求
window.location.reload();
})
.catch(error => {
console.error('Token refresh failed:', error);
// 重新引导用户登录
window.location.href = '/login';
});
}
3. 跨域问题处理
如果前端应用和认证服务器、后端服务器不在同一个域名下,需要处理跨域问题。可以在后端服务器配置 CORS 头,允许前端应用的域名进行跨域请求。
javascript
// 后端 Node.js 示例,配置 CORS 头
const express = require('express');
const app = express();
const cors = require('cors
app.use(cors({
origin: 'https://your-app.com',
credentials: true
}));
// 其他路由和中间件配置
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
通过以上步骤,前端可以实现单点登录功能,为用户提供便捷的登录体验。
1. vue3 中是如何使用 proxy 来进行代理的?
在 Vue 3 中,Proxy 对象被用于实现响应式系统。Vue 3 通过 Proxy 对数据对象进行代理,当对象的属性被访问或修改时,Vue 能够自动追踪这些变化并更新与之绑定的 DOM。以下是关于 Vue 3 如何使用 Proxy 进行代理的详细介绍:
基本原理
Vue 3 的响应式系统基于 Proxy 对象,它允许你拦截并自定义对象的基本操作,如属性访问、赋值、枚举、函数调用等。当你创建一个响应式对象时,Vue 会使用 Proxy 来包装原始对象,从而能够追踪对象属性的访问和修改。
示例代码
下面是一个简单的示例,展示了如何手动使用 Proxy 来实现类似 Vue 3 响应式的功能:
javascript
// 模拟 Vue 3 的响应式系统
function reactive(target) {
// 创建一个 Proxy 对象来包装目标对象
return new Proxy(target, {
// 拦截属性访问操作
get(target, key) {
console.log(`Getting property ${key}`);
return target[key];
},
// 拦截属性赋值操作
set(target, key, value) {
console.log(`Setting property ${key} to ${value}`);
target[key] = value;
// 这里可以触发更新操作,如更新 DOM
return true;
}
});
}
// 创建一个普通对象
const original = {
message: 'Hello, Vue 3!'
};
// 创建响应式对象
const reactiveObj = reactive(original);
// 访问属性
console.log(reactiveObj.message);
// 修改属性
reactiveObj.message = 'New message';
在 Vue 3 组件中使用
在 Vue 3 组件中,你可以使用 reactive 函数来创建响应式对象。以下是一个简单的 Vue 3 组件示例:
vue
<template>
<div>
<p>{{ state.message }}</p>
<button @click="updateMessage">Update Message</button>
</div>
</template>
<script setup>
import { reactive } from 'vue';
// 创建响应式对象
const state = reactive({
message: 'Hello, Vue 3!'
});
// 定义更新消息的函数
const updateMessage = () => {
state.message = 'New message';
};
</script>
代码解释
reactive函数:Vue 3 提供的reactive函数用于创建响应式对象。它内部使用Proxy来包装原始对象,并拦截属性的访问和赋值操作。get拦截器:当访问响应式对象的属性时,get拦截器会被触发。在这个拦截器中,你可以执行一些额外的操作,如依赖收集。set拦截器:当修改响应式对象的属性时,set拦截器会被触发。在这个拦截器中,你可以执行一些额外的操作,如触发更新。
通过使用 Proxy,Vue 3 能够实现高效的响应式系统,使得数据的变化能够自动反映在 DOM 上。
虚拟列表是如何实现的?自己做虚拟列表主要是做了哪些工作
虚拟列表是一种优化长列表渲染性能的技术,当需要展示大量数据时,如果一次性将所有数据渲染到页面上,会导致页面加载缓慢、滚动卡顿等问题。虚拟列表的核心思想是只渲染当前可见区域的数据,当滚动时动态更新渲染的数据,从而减少 DOM 节点的数量,提高性能。
以下是实现虚拟列表的基本步骤和原理:
1. 计算可见区域
- 首先需要知道列表容器的高度和滚动位置,通过这两个信息可以计算出当前可见区域的起始和结束位置。
2. 确定渲染数据范围
- 根据可见区域的起始和结束位置,从原始数据中截取相应的数据片段,只渲染这部分数据。
3. 占位元素
- 为了保证列表的滚动条正常显示和滚动位置的正确性,需要在列表容器中添加占位元素,占位元素的高度等于整个列表数据的总高度。
4. 监听滚动事件
- 当用户滚动列表时,重新计算可见区域和渲染数据范围,并更新 DOM 节点。
自己做虚拟列表主要的工作
1. 布局和样式设计
- 设计列表容器和列表项的样式,确保列表的外观符合需求。
- 确定列表项的高度,可以是固定高度或动态高度。
2. 数据处理
- 准备原始数据,通常是一个数组。
- 根据可见区域计算需要渲染的数据片段。
3. 滚动事件监听
- 监听列表容器的滚动事件,在滚动时重新计算可见区域和渲染数据范围。
4. DOM 操作
- 创建和更新需要渲染的 DOM 节点,将截取的数据渲染到页面上。
- 管理占位元素的高度,确保滚动条的正确性。
示例代码
以下是一个简单的虚拟列表实现示例:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
#list-container {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
}
.list-item {
height: 30px;
line-height: 30px;
border-bottom: 1px solid #eee;
padding: 0 10px;
}
</style>
</head>
<body>
<div id="list-container
<div id="placeholder"></div>
<div id="visible-list"></div>
</div>
<script>
// 模拟原始数据
const data = Array.from({ length: 1000 }, (_, index) => `Item ${index + 1}`);
const listContainer = document.getElementById('list-container');
const placeholder = document.getElementById('placeholder');
const visibleList = document.getElementById('visible-list');
const itemHeight = 30;
// 计算占位元素的高度
placeholder.style.height = `${data.length * itemHeight}px`;
// 渲染可见区域的数据
function renderVisibleList() {
const scrollTop = listContainer.scrollTop;
const containerHeight = listContainer.offsetHeight;
// 计算可见区域的起始和结束索引
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(startIndex + Math.ceil(containerHeight / itemHeight), data.length);
// 清空可见列表
visibleList.innerHTML = '';
// 渲染可见区域的数据
for (let i = startIndex; i < endIndex; i++) {
const item = document.createElement('div');
item.classList.add('list-item');
item.textContent = data[i];
item.style.transform = `translateY(${i * itemHeight}px)`;
visibleList.appendChild(item);
}
}
// 初始化渲染
renderVisibleList();
// 监听滚动事件
listContainer.addEventListener('scroll', renderVisibleList);
</script>
</body>
</html>
代码解释
-
HTML 结构:包含一个列表容器
list-container,内部有一个占位元素placeholder和一个用于渲染可见数据的visible-list。 -
CSS 样式:设置列表容器的高度和滚动条样式,以及列表项的高度和样式。
-
JavaScript 代码:
- 模拟原始数据
data。 - 计算占位元素的高度,确保滚动条的正确性。
renderVisibleList函数用于计算可见区域的起始和结束索引,截取相应的数据片段并渲染到页面上。- 监听列表容器的滚动事件,在滚动时重新调用
renderVisibleList函数更新渲染数据。
- 模拟原始数据
webpack 文件压缩,到底是压缩掉了什么内容?
1. 空白字符
- 空格、制表符和换行符:在代码中,为了提高可读性,开发者会添加大量的空格、制表符和换行符,但这些字符在代码运行时并没有实际作用。例如,在 JavaScript 代码中:
javascript
function add(a, b) {
return a + b;
}
压缩后可能会变成:
javascript
function add(a,b){return a+b;}
2. 注释
- 单行注释和多行注释:注释是为了帮助开发者理解代码,但在生产环境中,这些注释对代码的执行没有影响,可以被移除。例如:
javascript
// 这是一个加法函数
function add(a, b) {
/*
返回两个数的和
*/
return a + b;
}
压缩后注释会被去掉:
javascript
function add(a,b){return a+b;}
3. 冗余代码
- 未使用的变量和函数:如果代码中定义了一些变量或函数,但在整个项目中并没有被使用,压缩工具会将其移除。例如:
javascript
function unusedFunction() {
return 'This function is not used';
}
function add(a, b) {
return a + b;
}
const result = add(1, 2);
压缩后 unusedFunction 可能会被移除:
javascript
function add(a,b){return a+b;}const result=add(1,2);
4. 缩短变量和函数名
- 重命名标识符:压缩工具会将长的变量名和函数名替换为更短的名称,只要不影响代码的逻辑。例如:
javascript
function calculateSumOfTwoNumbers(a, b) {
return a + b;
}
压缩后可能变成:
javascript
function c(a,b){return a+b;}
5. 代码优化
- 常量折叠:对于一些常量表达式,压缩工具会在编译时进行计算,减少运行时的计算量。例如:
javascript
const a = 2 + 3;
压缩后可能直接变成:
javascript
const a = 5;
在 Webpack 中,通常会使用 terser-webpack-plugin 来进行 JavaScript 文件的压缩,使用 css-minimizer-webpack-plugin 来进行 CSS 文件的压缩。这些插件会自动完成上述的压缩操作。
1. 大文件文本上传是如何设计的?
前端部分
- 文件切片
将大文件切割成多个小块,这样可以降低单次上传的数据量,减少网络波动对上传的影响,也便于实现断点续传。可以使用File.prototype.slice方法来实现文件切片。
javascript
function sliceFile(file, chunkSize) {
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
chunks.push(chunk);
start = end;
}
return chunks;
}
- 进度监控
利用XMLHttpRequest或fetchAPI 的进度事件来监控每个切片的上传进度,然后汇总计算整个文件的上传进度。
javascript
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
// 更新进度条
console.log(`当前切片上传进度: ${percentComplete}%`);
}
});
- 断点续传
在上传过程中,记录已经上传的切片信息。当上传中断后,下次上传时可以跳过已经上传的切片,只上传未上传的部分。可以使用本地存储(如localStorage)来记录上传信息。
javascript
// 记录已上传的切片索引
const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
// 上传未上传的切片
for (let i = 0; i < chunks.length; i++) {
if (!uploadedChunks.includes(i)) {
// 上传切片
}
}
大文件上传超时咋办
前端解决方案
1. 优化切片策略
- 调整切片大小:如果切片过大,可能会导致单个切片上传时间过长而超时。可以适当减小切片大小,增加并发上传的切片数量,从而提高上传效率。
javascript
// 调整切片大小为 2MB
const chunkSize = 2 * 1024 * 1024;
function sliceFile(file, chunkSize) {
const chunks = [];
let start = 0;
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
chunks.push(chunk);
start = end;
}
return chunks;
}
2. 增加重试机制
- 当某个切片上传超时时,前端可以自动重试上传该切片,避免因网络波动等原因导致上传失败。
javascript
async function uploadChunkWithRetry(chunk, index, maxRetries = 3) {
let retries = 0;
while (retries < maxRetries) {
try {
await uploadChunk(chunk, index);
return;
} catch (error) {
retries++;
console.log(`第 ${index} 个切片上传失败,正在进行第 ${retries} 次重试...`);
}
}
throw new Error(`第 ${index} 个切片上传失败,已达到最大重试次数`);
}
3. 优化并发控制
- 合理控制并发上传的切片数量,避免过多的并发请求导致网络拥塞,从而增加超时的风险。
javascript
async function uploadChunks(chunks, concurrency = 3) {
let index = 0;
async function uploadNext() {
if (index < chunks.length) {
const currentIndex = index++;
await uploadChunkWithRetry(chunks[currentIndex], currentIndex);
await uploadNext();
}
}
const promises = Array.from({ length: concurrency }, uploadNext);
await Promise.all(promises);
}
1. web worker 和主线程之间的交互是怎么实现的?
Web Worker 允许在主线程之外创建一个独立的线程来执行脚本,从而避免阻塞主线程,提高页面的响应性能。Web Worker 和主线程之间的交互主要通过 postMessage() 方法和 onmessage 事件来实现,以下是详细介绍:
基本原理
postMessage()方法:用于在主线程和 Web Worker 之间发送消息。消息可以是各种数据类型,如字符串、对象、数组等。onmessage事件:用于监听接收到的消息。当一方调用postMessage()发送消息时,另一方可以通过监听onmessage事件来接收消息。
实现步骤
1. 创建 Web Worker
在主线程中,使用 Worker 构造函数创建一个新的 Web Worker 实例,并指定要执行的脚本文件路径。
javascript
// main.js(主线程)
// 创建一个新的 Web Worker 实例
const worker = new Worker('worker.js');
2. 主线程向 Web Worker 发送消息
在主线程中,调用 postMessage() 方法向 Web Worker 发送消息。
javascript
// main.js(主线程)
// 向 Web Worker 发送消息
worker.postMessage('Hello from main thread!');
3. Web Worker 接收并处理消息
在 Web Worker 脚本中,监听 onmessage 事件来接收主线程发送的消息,并进行相应的处理。
javascript
// worker.js(Web Worker)
// 监听接收到的消息
self.onmessage = function(event) {
const message = event.data;
console.log('Received message from main thread:', message);
// 向主线程发送回复消息
self.postMessage('Hello from worker!');
};
4. 主线程接收 Web Worker 的回复消息
在主线程中,监听 Web Worker 的 onmessage 事件来接收 Web Worker 发送的回复消息。
javascript
// main.js(主线程)
// 监听 Web Worker 发送的消息
worker.onmessage = function(event) {
const message = event.data;
console.log('Received message from worker:', message);
};
完整示例代码
主线程代码(main.js)
javascript
// 创建一个新的 Web Worker 实例
const worker = new Worker('worker.js');
// 向 Web Worker 发送消息
worker.postMessage('Hello from main thread!');
// 监听 Web Worker 发送的消息
worker.onmessage = function(event) {
const message = event.data;
console.log('Received message from worker:', message);
};
Web Worker 代码(worker.js)
javascript
// 监听接收到的消息
self.onmessage = function(event) {
const message = event.data;
console.log('Received message from main thread:', message);
// 向主线程发送回复消息
self.postMessage('Hello from worker!');
};
注意事项
- 同源策略:Web Worker 脚本文件必须与主线程的页面同源,否则会引发安全错误。
- 数据复制:
postMessage()方法传递的数据是通过复制的方式进行的,而不是引用传递。因此,在传递大量数据时需要注意性能问题。 - 错误处理:可以通过监听
onerror事件来捕获 Web Worker 中发生的错误。
javascript
// main.js(主线程)
worker.onerror = function(error) {
console.error('Web Worker error:', error.message);
};
通过以上步骤,就可以实现 Web Worker 和主线程之间的双向通信。
1. postMessage 如何区分是 iframe 还是 web worker 发的消息
1. 消息来源属性判断
- 原理:
postMessage传递的事件对象有不同的属性可以用于区分消息来源。对于iframe发送的消息,事件对象的source属性会指向iframe的window对象;而对于Web Worker发送的消息,事件对象没有source属性。 - 示例代码:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<iframe id="myIframe" src="iframe.html"></iframe>
<script>
// 创建 Web Worker
const worker = new Worker('worker.js');
// 监听来自 iframe 和 Web Worker 的消息
window.addEventListener('message', function (event) {
if (event.source) {
console.log('消息来自 iframe');
} else {
console.log('消息来自 Web Worker');
}
console.log('消息内容:', event.data);
});
// 向 Web Worker 发送消息
worker.postMessage('来自主线程的消息给 Web Worker');
</script>
</body>
</html>
iframe.html 文件内容:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
// 向父窗口发送消息
window.parent.postMessage('来自 iframe 的消息', '*');
</script>
</body>
</html>
worker.js 文件内容:
javascript
// 监听主线程的消息
self.onmessage = function (event) {
console.log('收到主线程的消息:', event.data);
// 向主线程发送消息
self.postMessage('来自 Web Worker 的消息');
};
2. 自定义消息格式
- 原理:在发送消息时,在消息内容中添加一个特定的标识字段,用于表明消息的来源。在接收消息时,通过检查这个标识字段来区分消息是来自
iframe还是Web Worker。
-示例代码**:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<iframe id="myIframe" src="iframe.html"></iframe>
<script>
// 创建 Web Worker
const worker = new Worker('worker.js');
// 监听来自 iframe 和 Web Worker 的消息
window.addEventListener('message', function (event) {
const { source, data } = event.data;
if (source === 'iframe') {
console.log('消息来自 iframe');
} else if (source === 'webWorker') {
console.log('消息来自 Web Worker');
}
console.log('消息内容:', data);
}); // 向 Web Worker 发送消息
worker.postMessage({ source: 'main', data: '来自主线程的消息给 Web Worker' });
</script>
</body>
</html>
iframe.html 文件内容:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
// 向父窗口发送消息
window.parent.postMessage({ source: 'iframe', data: '来自 iframe 的消息' }, '*');
</script>
</body>
</html>
worker.js 文件内容:
javascript
// 监听主线程的消息
self.onmessage = function (event) {
const { source, data } = event.data;
console.log(`收到 ${source} 的消息:`, data);
// 向主线程发送消息
self.postMessage({ source: 'webWorker', data: '来自 Web Worker 的消息' });
};
通过以上两种方式,就可以在使用 postMessage 时区分消息是来自 iframe 还是 Web Worker。
1. 代码规范,大致说说,项目中关注了哪些点
. 规范
- 变量和函数命名:使用有意义的名称,遵循驼峰命名法(camelCase)。例如
javascript
// 好的命名
let userInfo = { name: 'John', age: 30 };
function calculateTotalPrice(items) {
// 函数逻辑
}
// 不好的命名
let a = { n: 'John', a: 30 };
function calc(items) {
// 函数逻辑
}
- 类名:使用帕斯卡命名法(PascalCase)。例如:
javascript
class UserProfile {
constructor(name, age) {
this.name = name
this.age = age;
}
}
- 常量命名:使用全大写字母,单词之间用下划线分隔。例如:
javascript
const MAX_COUNT = 100;
### 2. 代码格式
- **缩进**:使用一致的缩进风格,通常为 2 个或 4 个空格。例如:
```javascript
function example() {
if (condition) {
// 代码块
}
}
- 空格和换行:在运算符、逗号、冒号等前后使用适当的空格,合理使用换行来提高代码的可读性。例如:
javascript
// 好格式
let sum = a + b;
let array = [1, 2, 3];
// 不好的格式
let sum=a+b;
let array=[1,2,3];
3. 注释规范
- 单行注释:
//进行单行注释,注释内容要简洁明了,解释代码的功能或意图。例如:
javascript
// 获取用户信息
userInfo = getUserInfo();
- 多行注释:使用
/* ... */进行多行注释,用于对函数、类等进行详细的说明。例如:
javascript
/*
* 计算两个数的和
* @param {number} a - 第一个数
* @param {number} b - 第二个数
* @returns {number} - 两个数的和
*/
function add(a, b) {
return a + b;
}
4. 代码结构
- 模块化:将代码拆分成多个模块,每个模块负责单一的功能,提高代码的可维护性和复用性。例如,使用 ES6 的模块语法:
javascript
// moduleA.jsexport function funcA() {
// 函数逻辑
}
// main.js
import { funcA } from './moduleA.js';
funcA();
- 文件组织:按照功能、类型等对文件进行合理的组织和分类。例如,将组件文件放在
components目录下,将样式文件放在styles目录下。
5. 错误处理
- 异常捕获:在可能出现异常的地方使用
try...catch语句进行异常捕获,避免程序崩溃。例如:
javascript
try {
// 可能会抛出异常的代码
let result = JSON.parse(jsonString);
} catch (error) {
console.error('解析 JSON 时出错:', error);
}
- 错误信息:提供清晰、有意义的错误信息,方便调试和定位问题。
6. 性能优化
- 避免全局变量:全局变量会增加命名冲突的风险,并且会影响代码的可维护性和性能。尽量使用局部变量。
- 减少 DOM 操作:DOM 操作是比较耗时的,尽量批量操作 DOM,避免频繁的重排和重绘。例如:
javascript
// 不好的做法
for (let i = 0; i < 100; i++) {
document.body.appendChild(document.createElement('div'));
}
// 好的做法
let fragment = document.createDocumentFragment();
for (let i 0; i < 100; i++) {
fragment.appendChild(document.createElement('div'));
}
document.body.appendChild(fragment);
7. 安全规范
- 防止 XSS 攻击:对用户输入进行过滤和转义,避免将用户输入直接插入到 HTML 中。例如:
javascript
function escapeHTML(str) {
return str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g,quot;')
.replace(/'/g, ''');
}
- 防止 CSRF 攻击:使用 CSRF 令牌来验证请求的合法性。
通过遵循以上代码规范,可以提高项目的代码质量和开发效率,减少潜在的问题和错误。同时,可以使用 ESLint、Prettier 等工具来自动化检查和格式化代码,确保代码规范的一致性。
git代码提交信息如何限制的?
1. 使用 Git Hooks
Git Hooks 是在 Git 执行特定操作(如提交、推送等)前后自动执行的脚本。可以使用 commit-msg 钩子来限制提交信息的格式。
步骤:
- **创建
commit-msg钩子脚本:在项目的.git/hooks目录下创建一个名为commit-msg的文件(如果不存在的话),并赋予可执行权限。
bash
cd your_project/.git/hooks
touch commit-msg
chmod +x commit-msg
- 编写脚本内容:以下是一个简单的示例脚本,要求提交信息必须以特定的前缀(如
feat:、fix:等)开头:
bash
#!/bin/sh
# 定义允许的提交信息前缀
ALLOWED_PREFIXES="feat:|fix:|docs:|style:|refactor:|test:|chore:"
# 获取提交信息
commit_msg=$(cat "$1")
# 检查提交信息是否符合格式
if ! echo "$commit_msg" | grep -qE "^($ALLOWED_PREFIXES)"; then
echo "提交信息必须以以下前缀之一开头: $ALLOWED_PREFIXES"
exit 1
fi
exit 0
- 解释:脚本首先定义了允许的提交信息前缀,然后读取提交信息,使用
grep命令检查提交信息是否以允许的前缀开头。如果不符合格式,脚本会输出错误信息并返回非零状态码,从而阻止提交。
2. 使用 Commitizen
Commitizen 是一个用于生成符合规范的提交信息的工具。它提供了一个交互式的命令行界面,引导开发者按照规范输入提交信息。
步骤:
- 安装 Commitizen:
bash
npm install -g commitizen
- 初始化项目:在项目根目录下运行以下命令,选择一个适配器(如
cz-conventional-changelog):
bash
commitizen init cz-conventional-changelog --save-dev --save-exact
- 使用 Commitizen 进行提交:使用
git cz代替git commit来提交代码,按照提示输入提交信息。
3. 使用 Husky 和 Commitlint
Husky 是一个 Git Hooks 工具,而 Commitlint 是一个用于检查提交信息格式的工具。结合使用这两个工具可以方便地实现提交信息的限制。
步骤:
- 安装依赖:
bash
npm install --save-dev husky @commitlint/config-conventional @commitlint/cli
- 配置 Commitlint:在项目根目录下创建一个
commitlint.config.js文件,配置提交信息的规则:
javascript
module.exports = {
extends: ['@commitlint/config-conventional'],
};
- 配置 Husky:在
package.json中添加 Husky 配置:
json
{
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAM"
}
}
}
- 解释:当执行
git commit时,Husky 会触发commit-msg钩子,调用 Commitlint 检查提交信息是否符合配置的规则。如果不符合规则,提交将被阻止。
通过以上方法,可以有效地限制 Git 代码提交信息的格式和内容,提高项目的代码管理质量。
1. JWT Token 是如何应用,在 axios 二次封装中
JSON Web Token(JWT)是一种用于在网络应用中安全传输信息的开放标准(RFC 7519)。在 Axios 二次封装中应用 JWT Token 通常可以按照以下步骤进行:
1. 安装 Axios
如果你还没有安装 Axios,可以使用 npm 或 yarn 进行安装:
bash
npm install axios
# 或者
yarn add axios
2. 封装 Axios
以下是一个简单的 Axios 二次封装示例,其中包含了 JWT Token 的应用:
javascript
import axios from 'axios';
// 创建一个 Axios
const service = axios.create({
baseURL: 'your_api_base_url', // API 的基础 URL
timeout: 5000 // 请求超时时间
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 从本地存储中获取 JWT Token
const token = localStorage.getItem('token');
if (token) {
// 在请求头中添加 JWT Token
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => {
console.log(error); // 打印错误信息
Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
return response.data;
},
error => {
console.log('err' + error); // 打印错误信息
return Promise.reject(error);
}
);
export default service;
### 3. 使用封装后的 Axios
在需要发送请求的地方引入封装后的 Axios 实例:
```javascript
import service from './axiosInstance';
// 发送 GET 请求
service.get('/api/data')
.then(response => {
console.log(response);
})
.catch(error => {
console.log(error);
});
// 发送 POST 请求
service.post('/api/login', {
username: 'your_username',
password: 'your_password'
})
.then(response => {
// 登录成功后,将 JWT Token 存储到本地存储中
const token = response.token;
localStorage.setItem('token', token);
})
.catch(error => {
console.log(error);
});
解释
- 请求拦截器:在每次发送请求之前,会检查本地存储中是否存在 JWT Token。如果存在,则在请求头中添加
Authorization字段,其值为Bearer ${token}。 - 响应拦截器:在接收到响应后,可以对响应数据进行处理,例如返回响应数据本身。
- 本地存储:在登录成功后,将服务器返回的 JWT Token 存储到本地存储中,以便后续请求使用。
通过以上步骤,你可以在 Axios 二次封装中应用 JWT Token,实现请求的身份验证。
1. 刷新页面,如何获取 Token,这些 Token 放置在哪儿呢?
1. 本地存储( Storage)
本地存储是一种在浏览器中存储数据的方式,数据会一直保留,直到手动清除。适合存储一些不敏感且需要长期保存的数据,如 Token。
javascript
// 存储 Token
localStorage.setItem('token', 'your_token_here');
// 获取 Token
const = localStorage.getItem('token');
2. 会话存储(Session Storage)
会话存储与本地存储类似,但数据仅在当前会话期间有效,关闭浏览器窗口后数据会被清除。适用于临时存储 Token,例如在用户登录后到关闭浏览器前使用。
javascript
// 存储 Token
sessionStorage.setItem('token', 'your_token_here');
// 获取 Token
const token = sessionStorage.getItem('token');
3. Cookie
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,可在浏览器下次向同一服务器再发起请求时被携带上并发送到服务器上。可以设置 Cookie 的过期时间。
javascript
// 存储 Token 到 Cookie
function setCookie(name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
// 获取 Cookie 中的 Token
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// 存储 Token
setCookie('token', 'your_token_here', 7);
// 获取 Token
const token = getCookie('token');
刷新页面时获取 Token
当页面刷新时,可以在页面加载完成后从上述存储位置获取 Token。以下是一个使用本地存储的示例:
javascript
window.addEventListener('load', () => {
const token = localStorage.getItem('token');
if (token) {
// 可以使用 Token 进行后续操作,如发送请求
console.log('获取到的 Token:', token);
} else {
console.log('未找到 Token');
}
});
综上所述,Token 可以存放在本地存储、会话存储或 Cookie 中,刷新页面时可以在页面加载完成的事件中从相应的存储位置获取 Token。选择哪种存储方式取决于 Token 的使用场景和安全性要求。
1. 如何区分项目中不同的环境
1. 使用环境变量
在项目构建时,可以借助环境变量来区分不同环境。以 Vue项目为例,通常会使用 .env 文件来环境变量。
定义环境变量文件
- 开发:创建
.env.development文件,内容如下:
plaintext
NODE_ENV=development
VUE_APP_API_BASE_URL=http://dev-api.example.com
- 生产环境:创建
.env.production文件,内容如下:
plaintext
NODE_ENV=production
VUE_APP_API_BASE_URL=http://prod-api.example.com
在代码里使用环境变量
javascript
// 在 Vue 组件中使用
export default {
created() {
console.log(process.env.NODE_ENV); // 输出当前环境
console.log(process.env.VUE_API_BASE_URL); // 输出对应环境的 API 地址
}
}
2. 配置文件
可以创建不同的配置文件,在不同环境下加载不同的配置。
创建配置文件
- 开发环境配置:
config.dev.js
javascript
module.exports = {
apiBaseUrl: 'http://dev-api.example.com',
debug: true
};
- 生产环境配置:
config.prod.js
javascript
module = {
apiBaseUrl: 'http://prod-api.example.com',
debug: false
};
在代码里加载配置文件
javascript
let config;
if (process.env.NODE_ENV === 'development') {
config = require('./config.dev.js');
} else {
config = require('./config.prod.js');
}
console.log(config.apiBaseUrl);
3. 构建脚本
在构建脚本里指定不同的环境参数。以 Webpack 为例:
配置 Webpack 构建脚本
javascript
const webpack = require('webpack');
const mode = process.env.NODE_ENV || 'development';
module.exports = {
mode: mode,
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(mode),
'process.env.API_BASE_URL': JSON.stringify(mode === 'development' ? 'http://dev-api.example.com' : 'http://prod-api.example.com')
})
]
};
在代码里使用
javascript
console.log(process.envODE_ENV);
console.log(process.env.API_BASE_URL);
4. 服务器端配置
如果项目有端,可以在服务器端配置不同的环境信息,然后通过接口传递给前端。
服务器端配置
javascript
// 假设使用 Node.js 和 Express
const express = require('express');
const app = express();
app.get('/config', (req, res) => {
const env = process.env.NODE_ENV || 'development';
let config;
if (env === 'development') {
config = {
apiBaseUrl: 'http://dev-api.example.com'
};
} {
config = {
apiBaseUrl: 'http://prod-api.example.com'
};
}
res.json(config);
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
前端获取配置
javascript
fetch('/config')
.then(response => response.json())
.then(config => {
console.log(config.apiBaseUrl);
});
以上这些方法都能有效地帮助你区分项目中的不同环境,你可以依据项目的具体需求和架构来选择合适的方法。
301 状态码
-
含义:301 状态码表示永久重定向(Moved Permanently)。当客户端向服务器请求时,服务器返回 301 状态码,意味着请求的资源已经永久地移动到了新的 URL 地址。浏览器或其他客户端在接收到 301 响应后,通常会记住这个重定向信息,下次再请求相同资源时,会直接访问新的 URL。
-
使用场景:常用于网站域名变更、页面永久迁移等情况。例如,网站从
http://old.example.com迁移到http://new.example.com,当用户访问旧域名时,服务器可以返回 301 状态码,将用户重定向到新域名。 -
302 状态码
-
含义:302 状态码表示临时重定向(Found)。与 301 不同,302 表示请求的资源只是临时移动到了新的 URL 地址。客户端在接收到 302 响应后,下次请求相同时,仍然会访问原来的 URL。
-
使用场景:常用于临时的页面跳转,比如在用户登录后将其重定向到特定页面,或者在进行 A/B 测试时临时重定向用户。
-
401 状态码
-
含义:401 状态码表示未授权(Unauthorized)。当客户端发送请求时,服务器返回 401 状态码,意味着客户端需要进行身份才能访问请求的资源。通常,服务器会在响应头中包含
WWW-Authenticate字段,指示客户端需要使用何种方式进行身份验证。 -
使用场景:常用于需要用户登录或授权才能访问的资源。例如,用户访问一个需要登录才能查看的页面,而用户当前未登录,服务器会返回 401 状态码。
1. 公共的函数或方法是如何发到 npm 包
1. 注册 npm 账号
如果你还没有 npm 账号,需要先到 npm 官网 注册一个账号。
2. 初始化项目
在本地创建一个新的项目目录,并在该目录下初始化一个 package.json 文件。可以使用以下命令:
bash
mkdir my-npm-package
cd my-npm-package
npm init -y
npm init - 命令会使用默认配置快速 package.json 文件,你也可以使用 npm init 命令,然后按照提示一步步填写项目信息。
3. 编写公共函数或方法
在项目目录下创建一个 JavaScript 文件(例如 index.js),并编写你的公共函数或方法。示例代码如下:
javascript
// index.js
function add(a, b) {
return a + b;
}
module.exports = {
add
};
4. 配置 package.json 文件
打开 package.json 文件,确保以下几个关键字段配置正确:
name:npm 包的名称,必须是唯一的。version:包的版本号,遵循语义化版本规范(SemVer)。main:指定包的入口文件,通常是index.js。
示例 package.json 文件如下:
json
{
"name": "my-npm-package",
"version": "1.0.0",
"description": "A simple npm package with a public function",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [
"add",
"function"
],
"author": "Your Name",
"license": "MIT"
}
5. 登录 npm
在命令中使用以下命令登录到你的 npm 账号:
bash
npm login
按照提示输入你的用户名、密码和邮箱,登录成功后会显示 Logged in as <your_username> on https://registry.npmjs.org/.。
6. 发布 npm 包
在项目目录下,使用以下命令发布 npm 包:
bash
npm publish
如果发布成功,会显示类似 + my-npm-package@1.0.0 的信息。
7. 更新 npm 包当你对公共函数或方法进行了修改,需要更新 npm 包时,首先要更新 package.json 文件中的 version 字段,然后再次使用 publish 命令发布更新后的包。例如:
bash
# 更新版本号
npm version patch # 修复 bug,版本号从 1.0.0 变为 1.0.1
# 或者
npm version minor # 新增功能,版本号从 1.0.0 变为 1.1.0
# 或者
npm version major # 重大更新,版本号从 1.0.0 变为 2.0.0
# 发布更新后的包
npm publish
注意事项
- 包名唯一性:确保
package.json中的name字段在 npm 上是唯一的,否则发布时会失败。 - 许可证:在
package.json中指定合适的许可证,常见的有 MIT、Apache 2.0 等。 - 测试:在发布之前,最好编写一些测试用例来确保公共函数或方法的正确性。可以使用 Jest、Mocha 等测试框架。
1. 需求:写一个函数,入参数是一个数组,你需要返回一个乱序的数组?说两种,为什么不推荐从前往后,而是采用从后往前
以下为你介绍两种将数组乱序的方法,并解释为何推荐从后往前而不是从前往后的顺序:
方法一:Fisher-Yates 洗牌算法(从后往前
Fisher-Yates 洗牌算法一种经典的随机排序算法,它的时间复杂度为 。该算法从数组的最后一个元素开始,依次向前遍历,每次随机选择一个前面的元素与当前元素交换位置。
javascript
function shuffleArray(arr) {
let len = arr.length;
while (len > 0) {
// 生成一个 0 到 len - 1 之间的随机索引
let randomIndex = Math.floor(Math.random() * len);
len--;
// 交换当前元素和随机选择的元素
let temp = arr[len];
arr[len] = arr[randomIndex];
arr[randomIndex] = temp;
}
return arr;
}
// 示例用法
let array = [1, 2, 3, 4, 5];
let shuffledArray = shuffleArray(array);
console.log(shuffledArray);
方法二:从前往后随机交换元素
这种方法从数组的第一个元素开始,依次向后遍历,每次随机选择一个后面的元素与当前元素交换位置。
javascript
function shuffleArrayForward(arr) {
let len = arr.length;
for (let i = 0; i < len; i++) {
// 生成一个 i 到 len - 1 之间的随机索引
let randomIndex = Math.floor(Math.random() * (len - i)) + i;
// 交换当前元素和随机选择的元素
let temp = arr[i];
arr[i] = arr[random];
arr[randomIndex] = temp;
}
return arr;
}
// 示例用法
let array2 = [1, 2, 3, 4, 5];
let shuffledArray2 = shuffleArrayForward(array2);
console.log(shuffledArray2);
为什么不推荐从前往后,而是采用从后往前
从理论上来说Fisher-Yates 算法(从后往前)是一种更优的随机排序算法,原因如下:
- 随机性更均匀:从后往前的 Fisher-Yates 算法能够保证每个元素在每个位置出现的概率是相等的,即每个排列的概率都是 ,这是一种真正的随机排列。而从前往后的方法虽然也能实现乱序,但在某些情况下,元素分布的随机性可能不如从后往前的方法均匀。
- 代码简洁性:从后往前的 Fisher-Yates 算法代码实现相对简洁,逻辑也更清晰,只需要一个循环就可以完成乱序操作。
综上所述,在实际应用中,推荐使用 Fisher-Yates 洗牌算法(从后往前)来实现数组的乱序。
1. 生成索引不重复,如何实现的
在编程中实现生成不重复的索引可以有多种方式,下面为介绍几种常见的实现方法,这里以 JavaScript 为例:
方法一:使用 Set 数据结构
Set 是 ES6 引入的一种新的数据结构,它类似于数组,但成员的值都是唯一的,没有重复的值。可以利用这个特性来生成不重复的索引。
javascript
function generateUniqueIndices(length, count) {
const indices = new Set();
while (indices.size < count) {
const index = Math.floor(Math.random() * length);
indices.add(index);
}
return Array.from(indices);
}
// 示例使用
const arrayLength = 0;
const numIndices = 5;
const uniqueIndices = generateUniqueIndices(arrayLength, numIndices);
console.log(uniqueIndices);
方法二:洗牌算法
洗牌算法可以将一个数组的元素随机打乱,取前 n 个元素作为不重复的索引。
javascript
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function generateUniqueIndicesByShuffle(length, count) {
const allIndices = Array.from({ length }, (_, i) => i);
const shuffledIndices = shuffleArray(allIndices);
return shuffledIndices.slice(0, count);
}
// 示例使用
const arrayLength2 = 10;
const num2 = 5;
const uniqueIndices2 = generateUniqueIndicesByShuffle(arrayLength2, numIndices2);
console.log(uniqueIndices2);
代码解释
- Set 方法:通过不断生成随机索引并添加到 Set 中,由于 Set 的特性,重复索引不会被添加,直到 Set 的大小达到所需的索引数量,最后将 Set 为数组返回。
- 洗牌算法方法:首先创建一个包含所有可能索引的数组,然后使用洗牌算法将数组元素随机打乱,最后取前
n个元素作为不重复的索引。
这两种方法都能有效地生成不重复的索引,你可以根据具体的需求选择的方法。
1. 如何实现垂直居中?translate 与 margin 来偏移又何不同
实现居中的方法
1. 使用 Flexbox
Flexbox 是一种现代且强大的布局模型,非常适合实现垂直和水平居中。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.parent {
display: flex;
justify-content: center
align-items center;
height: 300px;
background-color: #f0f0f0;
}
.child {
background-color:ccc;
padding: 20px;
}
</style>
</head>
<body>
<div class="parent">
<div class="child">垂直居中的内容</div>
</div>
</body>
</html>
在上述中,父元素设置了 display: flex,并使用 -content: center 和 align-items: center 分别实现水平和垂直居中。
2. 使用绝对定位和 transform
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.parent {
position: relative;
height: 300px;
background-color: #f0f0f0;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #;
padding: 20px;
}
</style>
</head>
<body>
<div class="parent">
<div class="child">垂直居中的内容</div>
</div>
</body>
</html>
这里父元素设置为相对定位,子元素设置为绝对定位,通过 top:50% 和 left: 50% 将子元素的左上角到父元素的中心,再使用 transform: translate(-50%, -50%) 将子元素向上和向左移动自身宽度和高度的一半,从而实现垂直和水平居中。
3. 使用 Grid
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
.parent {
display: grid;
place-items: center;
height: 300px;
background-color: #f0f0f0;
}
.child {
background-color: #ccc;
padding: 20px;
}
</style>
</head>
<body>
<div class="parent">
<div class="child">垂直居中的内容</div>
</div>
</body>
</html>
使用 display: grid 和 place-items: center 可以简洁地实现子元素在父元素中的垂直和水平居中。
translate 与 margin 偏移的不同
1. 对文档流的影响
- **
margin:使用margin进行偏移会影响元素在文档流中的位置,它会改变元素周围其他元素的布局。例如,给一个元素设置margin-top: 20px,会将该元素向下移动 20px,同时下面的元素也会跟着向下移动。 translate:translate是通过 CSS3 的transform属性实现的,它不会影响元素在文档流中的。元素虽然在视觉上发生了偏移,但它原本占据的空间仍然保留,不会影响其他元素的布局。
2. 百分比值的计算方式
margin:当margin使用百分比值时,其计算是相对于父元素的宽度。例如,margin-top: 10%会根据父元素的宽度来计算偏移量。translate:translate的百分比值是相对于元素自身的宽度和高度。例如,transform: translate(50%, 50%)会将元素向右和向下移动自身宽度和高度的 50%。
3. 性能表现
margin:改变margin值会触发浏览器的重排(reflow)和重绘(repaint),因为它会影响元素的布局和位置,性能开销相对较大。translate:transform属性的改变只会触发浏览器的合成(composite)阶段,不会触发重排和重绘,性能表现更好,尤其是在动画效果中。
如何实现加载中?
在前端开发中,实现“加载中”效果可以有多种方式,下面为你详细介绍几种常见的实现方法:
1. 使用 CSS 动画实现简单的加载中效果
通过 CSS 的 @keyframes 规则创建动画,再应用到一个元素上,实现加载动画。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* 定义加载动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 加载中元素样式 */
.loading {
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
margin: 50px auto;
}
</style>
</head>
<body>
<div class="loading"></div>
</body>
</html>
在上述代码中,@keyframes spin 定义了一个旋转动画,从 0 度旋转到 360 度。.loading 类应用了这个动画,创建了一个旋转的加载中效果。
2. 使用 JavaScript 控制加载状态
结合 HTML、CSS 和 JavaScript,根据数据加载状态显示或隐藏加载中提示。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/* 加载中元素样式 */
.loading {
display: none;
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
margin: 50px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="loading"></div>
<button id="loadData">加载数据</button>
<script>
const loadingElement = document.querySelector('.loading');
const loadButton = document.getElementById('loadData');
loadButton.addEventListener('click', () => {
// 显示加载中提示
loadingElement.style.display = 'block';
// 模拟数据加载
setTimeout(() => {
// 隐藏加载中提示
loadingElement.style.display = 'none';
}, 3000);
});
</script>
</body>
</html>
此代码中,点击按钮触发数据加载,显示加载中提示,使用 setTimeout 模拟数据加载过程,加载完成后隐藏加载中提示。
3. 在 Vue 项目中实现加载中效果
在 Vue 项目里,可以使用组件和数据绑定来实现加载中效果。
vue
<template>
<div>
<button @click="loadData">加载数据</button>
<div v-if="isLoading" class="loading">
<div class="spinner"></div>
</div>
<div v-else>数据加载完成</div>
</div>
</template>
<script>
export default {
data() {
return {
isLoading: false
};
},
methods: {
loadData() {
this.isLoading = true;
// 模拟数据加载
setTimeout(() => {
this.isLoading = false;
}, 3000);
}
}
};
</script>
<style scoped>
.loading {
border: 16px solid #f3f3f3;
border-top: 16px solid #3498db;
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
margin: 50px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
在这个 Vue 组件中,通过 isLoading 数据属性控制加载中提示的显示与隐藏,点击按钮触发数据加载,模拟加载完成后更新 isLoading 的值。
1. 说说继承,有几种继承
1. 原型链继承
原型链继承的核心是让子类的原型指向父类的实例,这样子类实例就可以访问父类的属性和方法。
javascript
// 父类构造函数
function Parent() {
this.name = 'parent';
}
// 父类方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child() {}
// 子类的原型指向父类的实例
Child.prototype = new Parent();
// 创建子类实例
const child = new Child();
child.sayName(); // 输出: parent
缺点:
- 多个子类实例会共享父类的引用类型属性。
- 创建子类实例时,无法向父类构造函数传参。
2. 构造函数继承
构造函数继承是在子类构造函数中调用父类构造函数,通过 call、apply 或 bind 方法改变 this 的指向。
javascript
// 父类构造函数
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', ''];
}
// 子类构造函数
function Child(name) {
Parent.call(this, name);
}
// 创建子类实例
const child1 = new Child('child1');
const child2 = new Child('child2');
child1.colors.push('yellow');
console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'yellow']
console.log(child2.colors); // 输出: ['red', 'blue', 'green']
****:
- 可以向父类构造函数传参。
- 每个子类实例都有自己的一份父类属性副本,不会共享。
缺点:
- 只能继承父类构造函数中的属性和方法,无法继承父类原型上的属性和方法。
3. 组合继承
组合继承结合了原型链继承和构造函数继承的优点,既可以继承父类构造函数中的属性和方法,也可以继承父类原型上的属性和方法。
javascript
//父类构造函数
function Parent) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类原型方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name); // 构造函数继承
this.age = age;
}
// 子类的原型指向父类的实例
Child.prototype = new Parent();
Child.prototype.constructor = Child;
// 创建子类实例
const child = new Child('child', 18);
child.sayName(); // 输出: child
缺点:
- 父类构造函数会被调用两次,一次是在子类构造函数中,一次是在子类原型赋值时。
4. 寄生组合继承
寄生组合继承是对组合继承的优化,避免了父类构造函数被调用两次的问题。
javascript
// 父类构造函数
function Parent(name) {
this.name = name;
this.col = ['red', 'blue', ''];
}
// 父类原型方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name); // 构造函数继承
this.age = age;
}
// 寄生组合继承核心
function inheritPrototype(Child, Parent) {
const prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype;
}
// 实现继承
inheritPrototype(Child, Parent);
// 创建子类实例
const child = new Child('child', 18);
child.sayName(); // 输出: child
5. 类继承(ES6)
ES6 引入了 class 关键字和 extends 关键字,使得继承的语法更加简洁。
javascript
// 父类
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
// 创建子类实例
const child = new Child('child', 18);
child.sayName(); // 输出: child
这种方式是现代 JavaScript 中推荐的继承方式,语法简洁,且避免了传统继承方式的一些问题。
class 、extend继承相对于那种继承?调用 super 的原因是什么?
class 和 extends 继承相对于哪种继承
class 和 extends 是 ES6 引入的语法糖,用于实现类的继承,它本质上是基于寄生组合继承进行封装的,相对于传统的继承方式(如原型链继承、构造函数继承、组合继承等),class 和 extends 提供了更简洁、更直观的语法,使得代码的可读性和可维护性大大提高。
传统的寄生组合继承代码可能会比较复杂,需要手动处理原型链和构造函数的调用,而使用 class 和 extends 可以用更简洁的实现相同的功能。例如,下面是寄生组合继承和 class、extends 继承的对比:
寄生组合继承示例
javascript
// 父类构造函数
function Parent(name) {
this.name = name;
}
// 父类原型方法
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function Child(name, age) {
Parent.call(this, name); // 构造函数继承
this.age = age;
}
// 寄生组合继承核心
function inheritPrototype(Child, Parent) {
const prototype = Object.create(Parent.prototype);
prototype.constructor = Child;
Child.prototype = prototype
}
// 实现继承
inheritPrototype(Child, Parent);
// 创建子类实例
const child = new Child('child', 18);
child.sayName();
class 和 extends 继承示例
javascript
// 父类
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
// 子类
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类构造函数
this.age = age;
}
}
// 创建子类实例
const child = new Child('child', 18);
child.sayName();
调用 super 的原因
在使用 class 和 extends 实现继承时,在子类的构造函数中调用 super 方法是非常重要的,原因如下:
- 调用父类构造函数:
super用于调用父类的构造函数,确保子类实例能够继承父类的属性。如果子类定义了自己的构造函数,并且父类也有构造函数,那么在子类构造函数中必须先调用super,否则会报错。例如:
javascript
class Parent {
constructor(name) {
this.name = name;
}
}
class Child extends Parent {
constructor(name, age) {
// 如果不调用 super,会报错
super(name);
this.age = age;
}
}
const child = new Child('child', 18);
console.log(child.name);
console.log(child.age);
- 绑定
this:在调用super时,它会将父类构造函数中的this绑定到子类实例上,使得子类实例能够正确地继承父类的属性和方法。如果不调用super,子类实例的this就无法正确初始化,可能会导致后续操作出错。
总之,super 是在子类构造函数中调用父类构造函数的关键,它保证了子类能够正确地继承父类的属性和方法。
1. 说说事件循环
事件循环( Loop)是 JavaScript 的执行机制,它负责处理异步操作,确保 JavaScript 能够在单线程环境下实现非阻塞的 I/O 操作,从而提高程序的性能和响应能力。以下是关于事件循环的详细介绍:
基本概念
JavaScript 是单线程的,这意味着它一次只能执行一个任务。为了处理异步操作(如定时器、网络请求等),JavaScript 引入了事件循环机制。事件循环主要涉及以下几个概念:
-
调用栈(Call Stack) :也称为执行栈,是一个后进先出(LIFO)的数据结构,用于存储正在执行的函数调用。当调用一个函数时,会将该函数的执行上下文压入调用栈;当函数执行完毕后,会将其执行上下文从调用栈中弹出。
-
任务队列(Task Queue) :也称为消息队列,是一个先进先出(FIFO)的数据结构,用于存储异步任务的回调函数。任务队列分为宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue)。
- 宏任务(Macro Task) :常见的宏任务包括
setTimeout、setInterval、setImmediate(Node.js 环境)、I/O操作、UI rendering等。 - 微任务(Micro Task) :常见的微任务包括
Promise.then、MutationObserver、process.nextTick(Node.js 环境)等。
- 宏任务(Macro Task) :常见的宏任务包括
-
事件循环(Event Loop) :不断地从任务队列中取出任务并将其放入调用栈中执行。事件循环的主要步骤如下:
- 检查调用栈是否为空,如果为空,则从微任务队列中取出所有任务并依次执行,直到微任务队列为空。
- 执行完所有微任务后,从宏任务队列中取出一个任务并放入调用栈中执行。
- 重复上述步骤,不断循环。
示例代码以下是一个简单的示例代码,用于演示事件循环的工作原理:
javascript
console.log('script start');
// 宏任务
setTimeout(() => {
console.log('setTimeout');
}, 0);
// 微任务
Promise.resolve().then(() => {
console.log('Promise then');
});
console.log('script end');
代码执行过程分析
- 首先,将
console.log('script start')压入调用栈并执行,输出script start。 - 遇到
setTimeout,将其回调函数放入宏任务队列中。 - 遇到
Promise.resolve().then,将其回调函数放入微任务队列中。 - 执行
console.log('script end'),输出script end。 - 此时调用栈为空,开始处理微任务队列,执行
Promise then回调函数,输出Promise then。 - 微任务队列处理完毕后,从宏任务队列中取出
setTimeout回调函数并执行,输出setTimeout。
总结
事件循环是 JavaScript 处理异步操作的核心机制,通过调用栈、任务队列和事件循环的协同工作,实现了单线程环境下的异步编程。理解事件循环的工作原理对于编写高效、稳定的 JavaScript 代码至关重要。
1. 说说 Promise 与 await 的区别
语法形式
- Promise:
Promise是一个构造函数,通过new Promise()来创建一个Promise实例,它接收一个执行器函数,该函数有两个参数resolve和reject,分别用于将Promise状态变为成功和失败。使用.then()方法处理成功的结果,使用.catch()方法处理失败的结果。
javascript
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success');
}, 1000);
});
promise.then((result) => {
console.log(result);
}).catch((error) => {
console.error(error);
});
- await:await
是async/await语法的一部分,await只能在async函数内部使用。await后面通常跟一个Promise,它会暂停async函数的执行,直到Promise被解决(resolved)或被拒绝(rejected),然后返回Promise` 的结果。
javascript
async function asyncFunction {
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success');
}, 1000);
});
const result = await promise;
console.log(result);
}
asyncFunction();
错误处理
- Promise:使用
.catch()方法来捕获Promise链中任何一个环节错误。如果在.then()方法中没有正确处理错误,错误会被传递到下一个.catch()中。
javascript
const promise = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
promise.then((result) => {
console.log(result);
}).catch((error) => {
console.error(error);
});
- await:在
async函数中,使用try...catch块来捕获await操作可能抛出的错误。
javascript
async function asyncFunction() {
try {
const promise = new Promise((resolve, reject) => {
reject(new Error('Something went wrong'));
});
const result = await promise;
console.log(result);
} catch (error) {
console.error(error);
}
}
asyncFunction();
代码可读性和编写方式
- Promise:当处理多个异步操作时,
Promise链可能会导致代码嵌套过深,形成所谓的“回调地狱”,影响代码的可读性和维护性。
javascript
function asyncOperation1() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Result 1');
}, 1000);
});
}
function asyncOperation2() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Result 2');
}, 1000);
});
}
asyncOperation1().then((result1) => {
console.log(result1);
returnOperation2();
}).then((result2) => {
console.log(result2);
});
- await:
async/await语法使异步代码看起来更像同步代码,避免了回调地狱,提高了代码的可读性和可维护性。
javascript
async function asyncFunction() {
const result1 = await asyncOperation1();
console.log(result1);
const result2 = await asyncOperation2();
console.log(result2);
}
asyncFunction();
执行顺序
- Promise:
Promise的.then()和.catch()方法是异步执行的,它们会被放入微任务队列中,在当前调用栈执行完毕后才会执行。 - await:
await会暂停async函数的执行,直到Promise被解决,这使得代码的执行顺序更符合同步代码的逻辑。
综上所述,Promise 是基础的异步处理机制,而 async/await 是基于 Promise 的语法糖,提供了更简洁、更易读的异步代码编写方式。
1. 箭头函数和普通函数的区别
1. 语法形式
- 普通函数:使用
function关键字来定义,可以有函数名,也可以是匿名函数。
javascript
// 具名函数
function add(a, b) {
return a + b;
}
// 匿名函数
const subtract = function(a, b) {
return a - b;
};
- 箭头函数:使用箭头
=>来定义,语法更为简洁。当只有一个参数时,可以省略括号;当函数体只有一条语句时,可以省略花括号和return关键字。
javascript
// 单个参数
const square = x => x * x;
// 多个参数
const multiply = (a, b) => a * b;
// 函数体多条语句
const complex = (a, b) => {
const sum = a + b;
return sum * 2;
};
2. this 指向
- 普通函数:
this的指向在函数调用时动态确定,取决于函数的调用方式。常见的调用方式有全局调用、方法调用、构造函数调用和call/apply/bind调用等,不同调用方式下this指向不同。
javascript
const obj = {
name: 'John',
sayName: function() {
console.log(this.name);
}
};
obj.sayName(); // 方法调用,this 指向 obj,输出 'John'
const sayName = obj.sayName;
sayName(); // 全局调用,this 指向全局对象(在浏览器中是 window),输出 undefined
- 箭头函数:
this的指向取决于定义时的上下文,而不是调用时的上下文。它会捕获其所在上下文的this值,并且不会改变。
javascript
const obj = {
name: 'John',
sayName: () => {
console.log(this.name);
}
};
obj.sayName(); // 这里的 this 指向全局对象(在浏览器中是 window),输出 undefined
3. arguments 对象
- 普通函数:函数内部有一个
arguments对象,它是一个类数组对象,包含了函数调用时传递的所有参数。
javascript
function showArgs() {
console.log(arguments);
}
showArgs(1, 2, 3); // 输出 Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
- 箭头函数:没有自己的
arguments对象。如果需要访问参数,可以使用剩余参数语法。
javascript
const showArgs = (...args) => {
console.log(args);
};
showArgs(1, 2, 3); // 输出 [1, 2, 3]
4. 使用 new 关键字
- 普通函数:可以使用
new关键字作为构造函数来创建对象实例。
javascript
function Person(name) {
this.name = name;
}
const person = new Person('John');
console.log(person.name); // 输出 'John'
- 箭头函数:不能使用
new关键字,因为它没有自己的this和prototype,不是构造函数。
javascript
const Person = (name) => {
this.name = name;
};
try {
const person = new Person('John');
} catch (error) {
console.error(error); // 抛出错误:Person is not a constructor
}
5. yield 关键字
- 普通函数:可以使用
yield关键字将函数定义为生成器函数。
javascript
function* generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
console.log(gen.next().value); // 输出 1
- 箭头函数:不能使用
yield关键字,因此不能定义为生成器函数。
分析输出
async function fun1(){
await fun2()
console.log('async1')
}
async function fun2(){
console.log('async2')
}
fun1()
setTimeout(()=>{
console.log('setTimeout')
},0)
new Promise((resolve)=>{
console.log('promise1')
resolve()
}).then(()=>{
console.log('promise2')
}).then(()=>{
console.log('promise3')
})
console.log('script end')
要分析这段代码的输出,需要了解 JavaScript 中的事件循环,特别是异步操作(如 async/await``Promise 和 setTimeout)的执行顺序。以下是对代码执行过程的详细分析:
代码执行步骤
-
同步代码执行:
- 首先执行
console.log('script start'),输出script start。 - 定义
async函数fun1和fun2,函数定义不会立即执行。 - 调用
fun1(),进入fun1函数内部。 - 在
fun1函数中,遇到await fun2(),调用fun2函数。 - 执行
fun2函数中的console.log('async2'),输出async2。 - 由于
await会暂停fun1函数的执行,将fun1函数剩余的代码(console.log('async1'))放入微任务队列。 - 执行
setTimeout,将其回调函数放入宏任务队列。 - 执行
new Promise,Promise构造函数中的代码是同步执行的,所以执行console.log('promise1'),输出promise1。 - 调用
resolve(),将Promise的then回调函数放入微任务队列。 - 执行
console.log('script end'),输出script end。
- 首先执行
-
微任务队列执行:
- 同步代码执行完毕后,开始执行微任务队列中的任务。
- 首先执行
fun1函数中剩余的console.log('async1'),输出async1`。 - 然后执行
Promise的第一个then回调函数console.log('promise2'),输出promise2。 - 接着执行
Promise的第二个then回调函数console.log('promise3'),输出promise3。
-
宏任务队列执行:
- 微任务队列执行完毕后,开始执行宏任务队列中的任务。
- 执行
setTimeout的回调函数console.log('setTimeout'),输出setTimeout。
输出结果
null
script start
async2
promise1
script end
async1
promise2
promise3
setTimeout
综上所述,代码的输出顺序是由 JavaScript 的事件循环机制决定的,先执行同步代码,再执行微任务队列中的任务,最后执行宏任务队列中的任务。