# JS面试题
# 事件循环
JS是单线程,防止代码阻塞,我们把代码(任务):同步和异步
同步代码给js引擎执行,异步代码交给宿主环境
同步代码放入执行栈中,异步代码等待时机成熟送入任务队列排队
执行栈执行完毕,会去任务队列(宏任务队列或微任务队列)看是否有异步任务,有就送到执行栈执行,反复循环查看执行,这个过程是事件循环(eventloop)
*注意:微任务由JS引擎发起,宏任务由宿主环境发起(浏览器或者node环境)
# ==和===的区别
***==***:等于操作符
两个都为简单类型,字符串和布尔值都会转换成数值,再比较
简单类型与引用类型比较,对象转换为原始类型的值,再比较
两个都为引用类型,则比较它们是否指向同一个对象
null和undefined相等
存在NaN则返回false
/** * ==:如果操作数相等,则返回true,并且在比较过程中,==会先进行类型转换再确定操作数是否相等 * */ // 布尔值为转换为数值 console.log('==============布尔值->会转换为数值==============') const result1 = (true == 1); console.log(result1); // 输出 true const result2 = (false == 0) console.log(result2); // 输出 true // 字符串会转换为数值 console.log('==============字符串->转换为数值==============') const result3 = ('1' == 1) console.log(result3); // 输出 true const result4 = ('0' == 0) console.log(result4); // 输出 true // 如果一个操作数是对象,另一个操作数是字符串、数值或符号,则对象会转换为原始值,然后再进行比较 console.log('==============对象->转换为原始值==============') const obj1 = { name: 'obj1-name', valueOf: function () { return '1'; }, toString: function () { return '1'; }, } console.log(obj1 == 1) // 货币对象,可以与数字直接比较 class Money { constructor(amount, currency = 'USD') { this.amount = amount; this.currency = currency; } valueOf() { return this.amount; // 返回数值用于比较和计算 } toString() { return `${this.amount} ${this.currency}`; } } const price = new Money(100, 'USD'); console.log(price == 100); // true,可以直接与数字比较 console.log(price > 50); // true,可以参与数值比较 // null和undefined只在与自身比较时相等 console.log('==============null和undefined==============') console.log(null == null); // true console.log(undefined == undefined); // true console.log(null == undefined); // true console.log(null === undefined); // false console.log(null == 0); // false console.log(undefined == 0); // false console.log(null == null == undefined); // false (null == null) -> true; true == undefined -> false // 如果有任意操作数为NaN,则比较总是返回false console.log('==============NaN==============') console.log(NaN == NaN); // false console.log(NaN === NaN); // false // 两边都是对象,只有引用同一个对象时才相等 console.log('==============对象引用==============') let obj2 = { name: "xxx" } let obj3 = { name: "xxx" } let obj4 = obj2; console.log(obj2 == obj3); // 输出 false, 因为它们引用不同的对象 console.log(obj2 == obj4);===:全等操作符,只有两个操作数再不转换的前提下相等才返回true,即类型相同,值也需相同
undefined和null与自身严格相等
全等运算符不会做类型转换
console.log(null === undefined); // false console.log(null === null) // true console.log(undefined === undefined) // true console.log(1 === 1) // true console.log('1' === 1) // false
# 数据结构
数组,栈,队列,链表,字典
数组
数组是按顺序存储多个数据的集合,它允许通过索引(下标)快速访问、增删元素,是处理列表类数据的核心数据结构 。
核心特性:
- 索引访问:通过数字索引(从 0 开始)直接定位元素,如
arr[0]能快速获取第一个元素,这是其与对象(键值对访问)的核心区别 。 - 动态长度:数组长度可自动调整,执行
push()(末尾添加)、pop()(末尾删除)等操作时,length属性会实时更新 。 - 多数据类型:可同时存储不同类型数据,如
[1, '前端', true, {}],但实际开发中建议存储同类型数据,便于遍历和处理 。 - 丰富 API:内置大量实用方法,如
map()遍历转换数据、filter()筛选元素、reduce()汇总计算等,是前端数据处理的 “瑞士军刀” 。
- 索引访问:通过数字索引(从 0 开始)直接定位元素,如
栈
遵循 “先进后出(LIFO)” 规则的线性数据结构,仅允许在一端(栈顶)进行元素的插入(入栈)和删除(出栈)操作,如同叠放的盘子,只能从最顶端取放 。
核心特性:
- 操作限制:仅栈顶可操作,底部元素需等上方元素全部移除才能访问。例如调用栈中,最内层函数执行完(出栈),外层函数才能继续执行 。
- 前端高频应用场景
- 函数调用栈:JS 引擎执行代码时,每次调用函数会将其压入栈顶,函数执行完毕后从栈顶弹出。如递归函数调用,若递归无终止条件,会导致栈溢出(Stack Overflow) 。
- 浏览器历史记录:浏览器的 “前进 / 后退” 功能基于栈实现,访问新页面时入栈,点击 “后退” 时将当前页面出栈,恢复上一页面(栈顶元素) 。
- 编辑器撤销 / 重做:编辑文档时,每一步操作入栈,“撤销” 即弹出最近操作(恢复上一状态),部分场景下 “重做” 可借助辅助栈实现 。
栈实例:
class MinStack { _stack = []; _minStack = []; push(val){ this._stack.push(val) if(this._minStack.length === 0 || val <= this._minStack[this._minStack.length - 1]){ this._minStack.push(val) } } pop(){ const val = this._stack.pop() if(val === this._minStack[this._minStack.length - 1]){ this._minStack.pop() } return val; } top(){ return this._stack[this._stack.length - 1] } // 获取栈中的最小值 getMin(){ return this._minStack[this._minStack.length - 1] } } const minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(5); console.log(minStack.top()); // 输出 5 console.log(minStack._stack) console.log(minStack.getMin()); // 输出 -2
# script标签的位置以及加载顺序
默认行为
- 阻塞性加载:浏览器解析HTML时遇到
<script>标签会暂停HTML解析,下载并执行脚本,完成后才继续解析HTML - 顺序执行:多个
<script>标签按它们在HTML中出现的顺序依次执行
放置位置
<head>内
<head>
<script src="script1.js"></script>
<script src="script2.js"></script>
</head>
- 优点:尽早加载脚本
- 缺点:会阻塞页面渲染,可能导致白屏时间延长
<body>底部(推荐)
<body>
<!-- 页面内容 -->
<script src="script1.js"></script>
<script src="script2.js"></script>
</body>
- 优点:页面内容先渲染,用户体验更好
- 缺点:脚本执行延迟,可能影响依赖脚本的功能
控制加载行为的属性
async属性
<script src="script.js" async></script>
- 异步下载,不阻塞HTML解析
- 下载完成后立即执行(可能在HTML解析完成前)
- 执行顺序不确定(先下载完的先执行)
defer属性
<script src="script.js" defer></script>
- 异步下载,不阻塞HTML解析
- 等到HTML解析完成后,按顺序执行所有
defer脚本 - 执行顺序与在文档中的位置一致
type="module"
<script type="module" src="module.js"></script>
- 默认具有
defer行为 - 可以添加
async属性改变行为
动态加载
const script = document.createElement('script');
script.src = 'dynamic.js';
document.head.appendChild(script);
- 完全异步加载和执行
- 不阻塞页面渲染
最佳实践建议
- 关键脚本放在
<head>中并使用defer - 非关键脚本放在
<body>底部 - 完全独立的脚本(如分析代码)可使用
async - 现代开发推荐使用模块化(
type="module")和打包工具
# script标签中的module有什么意义
模块化代码组织
模块内部的变量、函数、类默认具有 块级作用域,不会污染全局作用域。只有通过
export显式导出的内容,才能被其他模块通过import引入使用,实现了代码的封装和隔离。<!-- 模块文件 module.js --> <script type="module"> const message = "Hello Module"; export function logMessage() { console.log(message); } </script> <!-- 引入模块 --> <script type="module"> import { logMessage } from './module.js'; logMessage(); // 输出 "Hello Module" console.log(message); // 报错:message 未定义(模块内私有) </script>支持
import/export语法允许使用
import加载其他模块的导出内容,或通过export暴露当前模块的接口,实现模块间的依赖管理和代码复用,这是模块化开发的核心能力。自动采用严格模式
模块内默认启用严格模式(
use strict),对变量声明、函数作用域等有更严格的语法限制,减少代码错误(例如禁止未声明的变量赋值)。延迟执行(类似
defer)模块脚本会 异步加载,不会阻塞 HTML 解析,且会按照在文档中出现的顺序执行(与
defer特性类似),但执行时机晚于普通脚本。跨域限制
模块脚本的加载受同源策略限制,若从其他域名加载模块,服务器需配置 CORS 响应头,否则会加载失败(普通脚本无此限制)。
顶层
this为undefined模块顶层的
this指向undefined,而普通脚本中this指向全局对象(如浏览器中的window),进一步强化了模块的独立性。
在module中访问this
console.log(globalThis)
# foreEach和map的区别
forEach和 map都是 JavaScript 中用于遍历数组的方法,但它们有几个关键区别:
主要区别
| 特性 | forEach | map |
|---|---|---|
| 返回值 | undefined | 新数组(由回调函数的返回值组成) |
| 链式调用 | 不支持(返回undefined) | 支持(返回新数组) |
| 使用场景 | 仅需遍历数组执行操作 | 需要基于原数组创建新数组 |
详细说明
- 返回值
forEach:总是返回undefinedconst result = [1, 2, 3].forEach(item => item * 2); console.log(result); // undefinedmap:返回一个新数组,包含回调函数每次执行的返回值const result = [1, 2, 3].map(item => item * 2); console.log(result); // [2, 4, 6]
- 是否改变原数组
两者都不会直接改变原数组,但:
forEach可以在回调中修改原数组:const arr = [1, 2, 3]; arr.forEach((item, index, array) => { array[index] = item * 2; // 直接修改原数组 }); console.log(arr); // [2, 4, 6]map的设计理念是不应该修改原数组,而是返回新数组
- 链式调用
forEach不能链式调用,因为它返回undefined// 错误示例 [1, 2, 3].forEach(...).filter(...); // 报错map可以链式调用:const result = [1, 2, 3] .map(x => x * 2) .filter(x => x > 3); console.log(result); // [4, 6]
- 性能考虑
在大多数情况下,forEach比 map稍微快一些,因为 map需要创建并返回新数组。但差异通常很小,不应作为选择的主要依据。
使用场景
使用 forEach的情况:
- 只需要遍历数组执行某些操作(如打印、修改外部变量等)
- 不需要返回新数组
- 不需要链式调用
// 示例:打印数组元素
[1, 2, 3].forEach(item => console.log(item));
// 示例:累加外部变量
let sum = 0;
[1, 2, 3].forEach(item => {
sum += item;
});
使用 map的情况:
- 需要基于原数组创建新数组
- 需要进行链式操作
- 需要保持原数组不变
// 示例:创建新数组
const doubled = [1, 2, 3].map(item => item * 2);
// 示例:链式调用
const result = users
.map(user => user.name)
.filter(name => name.startsWith('A'));
总结
选择 forEach还是 map取决于你的需求:
- 如果只需要遍历数组并执行某些操作,使用
forEach - 如果需要转换数组并得到新数组,使用
map - 在函数式编程中,通常优先使用
map,因为它更符合不可变性的原则
# indexof和includes的区别
indexOf和 includes都是 JavaScript 中用于检查数组中是否包含某个元素的方法,但它们有几个重要区别:
主要区别
| 特性 | indexOf | includes |
|---|---|---|
| 返回值 | 元素的索引(找不到返回-1) | 布尔值(true/false) |
| NaN处理 | 无法检测NaN | 可以正确检测NaN |
| 空值处理 | 可以检测undefined/null | 可以检测undefined/null |
| 可读性 | 较低(需要与-1比较) | 较高(直接返回布尔值) |
| ES版本 | ES5 | ES6 (ES2015) |
详细说明
- 返回值不同
indexOf返回元素在数组中的位置索引(从0开始),如果找不到则返回-1const arr = ['a', 'b', 'c']; console.log(arr.indexOf('b')); // 1 console.log(arr.indexOf('d')); // -1includes返回布尔值,表示数组是否包含该元素const arr = ['a', 'b', 'c']; console.log(arr.includes('b')); // true console.log(arr.includes('d')); // false
- 对NaN的处理不同
indexOf无法检测NaNconst arr = [1, NaN, 3]; console.log(arr.indexOf(NaN)); // -1 (找不到)includes可以正确检测NaNconst arr = [1, NaN, 3]; console.log(arr.includes(NaN)); // true
- 语法差异
indexOf语法:arr.indexOf(searchElement[, fromIndex])includes语法:arr.includes(searchElement[, fromIndex])
两者都接受可选的第二个参数,表示从哪个索引开始搜索。
- 使用场景对比
使用 indexOf的情况:
- 需要知道元素的具体位置
- 需要兼容老版本JavaScript环境(ES5及以下)
- 不需要检测NaN
// 查找元素位置
const index = fruits.indexOf('apple');
if (index !== -1) {
console.log(`找到苹果,位置在${index}`);
}
// 删除特定元素
const index = items.indexOf(itemToRemove);
if (index > -1) {
items.splice(index, 1);
}
使用 includes的情况:
- 只需要知道元素是否存在
- 需要检测NaN
- 代码可读性更重要
- 运行环境支持ES6+
// 检查权限
if (userPermissions.includes('admin')) {
// 执行管理员操作
}
// 验证输入
const validOptions = ['yes', 'no'];
if (!validOptions.includes(userInput)) {
console.log('无效输入');
}
性能比较
在大多数现代JavaScript引擎中,两者的性能差异可以忽略不计。但在某些情况下:
- 当元素存在时,
indexOf可能在找到元素后立即返回,稍微快一点 - 当元素不存在时,两者都需要遍历整个数组
总结
- 如果你需要元素的位置,使用
indexOf - 如果你只需要知道元素是否存在,使用
includes(更直观) - 如果你需要检测NaN,必须使用
includes - 在ES6+环境中,
includes通常是更好的选择,除非你需要兼容旧环境或需要元素位置信息
补充:字符串中的使用
这两个方法也可以用于字符串:
// 字符串中的indexOf
const str = 'hello';
console.log(str.indexOf('e')); // 1
console.log(str.indexOf('x')); // -1
// 字符串中的includes
console.log(str.includes('ell')); // true
console.log(str.includes('x')); // false
在字符串中使用时的区别与数组中的区别类似。
# uniapp的问题 打包,如何缩小打包体积
UniApp 打包体积过大会影响应用加载速度和用户体验。以下是一些有效的优化策略:
- 代码层面优化
移除无用代码和依赖
// 定期检查package.json,移除未使用的依赖
// 使用工具分析依赖关系:
npx depcheck
按需引入组件和API
// 避免整体引入
import * as utils from '@/common/utils.js'; // ❌ 不推荐
// 按需引入
import { formatDate, debounce } from '@/common/utils.js'; // ✅ 推荐
优化静态资源
<!-- 压缩图片 -->
<image src="/static/optimized-image.jpg" mode="aspectFit"></image>
<!-- 使用WebP格式(兼容性处理) -->
<picture>
<source srcset="image.webp" type="image/webp">
<image src="image.jpg" alt="fallback"></image>
</picture>
- 构建配置优化
修改 manifest.json
{
"app-plus": {
"optimization": {
"treeShaking": {
"enable": true
}
},
"compilerVersion": 3, // 使用V3编译器,体积更小
"webpack-bundle-analyzer": true // 启用分析工具
}
}
配置 vue.config.js
// vue.config.js
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: 5,
chunks: 'initial'
}
}
}
},
plugins: [
// 启用Gzip压缩
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i,
threshold: 10240,
minRatio: 0.8
}),
// 包分析工具(开发时使用)
process.env.NODE_ENV === 'analyze' && new BundleAnalyzerPlugin()
].filter(Boolean)
},
chainWebpack: (config) => {
// 移除prefetch插件减少冗余代码
config.plugins.delete('prefetch');
// 生产环境配置
if (process.env.NODE_ENV === 'production') {
config.optimization.minimize(true);
}
}
};
- 分包优化策略
配置分包加载
// manifest.json 或 pages.json
{
"optimization": {
"subPackages": true
},
"subPackages": [
{
"root": "pages/sub1",
"pages": [
"page1",
"page2"
]
},
{
"root": "pages/sub2",
"pages": [
"page3",
"page4"
]
}
]
}
按需加载组件
// 使用异步组件
export default {
components: {
HeavyComponent: () => import('@/components/HeavyComponent.vue')
}
}
- 静态资源优化
使用CDN加速
<!-- 将大体积库改为CDN引入 -->
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<!-- 在vue.config.js中配置externals -->
module.exports = {
configureWebpack: {
externals: {
'lodash': '_',
'moment': 'moment'
}
}
}
压缩和优化图片
# 使用图片压缩工具
npm install -g imagemin-cli
imagemin static/images/* --out-dir=static/optimized-images
- 运行时优化
懒加载和预加载策略
// pages.json 中配置
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"lazyCodeLoading": "requiredComponents" // 按需加载代码
}
}
]
}
清理console日志
// vue.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
configureWebpack: {
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: process.env.NODE_ENV === 'production'
}
}
})
]
}
}
};
- 分析和监控工具
使用webpack-bundle-analyzer
// package.json
{
"scripts": {
"analyze": "cross-env NODE_ENV=analyze uni-build"
}
}
# 运行分析
npm run analyze
监控打包体积
// 在vue.config.js中添加体积限制
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
configureWebpack: {
performance: {
hints: 'warning',
maxAssetSize: 500000, // 500KB
maxEntrypointSize: 500000
}
}
};
- 平台特定优化
H5平台优化
// 使用路由懒加载
const routes = [
{
path: '/home',
component: () => import('@/pages/home.vue')
}
];
小程序平台优化
{
"app-plus": {
"optimization": {
"codeSplitting": true,
"minifyJS": true,
"minifyWXML": true,
"minifyWXSS": true
}
}
}
最佳实践总结
- 定期清理:每季度检查并移除未使用的依赖
- 按需引入:避免整体引入大型库
- 分包策略:将不常用的功能拆分为子包
- 资源优化:压缩图片和使用CDN
- 构建分析:使用分析工具识别体积问题
- 持续监控:设置体积阈值防止反弹
通过综合运用这些策略,通常可以将UniApp打包体积减少30%-50%,显著提升应用性能。
#
# 有三个请求,需要前两个接口请求之后再去请求第三个接口
- 使用 Promise 链式调用
function request1() {
return fetch('https://api.example.com/request1');
}
function request2() {
return fetch('https://api.example.com/request2');
}
function request3() {
return fetch('https://api.example.com/request3');
}
// 顺序执行
request1()
.then(response1 => {
console.log('请求1完成', response1);
return request2();
})
.then(response2 => {
console.log('请求2完成', response2);
return request3();
})
.then(response3 => {
console.log('请求3完成', response3);
})
.catch(error => {
console.error('请求出错', error);
});
- 使用 async/await 语法
async function executeRequests() {
try {
const response1 = await request1();
console.log('请求1完成', response1);
const response2 = await request2();
console.log('请求2完成', response2);
const response3 = await request3();
console.log('请求3完成', response3);
} catch (error) {
console.error('请求出错', error);
}
}
executeRequests();
- 使用 Promise.all 并行前两个请求
如果前两个请求可以并行执行,然后第三个请求再执行:
Promise.all([request1(), request2()])
.then(([response1, response2]) => {
console.log('请求1和请求2都已完成', response1, response2);
return request3();
})
.then(response3 => {
console.log('请求3完成', response3);
})
.catch(error => {
console.error('请求出错', error);
});
- 使用 async/await 结合 Promise.all
async function executeRequests() {
try {
const [response1, response2] = await Promise.all([request1(), request2()]);
console.log('请求1和请求2都已完成', response1, response2);
const response3 = await request3();
console.log('请求3完成', response3);
} catch (error) {
console.error('请求出错', error);
}
}
executeRequests();
- 使用 Axios 的示例
如果你使用 Axios 库:
import axios from 'axios';
async function fetchData() {
try {
const result1 = await axios.get('/api/request1');
const result2 = await axios.get('/api/request2');
// 前两个请求都完成后
const result3 = await axios.get('/api/request3', {
params: {
dataFromFirst: result1.data,
dataFromSecond: result2.data
}
});
console.log('最终结果:', result3.data);
} catch (error) {
console.error('请求失败:', error);
}
}
fetchData();
- 处理请求间的数据依赖
如果第三个请求需要前两个请求的结果作为参数:
async function executeDependentRequests() {
try {
const response1 = await request1();
const response2 = await request2();
// 使用前两个请求的结果作为第三个请求的参数
const response3 = await request3({
data1: response1.data,
data2: response2.data
});
console.log('所有请求完成', response3);
} catch (error) {
console.error('请求出错', error);
}
}
注意事项
- 错误处理:确保使用 try/catch 或 .catch() 来捕获可能的错误
- 性能考虑:如果请求之间没有依赖关系,考虑并行执行以提高性能
- 取消请求:在 React 组件中使用时,注意组件卸载时取消未完成的请求
- 加载状态:在 UI 中显示适当的加载状态
选择哪种方法取决于你的具体需求:
- 如果请求需要严格顺序执行,使用顺序的 await 或 then 链
- 如果前两个请求可以并行,使用 Promise.all
- 如果有复杂的数据依赖关系,确保正确处理数据传递
# fetch和axios如何中断
Fetch 中断请求
Fetch API 本身不直接提供中断请求的方法,但可以使用 AbortController来实现。
- 使用 AbortController
// 创建 AbortController 实例
const controller = new AbortController();
const signal = controller.signal;
// 发起 fetch 请求
fetch('https://api.example.com/data', {
signal: signal // 将 signal 传递给 fetch
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求已被取消');
} else {
console.error('请求错误:', error);
}
});
// 在需要时中断请求
controller.abort(); // 这会触发 AbortError
- React 组件中使用示例
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('组件卸载,请求取消');
}
});
// 组件卸载时取消请求
return () => controller.abort();
}, []);
return <div>组件内容</div>;
}
Axios 中断请求
Axios 提供了两种方式来中断请求:使用 CancelToken(旧版)或 AbortController(新版)。
- 使用 CancelToken(Axios < 0.22.0)
import axios from 'axios';
// 创建 CancelToken
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 发起请求
axios.get('https://api.example.com/data', {
cancelToken: source.token
})
.then(response => console.log(response.data))
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已被取消:', error.message);
} else {
console.error('请求错误:', error);
}
});
// 在需要时取消请求
source.cancel('操作被用户取消');
- 使用 AbortController(Axios ≥ 0.22.0)
import axios from 'axios';
const controller = new AbortController();
axios.get('https://api.example.com/data', {
signal: controller.signal
})
.then(response => console.log(response.data))
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已被取消');
} else {
console.error('请求错误:', error);
}
});
// 在需要时取消请求
controller.abort();
- React 组件中使用示例
import { useEffect } from 'react';
import axios from 'axios';
function MyComponent() {
useEffect(() => {
const controller = new AbortController();
axios.get('https://api.example.com/data', {
signal: controller.signal
})
.then(response => console.log(response.data))
.catch(error => {
if (axios.isCancel(error)) {
console.log('组件卸载,请求取消');
}
});
// 组件卸载时取消请求
return () => controller.abort();
}, []);
return <div>组件内容</div>;
}
对比总结
| 特性 | Fetch | Axios |
|---|---|---|
| 中断机制 | AbortController | CancelToken 或 AbortController |
| 错误类型 | AbortError | Cancel 对象 |
| React 集成 | 需要手动处理 | 提供 isCancel 方法检查 |
| 兼容性 | 现代浏览器 | 需要 Axios 版本适配 |
| 使用便捷性 | 较基础 | 更高级的 API |
最佳实践
- 组件卸载时取消请求:在 React 的 useEffect cleanup 函数中取消请求
- 用户操作取消:为用户提供取消按钮,点击时中断请求
- 错误处理:区分取消错误和其他类型的错误
- 版本适配:注意 Axios 版本差异,新版推荐使用 AbortController
- 多个请求:可以共享同一个 AbortController 来取消多个请求
// 共享 AbortController 取消多个请求
const controller = new AbortController();
// 请求1
fetch('/api/data1', { signal: controller.signal });
// 请求2
fetch('/api/data2', { signal: controller.signal });
// 同时取消两个请求
controller.abort();
通过合理使用请求中断机制,可以提升应用性能和用户体验,避免不必要的网络请求和潜在的内存泄漏问题。
# 浏览器的本地缓存(*)
- Cookie
基本特性
- 存储大小:约 4KB
- 生命周期:可设置过期时间,或会话结束时过期
- 自动发送:每次请求都会自动携带到服务器
- 作用域:可设置域名和路径范围
使用方法
// 设置 Cookie
document.cookie = "username=John; expires=Thu, 18 Dec 2025 12:00:00 UTC; path=/";
// 读取 Cookie
function getCookie(name) {
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [key, value] = cookie.trim().split('=');
if (key === name) return decodeURIComponent(value);
}
return null;
}
// 删除 Cookie
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
使用场景
- 用户身份认证
- 会话管理
- 简单的用户偏好设置
- Web Storage
localStorage
- 存储大小:约 5-10MB(不同浏览器有差异)
- 生命周期:永久存储,除非手动删除
- 作用域:同一域名下的所有页面共享
// 存储数据
localStorage.setItem('user', JSON.stringify({ name: 'John', age: 30 }));
// 读取数据
const user = JSON.parse(localStorage.getItem('user'));
// 删除数据
localStorage.removeItem('user');
// 清空所有数据
localStorage.clear();
// 监听存储变化(在同源的其他页面)
window.addEventListener('storage', (event) => {
console.log('存储发生变化:', event.key, event.newValue);
});
sessionStorage
- 存储大小:约 5-10MB
- 生命周期:会话结束时清除(关闭标签页或浏览器)
- 作用域:仅当前标签页可用
// 使用方法与 localStorage 相同
sessionStorage.setItem('token', 'abc123');
const token = sessionStorage.getItem('token');
使用场景
- localStorage:长期存储的用户设置、缓存数据
- sessionStorage:临时会话数据、表单数据暂存
- IndexedDB
基本特性
- 存储大小:通常至少 50MB,可请求更多空间
- 数据类型:支持存储复杂对象、文件等
- 事务支持:ACID 事务保证
- 异步操作:所有操作都是异步的
使用方法
// 打开或创建数据库
const request = indexedDB.open('MyDatabase', 1);
request.onerror = (event) => {
console.error('数据库打开失败:', event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('数据库打开成功');
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建对象存储空间(类似表)
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
}
};
// 添加数据
function addUser(db, user) {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
store.add(user);
}
// 读取数据
function getUser(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
使用场景
- 大量结构化数据存储
- 离线应用数据存储
- 需要复杂查询的场景
- Cache API (Service Worker)
基本特性
- 主要用于:缓存网络请求和响应
- 存储大小:与存储空间相关
- 生命周期:由 Service Worker 控制
使用方法
// 在 Service Worker 中
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('my-cache-v1').then((cache) => {
return cache.addAll([
'/',
'/styles.css',
'/script.js',
'/image.jpg'
]);
})
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});
使用场景
- PWA 应用的离线支持
- 静态资源缓存
- 网络请求的缓存策略
- 对比总结
| 技术 | 存储大小 | 生命周期 | 数据类型 | 同步/异步 | 主要用途 |
|---|---|---|---|---|---|
| Cookie | 4KB | 可设置 | 字符串 | 同步 | 身份认证、会话管理 |
| localStorage | 5-10MB(Chrome/Chromium/Edge:通常为 5MB。Firefox:约为 10MB。Safari:一般在 4MB 到 5MB 之间。) | 永久 | 字符串 | 同步 | 长期数据存储 |
| sessionStorage | 5-10MB(Chrome/Chromium/Edge:通常为 5MB。Firefox:约为 10MB。Safari:一般在 4MB 到 5MB 之间。) | 会话期间 | 字符串 | 同步 | 临时会话数据 |
| IndexedDB | 大量 | 永久 | 复杂对象 | 异步 | 大量结构化数据 |
| Cache API | 大量 | 可控 | 请求响应 | 异步 | 网络资源缓存 |
- 最佳实践
选择合适的存储方案
// 小量简单数据 → localStorage
localStorage.setItem('theme', 'dark');
// 会话临时数据 → sessionStorage
sessionStorage.setItem('formData', JSON.stringify(formValues));
// 大量复杂数据 → IndexedDB
const db = await openDB('app-database', 1);
await db.add('users', userData);
// 网络资源缓存 → Cache API
const cache = await caches.open('assets-v1');
await cache.add('/static/logo.png');
安全考虑
// 敏感数据不要存储在本地
// 使用 HTTPS 保护传输数据
// 考虑数据加密存储
// 简单的加密示例(实际应用应使用更安全的加密方式)
const encryptData = (data, key) => {
// 实际应用中应使用 Web Crypto API
return btoa(JSON.stringify(data) + key);
};
容量管理
// 检查存储空间
function checkLocalStorageSpace() {
let total = '';
for (let i = 0; i < 1024 * 1024; i++) {
total += 'a';
}
try {
localStorage.setItem('test', total);
localStorage.removeItem('test');
return true;
} catch (e) {
return false;
}
}
// 清理过期数据
function cleanupOldData() {
const now = Date.now();
const data = JSON.parse(localStorage.getItem('cachedData') || '{}');
Object.keys(data).forEach(key => {
if (data[key].expiry && data[key].expiry < now) {
delete data[key];
}
});
localStorage.setItem('cachedData', JSON.stringify(data));
}
- 现代存储方案推荐
使用库简化操作
// 使用 localForage 简化 IndexedDB 操作
import localForage from 'localforage';
// 配置
localForage.config({
name: 'myApp',
storeName: 'keyvaluepairs'
});
// 使用类似 localStorage 的 API
await localForage.setItem('key', 'value');
const value = await localForage.getItem('key');
选择合适的浏览器存储技术取决于你的具体需求:数据大小、生命周期、复杂度等因素。
# promise(*)
Promise 是 JavaScript 中用于处理异步操作的对象,它解决了传统回调函数嵌套("回调地狱")的问题,让异步代码的逻辑更清晰、更易维护。
核心概念
Promise 代表一个尚未完成但最终会完成的操作,有三种状态:
- pending(等待中):初始状态,操作尚未完成
- fulfilled(已成功):操作完成并返回结果
- rejected(已失败):操作出错并返回原因
状态一旦改变(从 pending → fulfilled 或 pending → rejected),就不会再变。
promise的方法
实例方法(Promise 对象自身的方法)
then()作用:处理 Promise 成功(fulfilled)或失败(rejected)的结果,返回一个新的 Promise,支持链式调用。
语法:
promise.then(onFulfilled, onRejected)参数:
onFulfilled:成功时的回调(接收 resolve 的结果)onRejected:失败时的回调(可选,接收 reject 的原因)
catch()- 作用:专门处理 Promise 失败(rejected)的情况,相当于
then(null, onRejected)。 - 语法:
promise.catch(onRejected) - 用途:集中捕获链式调用中的所有错误,更清晰。
- 作用:专门处理 Promise 失败(rejected)的情况,相当于
finally()- 作用:无论 Promise 成功或失败,都会执行的回调(无参数)。
- 语法:
promise.finally(onFinally) - 用途:清理操作(如关闭加载动画、释放资源)。
静态方法(Promise 类自身的方法)
Promise.all(iterable)作用:并行处理多个 Promise,等待所有都成功才返回结果数组;只要有一个失败,立即返回该失败原因。
适用场景:需要所有异步操作完成后再执行下一步(如同时加载多个资源)。
Promise.race(iterable)作用:并行处理多个 Promise,第一个状态改变(无论成功或失败) 的结果就是最终结果。
适用场景:设置超时控制(如请求超时后使用默认值)。
Promise.resolve(value)作用:快速创建一个已成功的 Promise,值为
value(若value本身是 Promise,则直接返回它)。用途:将非 Promise 对象转为 Promise,统一异步代码风格。
Promise.reject(reason)- 作用:快速创建一个已失败的 Promise,原因是
reason。
- 作用:快速创建一个已失败的 Promise,原因是
Promise.allSettled(iterable)作用:等待所有 Promise 完成(无论成功或失败),返回一个包含所有结果的数组(每个结果包含状态和值)。
适用场景:需要知道所有异步操作的最终状态(如批量任务统计成功 / 失败数量)。
这些方法覆盖了大多数异步处理场景,其中 then()、catch()、Promise.all() 和 Promise.race() 是日常开发中最常用的。
优势
- 解决回调地狱:通过
.then()链式调用,替代多层嵌套 - 错误处理统一:使用
.catch()集中处理所有异步操作的错误 - 状态明确:清晰区分异步操作的不同阶段
# Promise 和async await的区别
语法差异:
- promise:是链式调用 ;
- async/await:是基于promise的语法糖,简洁
错误处理方式:
- promise:是then中的第二个参数或者catch方法来处理异步操作;
- async/await是使用try/catch来捕获异常处理
代码结构:
- promise是链式调用可能会出现回调地狱;
- async/await更加清晰明了
性能上:性能差别不大,某些情况下 promise是更快一些
# JavaScript精度问题
主要由于 JavaScript 使用 IEEE 754 标准的浮点数表示数字,这种表示方式会产生一些精度损失。
这是因为二进制浮点数无法精确表示某些十进制小数,会产生微小的误差。
使用第三方库 decimal.js:使用的二进制来计算
const a = 9.99; const b = 8.03; // 加法 let c = new Decimal(a).add(new Decimal(b)) // 减法 let d = new Decimal(a).sub(new Decimal(b)) // 乘法 let e = new Decimal(a).mul(new Decimal(b)) // 除法 let f = new Decimal(a).div(new Decimal(b)) let c = a.plus(b); // c 的值是 Decimal(0.3) // 比较浮点数 console.log(c.equals(new Decimal(0.3))); // true在货币计算中使用固定小数位
let price = 9.99; let quantity = 3; let total = (price * quantity).toFixed(2); // "29.97" console.log(typeof total); // "string"整数运算
let a = 0.1; let b = 0.2; let result = (a * 10 + b * 10) / 10; // 0.3
# 强缓存和协商缓存(*)
强缓存(强制缓存):
强缓存是指浏览器在请求资源时,先检查本地缓存(本地磁盘缓存)是否存在该资源的有效副本,并根据缓存规则决定是否直接使用缓存副本,而不发送请求到服务器。
浏览器在收到服务器返回的响应头中的缓存控制字段(如Expires、Cache-Control)后,会根据这些字段判断是否使用强缓存。
如果资源未过期,浏览器直接从本地缓存中加载资源,不会向服务器发送请求,从而加快页面加载速度。
前端代码:
const getApi = async () => { let res = await axios.get('http://localhost:3000/api/dev') console.log(res) } onBeforeMount(async() => { getApi() })后端代码:
router.get('/api/dev', (req, res, next) => { res.setHeader('Cache-Control', max-age-3600') // 缓存一小时 res.json({a:1}) })
协商缓存:
当浏览器没有命中强缓存时就会发送请求,验证协商缓存是否命中,如果缓存命中则返回304状态码,否则返回新的资源数据
前端代码:
const getApi = async () => { let res = await axios.get('http://localhost:3000/api/dev', { headers: { 'If-None-Match': localStorage.getItem('ETag') || '' } }) if(res.status == 200){ console.log(res, res.headers.etag) localStorage.setItem('ETag', res.headers.etag) } } onBeforeMount(async() => { getApi() })后端代码:
let data = { name: '张三'} function generateETag(data){ const hash = crypto.createHash('md5').update(JSON.stringify(data)) return `"${hase}"` } let currentETag = generateETag(data) router.get('/api/data', (req, res, next) => { if(req.headers['If-None-Match'] === currentETag){ res.status(304).end() } else { res.send('ETag', currentETag) res.json(data) } })具体请求流程:
当浏览器发起一个资源请求时,浏览器会先判断本地是否有缓存记录,如果没有会向浏览器请求新的资源。
如果有缓存记录,先判断强缓存是否存在,如果强缓存的时间没有过期则返回本地缓存资源(状态码为200).
如果强缓存失败了,客户端会发起请求进行协商缓存策略,首先服务端判断头信息标识符,如果客户端传来标识符和当前服务器上的标识符是一致的,则返回状态码304(也就是不返回资源内容)。
如果etag和服务器上的标识符不一致,重新获取新的资源,并进行协商缓存返回数据。
# ES6新特性
- let, const
- for-of
- arguments
- promise
- Set(), Map()
- Proxy
- Generator
- 模板字符串
- 箭头函数
- 扩展运算符
- 解构赋值
# 构造函数新增的方法
Array.from()
Array.of()
# 实例对象新增的方法
copyWithin()
find()、findIndex()
fill()
entries(),keys(),values()
includes()
flat(),flatMap()
# JS 数据类型 ?存储上的差别?(*)
数据类型主要包括两部分:
基本数据类型: Undefined、Null、bigInt、Boolean、Number 和 String,Symbol(创建后独一无二且不可变的数据类型 )
引用数据类型: Object (包括 Object 、Array 、Function,Date,RegExp,Set,Map)
存储区别:
- 内存分配:
基本数据类型(如number、string)存储在栈中:因其占用空间固定且较小,适合在栈中快速分配内存,便于高效访问。
引用类型(如对象、数组)存储在堆中:变量仅在栈中保存指向堆内存的地址(指针),由于引用值大小不固定,堆存储可动态分配内存,避免影响栈的访问效率。
访问机制:不同的内存分配机制也带来了不同的访问机制
- 基本类型:直接访问栈中的值(按值访问)。
- 引用类型:无法直接访问堆中对象,需先通过栈中的地址找到堆内存位置,再获取对象值(按引用访问)。
复制变量时的不同 - 基本类型:复制时会生成原始值的独立副本,两个变量此后互不影响(值相同但存储独立)。 - 引用类型:复制的是栈中的地址指针,两个变量指向堆中同一个对象,任何一方修改都会影响另一方(共享同一份数据)。
# null 和 undefined 的区别
相同点:
都是原始类型的值,且保存在栈中 进行条件判断时,两者都是false:
不同点:
- null是js的关键字,表示空值;undefined不是js的关键字,它是一个全局变量
- null是Object的一个特殊值,如果一个Object为null,表示这个对象不是有效对象,null是一个不存在的对象的占位符;undefined是Globel的一个属性
- 类型不一样:
typeof(null) // object
typeof(undefined) //undefined
console.log(typeof(null) === 'object')//true
console.log(typeof(undefied) === 'undefined')//true
- 转换的值不一样:
console.log(Number(undefined));//NaN
console.log(Number(11+ undefined));//NaN
console.log(Number(null));//0
console.log(Number(11+ null));//11
# 跨域
什么是“域”:域,也叫做“源”。一个完整的域由三部分组成:协议,IP,端口。
什么是跨域:指的就是在一个域中向另一个不同的域发送请求。两个域之间,协议、IP、端口三者中有任意一个不一样,则判定为是两个不同的域。
为什么存在跨域:基于JavaScript的同源策略,该策略要求,在浏览器中,只能进行同源访问,不能进行跨域请求。
跨域的方式:
jsonp(只支持get请求)
jsonp的原理就是利用
<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。<script> var script = document.createElement('script'); script.type = 'text/javascript'; // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数 script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback'; document.head.appendChild(script); // 回调执行函数 function handleCallback(res) { alert(JSON.stringify(res)); } </script>$ajax中的使用方式
$.ajax({ url: 'http://localhost:3000/teachers/getTeacherInfo', type: 'GET', dataType: 'jsonp', // 重点 success(res) { console.log('getTeacherInfo', res); } }) // express后端返回时,将res.send()换成res.jsonp(返回数据)
proxy
websocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许客户端和服务器之间进行实时数据传输。与传统的 HTTP 请求-响应模式不同,WebSocket 可以实现服务器主动向客户端推送数据,实现实时更新和即时通讯等功能。
前端方法:
- onopen():当 WebSocket 连接成功建立时触发的事件。
- onmessage 事件:当接收到服务端发送的消息时触发的事件。
- send() 方法:用于向服务端发送消息。
- close() 方法:用于关闭 WebSocket 连接。
- onerror 事件:当 WebSocket 连接发生错误时触发的事件。
后端方法:
- connection 事件:当客户端与服务器建立连接时触发的事件。
- message 事件:当接收到客户端发送的消息时触发的事件。
- send() 方法:用于向客户端发送消息。
- close() 方法:用于关闭与客户端的连接。
- error 事件:当 WebSocket 服务器发生错误时触发的事件。
缺陷以及优化
WebSocket 作为一种实时通信协议,虽然具有很多优势,但也存在一些缺陷和需要优化的地方,主要包括:
兼容性问题:
- WebSocket 协议需要浏览器和服务器端都支持,对于老版本的浏览器和服务器可能存在兼容性问题。
- 为了解决兼容性问题,通常需要采用 WebSocket 回退机制,如 Socket.IO 等库。
资源消耗问题:
- WebSocket 连接会一直保持打开状态,这可能会增加服务器和客户端的资源消耗。
- 需要采取措施,如实现心跳机制、连接池管理等来优化资源使用。
安全问题:
- WebSocket 连接可能存在安全隐患,如跨站脚本攻击(XSS)、跨站请求伪造(CSRF)等。
- 需要采取安全措施,如验证消息来源、过滤输入输出、使用 SSL/TLS 加密等。
消息丢失问题:
- 由于 WebSocket 连接可能会断开,导致消息丢失的问题。
- 需要采取措施,如实现消息重发、消息缓存等机制来保证消息的可靠性。
nginx:反向代理
cors:跨域资源分享
// CORS 配置跨域 var allowCrossDomain = function (req, res, next) { // 设置允许跨域访问的请求源(* 表示接受任意域名的请求) res.header("Access-Control-Allow-Origin", "*"); // 设置允许跨域访问的请求头 res.header("Access-Control-Allow-Headers", "X-Requested-With,Origin,Content-Type,Accept,Authorization"); // 设置允许跨域访问的请求类型 res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); // 同意 cookie 发送到服务器(如果要发送cookie,Access-Control-Allow-Origin 不能设置为星号) res.header('Access-Control-Allow-Credentials', 'true'); next(); }; app.use(allowCrossDomain);
# 箭头函数的特点
- 不需要function关键字来创建函数,省略return关键字
- 箭头函数没有this,箭头函数中使用this是指向的外层普通函数的this,如果外层没有普通函数,则指向的是全局的this
- 箭头函数是匿名函数,不能作为构造函数,不可以使用new命令,否则后抛出错误。
- 箭头函数没有原型对象prototype这个属性
- 箭头函数不绑定arguments,取而代之用rest参数解决,同时没有super和new.target。
- 使用call,apply,bind并不会改变箭头函数中的this指向。
- 不能使用yield关键字,不能用作Generator函数
# var、let、const 区别
变量提升: var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,(暂时性死区)
块级作用域: var不存在块级作用域 let和const存在块级作用域
重复声明: var允许重复声明变量 let和const在同一作用域不允许重复声明变量
修改声明的变量: var和let可以 const声明一个只读的常量。一旦声明,常量的值就不能改变,但对于对象和数据这种引用类型,内存地址不能修改,可以修改里面的值。
# new操作符具体干了什么?
新建一个实例对象
obj把obj实例对象和构造函数通过原型链连接起来
将构造函数的
this指向obj根据构造函数返回的类型做判断,如果是原始值则被忽略,如果是返回对象,需要正常使用
function Test(name){ this.name = name return 1; } const test = new Test('张三') console.log(test.name) // 张三function Test(name){ this.name = name return { name: '李四'}; } const test = new Test('张三') console.log(test.name) // 李四
# 手写new
/**
* new操作符都干了什么?
* 1. 创建一个空对象
* 2. 将实例对象的__proto__指向构造函数的prototype
* 3. 将构造函数的this指向实例对象
* 4. 返回实例对象
* 注意:
* 1. 构造函数的返回值如果是基本类型,则返回实例对象;如果是引用类型,则返回该引用类型
*/
// 引入 MyNew 函数,用于创建一个新的函数
function MyNew(Func, ...args) {
// 创建一个新的对象
const obj = {};
// 将新对象的 prototype 设置为传入的 Func 函数的 prototype
obj.__proto__ = Func.prototype;
// 调用 Func 函数,并将 args 作为参数传入,获取返回值
const result = Func.apply(obj, args);
// 如果返回值是对象,则直接返回;否则返回新对象
return typeof result === 'object' ? result : obj;
}
// 定义 Person 函数,用于设置人的名字和年龄
function Person(name, age) {
// 将名字和年龄绑定到 this 上
this.name = name;
this.age = age;
}
// 定义 Person 的原型方法,用于输出人的信息
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name + ', I am ' + this.age + ' years old.');
}
// 使用 MyNew 函数创建一个 Person 对象
const person = MyNew(Person, 'John', 25);
person.sayHello()
# 浅拷贝
指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝 , 两个对象指向同一个地址
Object.assign,扩展运算符,Array.concat,Array.slice
var obj = {
name: 'John',
age: 30,
sports: ['football', 'basketball'],
hobbies: {
sports: ['football', 'basketball'],
music: ['rock', 'jazz'],
books: {
fiction: ['novel', 'mystery'],
poetry: ['shi', 'song']
}
}
}
const aa = Object.assign({}, obj);
浅拷贝在修改对象属性时,只能修改对象中第一层的基本数据类型,修改第一层的引用数据类型以及嵌套属性,均会受到影响
# 深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
- 封装一个递归方法
- JSON.stringify + JSON.parse
- lodash(_.cloneDeep())
- MessageChannel()
# 作用域
作用域指的是变量、函数和对象的可访问范围,它决定了代码中标识符(如变量名)的可见性和生命周期。
分类:
全局作用域;
// 全局变量 var greeting = 'Hello World!'; function greet() { console.log(greeting); } // 打印 'Hello World!' greet();函数作用域;
function greet() { var greeting = 'Hello World!'; console.log(greeting); } // 打印 'Hello World!' greet(); // 报错: Uncaught ReferenceError: greeting is not defined console.log(greeting);块级作用域;
{ // 块级作用域中的变量 let greeting = 'Hello World!'; var lang = 'English'; console.log(greeting); // Prints 'Hello World!' } // 变量 'English' console.log(lang); // 报错:Uncaught ReferenceError: greeting is not defined console.log(greeting);词法作用域
let number = 42; function printNumber() { console.log(number); } function log() { let number = 54; printNumber(); } log(); // 42
作用域链
概念:多层函数嵌套下,变量的访问范围,访问变量时,先在当前作用域下查找变量,如果当前作用域下没有则逐级向上查找,知道全局作用域
# 原型,原型链
prototype每个函数都有一个prototype属性,被称为显示原型__proto__每个实例对象都会有__proto__属性,其被称为隐式原型 每一个实例对象的隐式原型__proto__属性指向自身构造函数的显式原型prototypeconstructor每个prototype原型都有一个constructor属性,指向它关联的构造函数。
function Parent(month){
this.month = month;
}
// Parent.prototype.father = '爸爸'
Object.prototype.father = '爸爸'
var child = new Parent('Ann');
// console.log(child.month); // Ann
// console.log(child.father); // undefined
原型链(Prototype Chain)
原型链是 JavaScript 中对象通过__proto__属性串联起来的继承关系链,终点为null,决定了对象属性和方法的查找规则。
如:当访问一个对象的属性时:
- 首先在对象自身查找该属性
- 如果找不到,则去对象的proto(即构造函数的prototype)中查找
- 如果还找不到,则继续去原型对象的proto中查找
- 直到找到Object.prototype(所有对象的最终原型),如果仍然找不到则返回undefined

# 事件代理
事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。使用事件代理的好处是可以提高性能,可以大量节省内存占用,减少事件注册,比如在table上代理所有td的click事件就非常棒 可以实现当新增子对象时无需再次对其绑定
# this
this 是 JavaScript 中的关键字,它指向当前代码执行时的上下文对象,具体指向谁完全由调用方式决定,而非定义位置。
如果单独使用,this 表示全局对象。
在方法中,this 表示该方法所属的对象。
在函数中,this 表示全局对象。
在事件中,this 表示接收事件的元素。
严格模式下:
单独使用,this指向全局对象
在函数中,this 是未定义的(undefined)。
# 数组sort的底层原理,怎么实现
1.JavaScript中的sort()方法是用来对数组元素进行排序的。它使用了一种名为快速排序(QuickSort)的算法来进行排序。这个算法的基本思路是:选择一个基准元素,将数组分成两个子数组,比基准元素小的放在左边,比基准元素大的放在右边,然后再对左右两个子数组分别进行排序。这个过程一直递归下去,直到整个数组有序。 2.具体实现中,sort()方法会先将数组分成两个子数组,然后对每个子数组分别进行排序。在排序的过程中,sort()方法会比较数组元素的大小,然后通过交换元素的位置来实现排序。在排序完成后,sort()方法会返回排序后的数组。
# JavaScript中,实现继承的方式
原型链继承:
function Parent() { this.name = 'Parent'; } Parent.prototype.sayHello = function() { console.log('Hello from ' + this.name); } function Child() { this.name = 'Child'; } Child.prototype = new Parent(); var child = new Child(); child.sayHello(); // Output: Hello from Child构造函数继承(借用构造函数):
function Parent() { this.name = 'Parent'; this.sayHello = function() { console.log('Hello from ' + this.name); } } Parent.prototype.sayHello = function() { } function Child() { Parent.call(this); this.name = 'Child'; } var child = new Child(); child.sayHello(); // Output: Hello from Child在构造函数内部声明的方法和prototype上的方法之间的区别:
对比点 构造函数内声明 prototype 上声明 存储位置 每个实例自身 所有实例共享 内存占用 高(每个实例一份) 低(只存一份) 访问优先级 ✅ 更高 ❌ 更低 是否可访问 prototype 版本 ❌ 被覆盖 ✅ 仅当实例没有该方法 适合用途 需要私有/实例级状态 公共方法 组合继承(原型链继承 + 构造函数继承):
function Parent() { this.name = 'Parent'; } Parent.prototype.sayHello = function() { console.log('Hello from ' + this.name); } function Child() { Parent.call(this); this.name = 'Child'; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; var child = new Child(); child.sayHello(); // Output: Hello from Child原型式继承:
function createObject(obj) { function F() {} F.prototype = obj; return new F(); } var parent = { name: 'Parent', sayHello: function() { console.log('Hello from ' + this.name); } }; var child = createObject(parent); child.name = 'Child'; child.sayHello(); // Output: Hello from ChildES6中的类继承(extends关键字):
class Parent { constructor() { this.name = 'Parent'; } sayHello() { console.log('Hello from ' + this.name); } } class Child extends Parent { constructor() { super(); this.name = 'Child'; } } var child = new Child(); child.sayHello(); // Output: Hello from Child
# apply,bind,call的区别
| apply | bind | call | |
|---|---|---|---|
| 参数 | (this, []) | (this, 参数列表) | (this, 参数列表) |
| 执行时机 | 立即执行 | 不会立即执行 | 立即执行 |
| 执行时限 | 只临时改变this一次 | 返回一个永久改变this指向的函数 | 临时改变this指向一次 |
- 三者都可以改变函数的
this对象指向 - 三者第一个参数都是
this要指向的对象,如果没有这个参数或参数为undefined或null,则默认指向全局window - 三者都可以传参,但是
apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入 bind是返回绑定this之后的函数,apply、call则是立即执行
# 防抖和节流
节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
function throttle(fn, interval = 300) { let lastTime = 0; return function (...args) { const now = Date.now(); const context = this; if (now - lastTime >= interval) { fn.apply(context, args); lastTime = now; } }; } window.addEventListener('scroll', throttle(() => { console.log('scrolling...'); }, 500));
防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
function debounce(fn, delay = 300) { let timer = null; return function (...args) { const context = this; clearTimeout(timer); timer = setTimeout(() => { fn.apply(context, args); }, delay); }; } const input = document.querySelector('#input'); input.addEventListener('input', debounce((e) => { console.log(e.target.value); }, 500));
一个经典的比喻:
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制
电梯第一个人进来后,15秒后准时运送一次,这是节流
电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖
# 单点登录
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport,子系统本身将不参与登录操作
# web常见攻击
- xss(跨站脚本攻击)
- 存储型XSS
- 攻击者将恶意代码提交到目标网站的数据库中
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作
- 反射型XSS
- DOM型XSS
- 预防:
- 存储型XSS
- CSRF(跨站请求伪造)
- SQL注入攻击
# typeof 与 instanceof 区别
- 功能定位
- typeof:返回一个字符串,表示操作数的基本类型(如 "number"、"string"、"object" 等),主要用于检测基础数据类型。
- instanceof:返回布尔值,判断某个对象是否是指定构造函数的实例(基于原型链判断),主要用于检测引用类型的具体类型。
- 适用场景与局限性
- typeof:
- 能准确识别基础类型(
number/string/boolean/undefined/symbol),但对null会误判为 "object"。 - 对引用类型,除
function会返回 "function" 外,其他(如数组、对象、日期)均返回 "object",无法区分具体类型。
- 能准确识别基础类型(
- instanceof:
- 能精准判断引用类型(如
[1,2] instanceof Array → true、new Date() instanceof Date → true)。 - 无法判断基础数据类型(如
123 instanceof Number → false,因基础类型不是对象实例)。 - 结果受原型链影响(如
[] instanceof Object → true,因数组是对象的子类型)。
- 能精准判断引用类型(如
- 本质差异
- typeof 基于值的类型标签判断(底层类型编码),操作数可以是任意类型。
- instanceof 基于原型链查找,检测构造函数的
prototype是否存在于对象的原型链上,仅适用于对象。
# 判断是否为数组的5种方法?
- instanceof: data instanceof Array
- constructor: data.constructor == Array
- Array.isArray(): Array.isArray(data) 最推荐
- Object.prototype.toSrtring.call()
# 哪些操作会导致内存泄漏
内存泄漏: JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收
未使用 var 声明的全局变量
闭包函数(Closures)
循环引用(两个对象相互引用)
控制台日志(console.log)
移除存在绑定事件的DOM元素(IE)
定时器未清除
// ❌ 定时器未清理 setInterval(() => { // 每次都创建新对象 data = new LargeData(); }, 100); // ✅ 及时清除 const timer = setInterval(() => {...}, 100); clearInterval(timer);事件监听未移除
// ❌ 重复添加未移除 element.addEventListener('click', handler); // ❌ 移除时没用引用 element.removeEventListener('click', handler); // 必须是同一个函数引用全局变量
// ❌ 全局变量无法被回收 var globalData = new LargeData(); // ✅ 减少全局变量,使用 let/const let localData = new LargeData();Map/Set缓存未清理
const cache = new Map(); function process(data) { if (!cache.has(data)) { cache.set(data, heavyComputation(data)); } return cache.get(data); } // cache 无限增长
# Javascript中如何实现函数缓存?函数缓存有哪些应用场景?
函数缓存,就是将函数运算过的结果进行缓存
实现方式: 实现函数缓存主要依靠闭包、柯里化、高阶函数
应用场景:
- 对于昂贵的函数调用,执行复杂计算的函数
- 对于具有有限且高度重复输入范围的函数
- 对于具有重复输入值的递归函数
- 对于纯函数,即每次使用特定输入调用时返回相同输出的函数
# 闭包和闭包常用场景
闭包是指有权访问另一个函数作用域中的变量的函数,创建闭包常见方式,就是在一个函数的内部创建另一个函数 闭包有三个特性:
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收机制回收
闭包的好处:
- 能够实现封装和缓存等;
闭包的缺点:
- 就是常驻内存,会增大内存使用量,使用不当会造成内存泄漏
应用场景:
- 常见的防抖节流
- 使用闭包可以在 JavaScript 中模拟块级作用域
- 闭包可以用于在对象中创建私有变量
# 用过哪些设计模式?
工厂模式: 工厂模式解决了重复实例化的问题,但还有一个问题,那就是识别问题 主要好处就是可以消除对象间的耦合,通过使用工程方法而不是new关键字 构造函数模式: 使用构造函数的方法,即解决了重复实例化的问题,又解决了对象识别的问题,该模式与工厂模式的不同之处在于,直接将属性和方法赋值给 this对象;
# web开发中会话跟踪的方法有哪些?
cookiesessionurl重写- 隐藏
inputip地址
# for…in 和 for … of区别
for…in 循环是用来遍历对象属性的,它可以枚举目标对象的所有可枚举属性,包括继承链上的属性,但遍历的顺序是不确定的
for…of 循环是用来遍历可迭代对象 (Iterable) 的,它可以遍历数组、字符串、Map、Set 等内置的可迭代对象,但不能遍历普通的对象,也不能遍历对象的属性
区别:
- for…in遍历数组返回下标,遍历对象返回键
- for…of遍历数组返回数据,不可以遍历普通对象
# 如何判断一个对象为空对象
通过 Object.keys(obj) 方法获取对象的所有属性名,并判断属性数量是否为 0 来实现 、
let obj = {'name':'zs'}
Object.keys(obj).length //1
let objs = {}
Object.keys(objs).length //0
# 如何让多个异步函数顺序执行
- 使用
Promise的then方法链接异步任务。- 使用
async/await关键字。
# cookie,session,token区别
Cookie:Cookie是在用户端存储数据的一种机制,它可以存储一些简单的用户信息和标识。服务器通过设置Cookie并发送到客户端,在下次请求时客户端会自动携带该Cookie,从而实现对用户身份的验证或其他操作。Cookie的缺点是可能面临安全问题,因为Cookie存储在客户端,容易遭受窃取或伪造攻击。 Session:Session是在服务器端存储数据的一种机制,它可以保存一些复杂的用户信息和状态,用于实现对用户身份的验证和跟踪。服务器使用一个唯一的Session ID来和客户端进行交互,从而避免了安全性问题。但是Session也存在一些缺点,例如对服务器负载压力较大等问题。 Token:Token是一种包含用户身份和权限信息的加密字符串,通常由服务器生成并发送给客户端。客户端使用Token代替Cookie或Session来进行身份验证和数据传输。Token的优点是可以减轻服务器压力,减少网络流量和延迟,并且减少了安全性问题。Token的缺点是需要保证其加密和传输的安全性。 Cookie适用于简单的身份验证和数据存储,Session适用于需要复杂状态管理和用户跟踪的场景,而Token则更适合于分布式系统和APP等跨平台的数据传输。
# 导致页面加载白屏时间长的原因有哪些,怎么进行优化
原因:
大量 HTTP 请求:
在页面加载过程中,浏览器需要请求服务器获取页面的 HTML、CSS、JavaScript、图片等资源,如果请求过多,会导致页面加载时间变长。可以通过减少 HTTP 请求的数量来优化加载速度,例如合并 CSS 和 JavaScript 文件,压缩图片等。
大量 JavaScript 代码:
当浏览器下载并解析 JavaScript 代码时,页面的渲染会被阻塞,这也会导致页面加载时间变长。可以通过将 JavaScript 代码异步加载、延迟加载或分割成多个小文件来优化加载速度。
大量 CSS 代码:
与 JavaScript 类似,CSS 代码也会阻塞页面渲染,可以通过压缩 CSS 代码、减少 CSS 文件的大小和数量、使用外部链接等方法来优化加载速度。
服务器响应时间过长:
如果服务器响应时间过长,也会导致页面加载时间变长。可以通过升级服务器硬件、优化代码等方式来减少服务器响应时间。 不合理的 DOM 结构:如果页面的 DOM 结构不合理,也会导致页面加载时间变长。可以通过减少 DOM 节点数量、避免使用 table 布局、使用 CSS Sprite(雪碧图) 等方式来优化加载速度。
优化:
- 压缩 HTML、CSS、JavaScript、图片等资源,减少文件大小。
- 合并 CSS 和 JavaScript 文件,减少 HTTP 请求的数量。
- 将 JavaScript 代码异步加载、延迟加载或分割成多个小文件。
- 使用浏览器缓存,避免重复下载资源。
- 使用外部链接或 CDN 加速器等方式来加速资源加载。
- 减少 DOM 节点数量,避免使用 table 布局等方式来优化页面渲染速度。
# javascript 代码中的"use strict";是什么意思?说说严格模式的限制
use strict是一种ECMAscript 5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行,使JS编码更加规范化的模式,消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为 限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性
- 不能使用with语句
- 禁止this指向window
上一篇: 下一篇: