一、HTML & CSS 基础
1.语义化HTML
问: HTML5新增的语义化标签有哪些?举例说明使用场景?
定义文档或区块的页眉(通常包含标题、导航或者LOGO)
<header>
<h1>网站标题</h1>
<nav>主导航链接</nav>`在这里插入代码片`
</header>
- <nav>
定义导航栏链接集合(如左侧菜单、侧边导航栏)。
<nav>
<ul>
<li><a href="/">首页</a></li>
<li><a href="/about">关于我们</a></li>
</ul>
</nav>
- <article>
表示独立的内容区块(如:博客、新闻、评论)。
<article>
<h2>文章标题</h2>
<p>文章正文内容...</p>
</article>
- <section>
定义文档中的逻辑分区(如章节、主题分组)。
注: 通常带有标题,但内容需关联
<section>
<h2>产品介绍</h2>
<p>详细描述产品特性...</p>
</section>
- <aside>
定义与主内容间接相关的内容(如侧边栏、广告、引用)。
<aside>
<h3>相关推荐</h3>
<ul>...</ul>
</aside>
- <footer>
定义文档或区块的页脚(如版权信息、联系方式)。
<footer>
<p>© 2023 公司名称. 版权所有.</p>
</footer>
- <main>
标识页面的核心内容(一个页面仅一个)。
<main>
<h1>主要标题</h1>
<p>页面核心内容...</p>
</main>
- <figure> & <figcaption>
包裹媒体(如图片、图表)及其标题。
<figure>
<img src="chart.png" alt="数据图表">
<figcaption>图1: 年度销售数据</figcaption>
</figure>
- <time>
定义时间或日期(机器可读格式)。
<time datetime="2023-10-01">2023年10月1日</time>
- <mark>
高亮显示文本(如搜索关键词)。
<p>这是一个<mark>重要</mark>的提示。</p>
使用场景总结:
-
SEO优化:语义化标签帮助搜索引擎理解页面结构(如
<article>
明确内容主体)。 -
可访问性:屏幕阅读器依赖标签识别内容类型(如
<nav>
提示导航区域)。 -
代码可读性:替代无意义的
<div>
嵌套,便于团队协作维护。
注意:避免滥用<section>
,仅在内容需要逻辑分组时使用;<article>
应保证内容的独立性。
问: 如何优化页面SEO的HTML结构?
- 语义化HTML5标签
1.使用正确的HTML5语义标签(header, nav, main, article, section, aside, footer)
2.避免过度使用div标签
- 标题结构优化
1.保持标题层级清晰有序
2.每个页面只使用一个H1标签
3.避免跳过标题层级(如H1直接接H3)
- 元标签优化
<head> <title>简洁描述性标题(50-60字符)</title> <meta name="description" content="吸引人的页面描述(150-160字符)"> <meta name="keywords" content="关键词1,关键词2,关键词3"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta charset="UTF-8"> <link rel="canonical" href="https://example.com/page"> </head>
- 图片优化
1.始终使用alt属性
2.考虑使用懒加载(lazy loading)
3.使用适当的文件格式和压缩
- 链接优化
1.使用描述性锚文本
2.避免"点击这里"等模糊文本
3.为重要链接添加title属性
- 结构化数据标记
1.使用Schema.org标记
2.适用于文章、产品、活动等类型内容
- 移动友好设计
1.使用响应式设计
2.确保视口设置正确
3.避免使用Flash和弹出窗口
- 性能优化
1.最小化HTML代码
2.延迟加载非关键资源
3.优化CSS和JavaScript交付
注:SEO是一个持续的过程,需要定期审查和调整
2.CSS布局
问: 实现一个三栏布局(左右固定宽度,中间自适应),有哪些方案?
- flex布局
.container { display: flex; height: 100vh; } .left, .right { width: 200px; /* 固定宽度 */ background: #ccc; } .center { flex: 1; /* 自适应 */ background: #eee; }
- Grid布局
.container { display: grid; grid-template-columns: 200px auto 200px; height: 100vh; } .left, .right { background: #ccc; } .center { background: #eee; }
- float浮动
.left { float: left; width: 200px; background: #ccc; height: 100vh; } .right { float: right; width: 200px; background: #ccc; height: 100vh; } .center { margin: 0 200px; background: #eee; height: 100vh; }
- 绝对定位
.container { position: relative; height: 100vh; } .left { position: absolute; left: 0; width: 200px; background: #ccc; height: 100%; } .right { position: absolute; right: 0; width: 200px; background: #ccc; height: 100%; } .center { margin: 0 200px; background: #eee; height: 100%; }
- 表格布局
.container { display: table; width: 100%; height: 100vh; } .left, .center, .right { display: table-cell; } .left, .right { width: 200px; background: #ccc; } .center { background: #eee; }
- calc()计算方案
.left { float: left; width: 200px; background: #ccc; height: 100vh; } .right { float: right; width: 200px; background: #ccc; height: 100vh; } .center { width: calc(100% - 400px); float: left; background: #eee; height: 100vh; }
现代前端开发中,Flexbox 和 Grid 是最推荐使用的方案,它们具有更好的灵活性和响应式特性,代码也更简洁。其中:
Flexbox 适合一维布局(行或列)
Grid 适合二维布局(行列同时控制)
浮动和定位方案是传统方法,在需要兼容旧浏览器时可以考虑使用。
问: Flexbox和Grid的区别?如何选择?
特性 | Flexbox | Grid |
---|---|---|
维度 | 一维布局(行或列) | 二维布局(行和列同时控制) |
控制方向 | 一次只能控制一个方向 | 可以同时控制行和列 |
项目排列 | 基于内容流动排列 | 基于网格线精确放置 |
隐式网格 | 更依赖内容决定布局 | 显式定义网格结构 |
对齐方式 | 有更丰富的对齐属性 | 对齐功能更强大且统一 |
适用场景 | 组件内部布局、线性布局 | 整体页面布局、复杂二维布局 |
选择 Flexbox 当:
- 需要线性布局(沿一个轴排列)
- 布局内容大小未知或动态变化
- 需要内容优先的布局(内容决定布局)
- 处理组件内部的排列
- 需要等高列效果
- 典型用例:
- 导航栏
- 卡片组件
- 表单元素排列
- 媒体对象(如图文混排)
选择 Grid 当:
- 需要二维布局(同时控制行和列)
- 需要精确控制项目位置
- 布局结构固定或需要显式定义
- 创建整体页面框架
- 需要复杂的重叠布局
- 典型用例:
- 整个页面布局
- 仪表盘
- 图片画廊
- 复杂的表单布局
- 需要精确对齐的界面
实际开发中的建议
组合使用:它们不是互斥的,可以在同一个项目中组合使用。例如用Grid构建整体页面框架,用Flexbox排列内部组件。
渐进增强:对于简单布局先尝试Flexbox,当需求超出Flexbox能力时再使用Grid。
浏览器支持:
Flexbox有稍好的旧浏览器支持
Grid在现代浏览器中支持良好(IE11有部分支持)
学习曲线:Flexbox通常更容易上手,Grid概念更多但更强大。
维护性:Grid布局通常更易于维护,特别是在需要调整布局结构时。
问:解释BFC(块级格式上下文)及其应用场景?
什么是BFC?
BFC(Block Formatting Context,块级格式化上下文)是Web页面CSS渲染中的一种独立的渲染区域,它规定了内部的块级盒子如何布局,并且与外部环境隔离。
BFC的创建条件:
以下CSS属性可以触发BFC:
根元素(
<html>
)
float
不为none
position
为absolute
或fixed
display
为inline-block
、table-cell
、table-caption
、flex
、inline-flex
、grid
、inline-grid
overflow
不为visible
(常用hidden
或auto
)
contain
为layout
、content
或strict
BFC的特性
内部盒子垂直排列:BFC内的块级盒子会垂直方向一个接一个放置
Margin重叠解决:属于同一个BFC的两个相邻盒子的上下margin会重叠
独立布局环境:BFC内部元素不会影响外部元素
包含浮动元素:计算BFC高度时,浮动元素也参与计算
阻止元素被浮动覆盖:BFC区域不会与浮动元素重叠
应用场景:
- 清除浮动(解决高度塌陷)
<div class="container"> <div class="float-left"></div> </div>
.float-left { float: left; width: 100px; height: 100px; background: red; } /* 未触发BFC时,container高度为0 */ .container { overflow: hidden; /* 触发BFC,包含浮动元素 */ background: #eee; }
- 防止magin重叠
<div class="box1">Box 1</div> <div class="box2">Box 2</div>
.box1, .box2 { margin: 20px 0; background: #ccc; } /* 默认情况下两个盒子的上下margin会合并为20px */ /* 解决方案:用BFC包裹 */ .wrapper { overflow: hidden; /* 触发BFC */ }
- 实现自适应两栏布局
<div class="left">左侧浮动</div> <div class="right">右侧内容</div>
.left { float: left; width: 200px; height: 100px; background: #ccc; } .right { overflow: hidden; /* 触发BFC,不与浮动元素重叠 */ height: 100px; background: #eee; }
- 阻止文本环绕浮动元素
<div class="float-box"></div>
<div class="text-content">...</div>
.float-box {
float: left;
width: 100px;
height: 100px;
background: #ccc;
}
.text-content {
overflow: hidden; /* 触发BFC,文本不再环绕 */
}
BFC的注意事项
性能考量:某些BFC触发方式(如
overflow: hidden
)可能带来副作用(裁剪内容)选择合适方式:优先使用副作用小的方式创建BFC(如
display: flow-root
)现代布局替代:Flexbox和Grid布局自身会创建类似BFC的环境,可以减少显式创建BFC的需求
3.响应式设计
问:如何实现移动端1px边框问题?
在移动端开发中,由于高清屏幕(Retina等)的设备像素比(devicePixelRatio)较高,CSS中的1px实际上可能显示为2px或3px物理像素,导致边框看起来比设计稿更粗。
以下是几种主流解决方案:
- 使用0.5px方案(简单但兼容性有限)
.border { border: 0.5px solid #ccc; }
优缺点:
-
优点:简单直接
-
缺点:iOS8+支持,Android兼容性差
-
- 伪元素 + transform缩放(推荐方案)
.border { position: relative; } .border::after { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 100%; border: 1px solid #ccc; transform-origin: 0 0; transform: scale(0.5); box-sizing: border-box; pointer-events: none; /* 防止点击事件被拦截 */ }
优缺点:
-
优点:兼容性好,效果稳定
-
缺点:代码稍复杂,需要占据伪元素
-
- viewport + rem方案(动态调整)
<meta name="viewport" id="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"> <script> const viewport = document.getElementById("viewport"); if (window.devicePixelRatio === 2) { viewport.setAttribute('content', 'width=device-width,initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no'); } else if (window.devicePixelRatio === 3) { viewport.setAttribute('content', 'width=device-width,initial-scale=0.333333,maximum-scale=0.333333,minimum-scale=0.333333,user-scalable=no'); } </script>
优缺点:
-
优点:全局解决方案
-
缺点:需要调整整个页面的缩放比例,可能影响其他元素
-
- 使用border-image(适用于直线边框)
.border { border-width: 1px; border-style: solid; border-image: linear-gradient(to bottom, #ccc, #ccc) 2 2 stretch; }
- 使用box-shadow模拟(简单边框)
.border { box-shadow: 0 0 0 0.5px #ccc; }
优缺点:
-
优点:代码简单
-
缺点:只能模拟单边边框,圆角支持不好
-
- SVG方案(复杂但精确)
.border { background: url("data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><rect width='100%' height='100%' stroke='%23ccc' stroke-width='1' fill='none'/></svg>"); }
最佳实践建议
通用方案:伪元素+transform方案(方案2)是当前最平衡的解决方案
框架项目:可以使用postcss插件如
postcss-write-svg
或postcss-px-to-viewport
自动处理简单项目:考虑使用0.5px方案,配合渐进增强
特殊需求:圆角边框推荐使用SVG方案
问:rem、vw/vh、媒体查询定的适用场景?
- rem方案
原理:基于根元素(html)的字体大小进行计算,1rem = 根字体大小
适用场景:
整体缩放布局:需要整个页面按比例缩放的场景
固定比例设计:设计稿有固定比例要求时
兼容性要求高的项目(支持到IE9)
移动端H5页面开发(常配合flex布局)
- vw/vh 方案
原理:基于视口尺寸的单位,1vw = 1%视口宽度
,1vh = 1%视口高度
适用场景:
视口比例布局:元素尺寸需要与视口大小直接关联
全屏应用:需要填满整个屏幕的组件
字体自适应:希望文字随视口变化
现代浏览器项目 (IE9部分支持,IE10+完全支持)
- 媒体查询(Media Queries)
原理:根据设备特性(宽度、高度、方向等)应用不同的CSS规则
适用场景:
断点布局:在特定分辨率下改变布局
设备适配:针对不同设备类型(手机/平板/PC)定制样式
显示优化:根据屏幕特性(如打印样式、深色模式)调整
渐进增强:为不同能力设备提供不同体验
现代项目推荐:
主单位:vw/vh + clamp() (如
font-size: clamp(1rem, 2vw, 1.5rem)
)辅助:媒体查询处理特殊断点
备用:rem保证基本可访问性
二、JavaScript 核心
1.基础与原理
问:解释事件循环(Event Loop)机制,结合宏任务/微任务?
什么是事件循环?
事件循环是 JavaScript 实现异步编程的核心机制,它使得 JavaScript 虽然是单线程语言,却能够处理非阻塞 I/O 操作。事件循环负责监控调用栈(Call Stack)、任务队列(Task Queue)和微任务队列(Microtask Queue),决定何时执行什么代码。
事件循环的基本流程:
执行同步代码:所有同步代码在主线程上顺序执行,形成调用栈
处理微任务:当调用栈清空后,事件循环会检查微任务队列并执行所有微任务
渲染页面(如果需要):浏览器可能会进行页面渲染
处理宏任务:从宏任务队列中取出一个任务执行
循环:重复上述过程
宏任务(MacroTask)与微任务(MicroTask):
宏任务
包括:
setTimeout
,setInterval
,setImmediate
(Node.js), I/O 操作, UI 渲染, 事件回调等特点:每次事件循环只执行一个宏任务
微任务
包括:
Promise.then/catch/finally
,process.nextTick
(Node.js),MutationObserver
等特点:每次调用栈清空后,会执行所有微任务队列中的任务
代码执行顺序示例:
console.log('1. 同步代码 - 开始');
setTimeout(() => {
console.log('6. 宏任务 - setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('4. 微任务 - Promise 1');
}).then(() => {
console.log('5. 微任务 - Promise 2');
});
console.log('2. 同步代码 - 中间');
new Promise(resolve => {
console.log('3. 同步代码 - Promise 构造函数');
resolve();
}).then(() => {
console.log('7. 微任务 - Promise 3');
});
console.log('8. 同步代码 - 结束');
输出顺序:
1. 同步代码 - 开始
2. 同步代码 - 中间
3. 同步代码 - Promise 构造函数
8. 同步代码 - 结束
4. 微任务 - Promise 1
5. 微任务 - Promise 2
7. 微任务 - Promise 3
6. 宏任务 - setTimeout
事件循环的详细步骤:
执行所有同步代码,直到调用栈清空
执行所有微任务(直到微任务队列为空)
如有需要,进行 UI 渲染
执行下一个宏任务
重复上述过程
Node.js 与浏览器的事件循环差异 ?
浏览器:微任务在每个宏任务之后执行
Node.js:事件循环分为多个阶段,每个阶段有特定的任务类型,微任务在阶段切换之间执行
为什么需要理解事件循环?
避免阻塞主线程
优化代码执行顺序
解决异步编程中的竞态条件
理解为什么某些代码会以特定顺序执行
掌握事件循环机制是成为高级 JavaScript 开发者的关键一步,它能帮助我们编写更高效、更可预测的异步代码。
问:闭包的实际应用场景及内存泄漏风险?
闭包的核心概念
闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。简单说,当一个函数内部定义了另一个函数,并且内部函数引用了外部函数的变量时,就形成了闭包。
实际应用场景:
1)数据封装与私有变量
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
2)函数工厂
function createMultiplier(multiplier) {
return function(x) {
return x * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3)事件处理与回调
function setupButtons() {
const buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log('Button ' + index + ' clicked');
});
})(i);
}
}
4)模块模式
const myModule = (function() {
const privateVar = 'I am private';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
myModule.publicMethod(); // "I am private"
5)记忆化(Memoization)
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const factorial = memoize(function(n) {
return n <= 1 ? 1 : n * factorial(n - 1);
});
console.log(factorial(5)); // 120 (计算并缓存)
console.log(factorial(5)); // 120 (从缓存读取)
常见的内存泄漏场景:
1)意外的全局变量引用
function leakMemory() {
const bigArray = new Array(1000000).fill('*');
// 内部函数保留了对外部变量的引用
return function() {
console.log('I still have access to bigArray');
};
}
const leakedFn = leakMemory();
// 即使leakedFn不再需要,bigArray也无法被GC回收
2)DOM元素引用
function setup() {
const element = document.getElementById('large-element');
const data = new Array(1000000).fill('*');
element.addEventListener('click', function() {
console.log(data.length); // 闭包保留了data引用
});
}
setup();
// 即使移除DOM元素,由于事件监听器的闭包保留了引用,元素和data都无法被回收
3)定时器未清除
function startProcess() {
const data = new Array(1000000).fill('*');
setInterval(function() {
console.log(data.length); // 闭包保留了data引用
}, 1000);
}
startProcess();
// 即使不再需要data,定时器的回调函数仍然持有引用
如何避免内存泄漏:
1)及时解除引用
function cleanUp() {
const data = new Array(1000000).fill('*');
const element = document.getElementById('my-element');
function handleClick() {
console.log(data.length);
}
element.addEventListener('click', handleClick);
// 需要时移除事件监听
return function() {
element.removeEventListener('click', handleClick);
};
}
const dispose = cleanUp();
// 当不再需要时调用dispose()
2)使用WeakMap/WeakSet
const weakMap = new WeakMap();
function processElement(element) {
const data = new Array(1000000).fill('*');
weakMap.set(element, data);
element.addEventListener('click', function() {
console.log(weakMap.get(element).length);
});
}
const element = document.getElementById('my-element');
processElement(element);
// 当element被移除DOM时,WeakMap中的引用会自动释放
3)避免不必要的闭包
// 不好的做法 - 创建不必要的闭包
function processData(data) {
return function() {
console.log(data);
};
}
// 更好的做法 - 直接传递参数
function createHandler(data) {
return function() {
console.log(data);
};
}
调试内存泄漏:
使用Chrome DevTools的Memory面板
拍摄堆快照比较内存变化
使用Performance Monitor监控内存使用
Node.js中可以使用
--inspect
标志和Chrome DevTools调试
总结:
闭包是JavaScript强大功能之一,合理使用可以实现:
数据封装和私有变量
函数工厂和柯里化
模块化开发
高效的事件处理
但同时需要注意:
避免意外保留对大对象的引用
及时清理事件监听器和定时器
考虑使用WeakMap/WeakSet管理DOM关联数据
定期检查应用的内存使用情况
正确理解和使用闭包,可以让你写出既强大又高效的JavaScript代码。
问:this
的指向问题:call
/apply
/bind
的区别?
this
的基本指向规则:
在 JavaScript 中,
this
的指向取决于函数的调用方式:
全局上下文:在全局作用域中,
this
指向全局对象(浏览器中是window
,Node.js 中是global
)函数调用:普通函数调用时,
this
指向全局对象(严格模式下为undefined
)方法调用:作为对象方法调用时,
this
指向调用该方法的对象构造函数:使用
new
调用时,this
指向新创建的对象箭头函数:箭头函数没有自己的
this
,它继承自外层函数
改变 this
指向的方法:
1).call()方法
call()
方法立即调用函数,并显式设置 this
值和参数列表。
语法:func.call(thisArg, arg1, arg2, ...)
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Alice' };
greet.call(person, 'Hello', '!'); // 输出: "Hello, Alice!"
2).apply()方法
apply()
方法与 call()
类似,但参数以数组形式传递。
语法:func.apply(thisArg, [argsArray])
function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}
const person = { name: 'Bob' };
const args = ['Hi', '!!!'];
greet.apply(person, args); // 输出: "Hi, Bob!!!"
3).bind()方法
bind()
方法创建一个新函数,当调用时,其 this
值设置为提供的值。
语法:func.bind(thisArg[, arg1[, arg2[, ...]]])
function greet(greeting) {
console.log(greeting + ', ' + this.name);
}
const person = { name: 'Charlie' };
const greetPerson = greet.bind(person);
greetPerson('Hey'); // 输出: "Hey, Charlie"
实际应用场景
1)借用方法
// 借用数组的slice方法处理类数组对象
function toArray() {
return Array.prototype.slice.call(arguments);
}
const arr = toArray(1, 2, 3); // [1, 2, 3]
2)函数柯里化(使用 bind)
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
3)事件处理中的 this(使用 bind)
class Button {
constructor() {
this.text = 'Click me';
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.text);
}
}
const btn = new Button();
document.querySelector('button').addEventListener('click', btn.handleClick);
4)找出数组中的最大值(使用 apply)
const numbers = [5, 6, 2, 3, 7];
const max = Math.max.apply(null, numbers); // 7
// ES6中可以更简单地使用: Math.max(...numbers)
注意事项:
严格模式:在严格模式下,如果
thisArg
为null
或undefined
,this
将保持为null
或undefined
,而不会转换为全局对象箭头函数:箭头函数的
this
不能被call
/apply
/bind
改变,它始终指向定义时的上下文性能考虑:在性能敏感代码中,
call
通常比apply
稍快,因为不需要处理数组参数
总结
call
和apply
都是立即调用函数,区别在于参数传递方式
bind
返回一个新函数,适合需要延迟执行或作为回调的场景三者都可以显式设置
this
值,是 JavaScript 中灵活控制执行上下文的重要工具根据具体场景选择合适的方法,可以提高代码的可读性和性能
2.ES6+
问:Promise、async/await的异常处理方式?
Promise 的异常处理:
1)使用 .catch() 的方法
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => console.log(data))
.catch(error => {
console.error('Fetch error:', error);
// 可以在这里进行错误恢复或用户通知
});
2).then()
的第二个参数
fetch('https://api.example.com/data')
.then(
response => response.json(),
error => console.error('Initial fetch failed:', error)
)
.then(data => console.log(data));
3)区别与注意事项
.catch()
会捕获前面所有.then()
中的错误第二个参数错误处理只捕获当前
.then()
之前的错误推荐统一使用
.catch()
以获得更清晰的错误处理流程
async/await 的异常处理
1) try/catch 块
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error in fetchData:', error);
// 错误处理逻辑
}
}
fetchData();
2) 在调用处处理错误
async function fetchData() {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
}
// 在调用async函数的地方处理错误
(async () => {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error('Failed to fetch data:', error);
}
})();
3) 结合 Promise.catch()
async function fetchData() {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
}
fetchData()
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
高级异常处理技巧
1) 全局未捕获的 Promise 异常
// 浏览器环境
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason);
event.preventDefault(); // 阻止默认错误输出
});
// Node.js 环境
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
2) 错误边界模式(React 中的概念)
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
logErrorToService(error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// 使用
<ErrorBoundary>
<MyAsyncComponent />
</ErrorBoundary>
3) 自定义错误类
class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
}
}
async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new ApiError('User not found', response.status);
}
return await response.json();
}
try {
const user = await fetchUser(123);
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.statusCode}: ${error.message}`);
} else {
console.error('Unexpected error:', error);
}
}
4) 并行请求的错误处理
async function fetchMultipleUrls(urls) {
try {
// 使用Promise.allSettled获取所有结果(成功和失败)
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const errors = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
if (errors.length > 0) {
console.warn(`${errors.length} requests failed`);
}
return successful;
} catch (error) {
console.error('Unexpected error in fetchMultipleUrls:', error);
throw error;
}
}
最佳实践:
不要忽略错误:即使只是记录错误,也比完全忽略好
错误分类处理:根据错误类型采取不同的恢复策略
提供有意义的错误信息:帮助调试和用户理解问题
考虑错误恢复:如果可能,提供备用数据或重试机制
全局错误处理:设置全局捕获作为最后防线
日志记录:将错误信息记录到服务器以便分析
总结
Promise:使用
.catch()
或.then()
的第二个参数处理错误async/await:使用 try/catch 块捕获错误
高级技巧:全局错误捕获、自定义错误类、并行请求处理
最佳实践:始终处理错误,提供有意义的反馈,考虑恢复策略
正确的异常处理可以使你的异步代码更健壮、更易于维护,并提供更好的用户体验。
问:箭头函数与普通函数的区别?
1) 语法区别
普通函数:
function sum(a, b) {
return a + b;
}
const sum = function(a, b) {
return a + b;
};
箭头函数:
const sum = (a, b) => {
return a + b;
};
// 简写形式(当函数体只有一条返回语句时)
const sum = (a, b) => a + b;
2) this绑定(最重要区别)
普通函数:
- 有自己的
this
上下文 this
值取决于调用方式(动态绑定)- 可以使用
call
,apply
,bind
改变this
const obj = {
value: 10,
getValue: function() {
return this.value; // this 指向 obj
}
};
箭头函数:
- 没有自己的
this
,继承自外层作用域(词法绑定) this
在定义时确定,不会改变- 不能使用
call
,apply
,bind
改变this
const obj = {
value: 10,
getValue: () => {
return this.value; // this 指向外层作用域(通常是 window)
}
};
3) 构造函数与 new
关键字
普通函数:
- 可以用作构造函数,使用
new
调用 - 会创建新的实例对象
function Person(name) { this.name = name; } const p = new Person('Alice');
箭头函数:
- 不能用作构造函数
- 使用
new
调用会抛出错误const Person = (name) => { this.name = name; // 错误 }; // TypeError: Person is not a constructor
4) arguments
对象
普通函数:
- 有自己的
arguments
对象 - 包含所有传入的参数
function logArgs() { console.log(arguments); } logArgs(1, 2, 3); // Arguments(3) [1, 2, 3]
箭头函数:
- 没有自己的
arguments
对象 - 可以访问外层函数的
arguments
(如果有)const logArgs = () => { console.log(arguments); // 错误:arguments is not defined }; // 替代方案:使用剩余参数 const logArgs = (...args) => { console.log(args); };
5) prototype
属性
普通函数:
- 有
prototype
属性 - 用于实现基于原型的继承
function Person() {} console.log(Person.prototype); // 存在
箭头函数:
- 没有
prototype
属性 - 因为它们不能用作构造函数
const Person = () => {}; console.log(Person.prototype); // undefined
6) yield
关键字
普通函数:
- 可以用作生成器函数(使用
function*
语法) - 可以使用
yield
关键字
箭头函数:
- 不能用作生成器函数
- 不能使用
yield
关键字
7) 方法定义
对象方法:
- 普通函数更适合作为对象方法(需要访问
this
) - 箭头函数会导致
this
指向错误const obj = { value: 10, // 正确方式 getValue: function() { return this.value; }, // 错误方式(this 不会指向 obj) badGetValue: () => { return this.value; } };
8) 事件处理函数
DOM 事件处理:
- 普通函数可以正确绑定事件目标
- 箭头函数会导致
this
指向错误button.addEventListener('click', function() { console.log(this); // 指向 button 元素 }); button.addEventListener('click', () => { console.log(this); // 指向外层作用域(通常是 window) });
9) 简写语法
箭头函数的简洁性:
- 单参数时可以省略括号
- 单表达式时可以省略大括号和
return
const double = x => x * 2; const greet = name => `Hello, ${name}!`;
何时用哪种该函数 ?
使用箭头函数的场景:
需要词法
this
绑定时(如回调函数)需要简洁的匿名函数时
函数不需要自己的
this
、arguments
或作为构造函数时函数式编程(如
map
、filter
、reduce
)使用普通函数的场景:
需要作为构造函数时
需要作为对象方法时
需要访问
arguments
对象时需要动态
this
绑定时需要生成器函数时
问:实现一个简单的发布-订阅模式
发布-订阅模式(Pub/Sub)是一种消息通信模式,用于实现对象间的松耦合通信。下面是几种实现方式:
1) 基础实现
class EventEmitter {
constructor() {
this.events = {}; // 存储事件及回调
}
// 订阅事件
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
return this; // 支持链式调用
}
// 发布事件
emit(eventName, ...args) {
const callbacks = this.events[eventName];
if (callbacks) {
callbacks.forEach(cb => cb.apply(this, args));
}
return this; // 支持链式调用
}
// 取消订阅
off(eventName, callback) {
const callbacks = this.events[eventName];
if (callbacks) {
if (callback) {
// 移除特定回调
this.events[eventName] = callbacks.filter(cb => cb !== callback);
} else {
// 移除所有回调
delete this.events[eventName];
}
}
return this; // 支持链式调用
}
// 一次性订阅
once(eventName, callback) {
const wrapper = (...args) => {
callback.apply(this, args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
return this;
}
}
使用示例:
const emitter = new EventEmitter();
// 订阅
const callback1 = data => console.log('Callback 1:', data);
emitter.on('event', callback1);
// 一次性订阅
emitter.once('event', data => console.log('One-time:', data));
// 发布
emitter.emit('event', { message: 'Hello' });
// 输出:
// Callback 1: {message: "Hello"}
// One-time: {message: "Hello"}
// 再次发布
emitter.emit('event', { message: 'World' });
// 输出:
// Callback 1: {message: "World"}
// 取消订阅
emitter.off('event', callback1);
2) 支持命名空间的实现
class AdvancedEventEmitter {
constructor() {
this.events = {};
this.maxListeners = 10; // 默认最大监听器数量
}
// 添加监听器
on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 检查监听器数量限制
if (this.events[eventName].length >= this.maxListeners) {
console.warn(`Possible memory leak detected. ${this.events[eventName].length} listeners added for event "${eventName}".`);
}
this.events[eventName].push(listener);
return this;
}
// 触发事件
emit(eventName, ...args) {
const listeners = this.events[eventName];
if (listeners) {
listeners.forEach(listener => {
try {
listener.apply(this, args);
} catch (err) {
console.error(`Error in listener for event "${eventName}":`, err);
}
});
}
return this;
}
// 移除监听器
off(eventName, listenerToRemove) {
if (!this.events[eventName]) return this;
this.events[eventName] = this.events[eventName].filter(
listener => listener !== listenerToRemove
);
return this;
}
// 一次性监听器
once(eventName, listener) {
const onceWrapper = (...args) => {
listener.apply(this, args);
this.off(eventName, onceWrapper);
};
return this.on(eventName, onceWrapper);
}
// 设置最大监听器数量
setMaxListeners(n) {
this.maxListeners = n;
return this;
}
// 获取所有监听器
listeners(eventName) {
return this.events[eventName] || [];
}
// 移除所有监听器
removeAllListeners(eventName) {
if (eventName) {
delete this.events[eventName];
} else {
this.events = {};
}
return this;
}
}
3)更简洁的函数实现方式
function createPubSub() {
const subscribers = {};
function subscribe(eventName, callback) {
if (!subscribers[eventName]) {
subscribers[eventName] = new Set();
}
const callbacks = subscribers[eventName];
callbacks.add(callback);
return () => {
callbacks.delete(callback);
if (callbacks.size === 0) {
delete subscribers[eventName];
}
};
}
function publish(eventName, data) {
if (subscribers[eventName]) {
subscribers[eventName].forEach(callback => {
callback(data);
});
}
}
return {
subscribe,
publish,
};
}
// 使用示例
const { subscribe, publish } = createPubSub();
// 订阅
const unsubscribe = subscribe('message', data => {
console.log('Received:', data);
});
// 发布
publish('message', 'Hello World!');
// 取消订阅
unsubscribe();
4)支持异步事件的实现
class AsyncEventEmitter {
constructor() {
this.events = {};
}
async on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(listener);
}
async emit(eventName, ...args) {
const listeners = this.events[eventName];
if (listeners) {
for (const listener of listeners) {
try {
await listener(...args);
} catch (err) {
console.error(`Error in async listener for "${eventName}":`, err);
}
}
}
}
async off(eventName, listenerToRemove) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
listener => listener !== listenerToRemove
);
}
}
}
// 使用示例
const asyncEmitter = new AsyncEventEmitter();
asyncEmitter.on('data', async data => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log('Processed data:', data);
});
asyncEmitter.emit('data', { id: 1, value: 'test' });
选择建议:
基础需求:使用第一种基础实现即可满足大多数场景
生产环境:建议使用第二种更健壮的实现
函数式偏好:第三种实现更简洁
异步事件:第四种实现支持异步监听器
发布-订阅模式的优点:
- 解耦发布者和订阅者
- 支持一对多的消息通信
- 动态添加和移除订阅关系
实际应用场景:
- 组件间通信
- 插件系统
- 状态管理
- 事件驱动的架构
3.手写代码
问:防抖(Debounce)与节流(Throttle)的实现?
1) 防抖实现代码:
防抖的核心思想是:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
/**
* 防抖函数
* @param {Function} fn 需要防抖的函数
* @param {number} delay 延迟时间(毫秒)
* @param {boolean} immediate 是否立即执行
* @return {Function} 返回防抖后的函数
*/
function debounce(fn, delay = 300, immediate = false) {
let timer = null;
return function(...args) {
const context = this;
// 清除已有的定时器
if (timer) {
clearTimeout(timer);
}
// 立即执行模式
if (immediate && !timer) {
fn.apply(context, args);
}
// 设置新的定时器
timer = setTimeout(() => {
timer = null;
if (!immediate) {
fn.apply(context, args);
}
}, delay);
};
}
/*使用:*/
// 普通防抖
const debouncedFn = debounce(() => {
console.log('窗口大小改变了');
}, 500);
window.addEventListener('resize', debouncedFn);
// 立即执行防抖
const debouncedSearch = debounce(function(query) {
console.log('搜索:', query);
}, 1000, true);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
2) 节流实现代码:
节流的核心思想是:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
/**
* 节流函数
* @param {Function} fn 需要节流的函数
* @param {number} delay 节流时间间隔(毫秒)
* @param {boolean} trailing 是否在节流结束后执行最后一次调用
* @return {Function} 返回节流后的函数
*/
function throttle(fn, delay = 300, trailing = true) {
let lastTime = 0;
let timer = null;
return function(...args) {
const context = this;
const now = Date.now();
// 清除trailing的定时器
if (timer) {
clearTimeout(timer);
timer = null;
}
// 如果距离上次执行时间超过delay,立即执行
if (now - lastTime >= delay) {
fn.apply(context, args);
lastTime = now;
}
// trailing模式,设置定时器在delay后执行
else if (trailing) {
timer = setTimeout(() => {
fn.apply(context, args);
lastTime = Date.now();
timer = null;
}, delay - (now - lastTime));
}
};
}
/* 使用示例 */
// 基本节流
const throttledScroll = throttle(() => {
console.log('滚动事件处理');
}, 200);
window.addEventListener('scroll', throttledScroll);
// 带trailing的节流
const throttledResize = throttle(() => {
console.log('窗口大小改变处理');
}, 500, true);
window.addEventListener('resize', throttledResize);
实际应用建议
- 搜索建议:使用防抖,等待用户停止输入后再发送请求
- 无限滚动:使用节流,定期检查滚动位置
- 窗口调整:根据需求选择 - 如果只需要最终状态用防抖,如果需要实时调整用节流
- 游戏开发:玩家移动通常使用节流保证流畅体验
问:深拷贝函数(考虑循环引用)?
以下是一个健壮的深拷贝函数实现,可以处理各种数据类型(包括 Date、RegExp、Map、Set 等)和循环引用情况:
/**
* 深拷贝函数
* @param {*} target 要拷贝的目标值
* @param {WeakMap} [map] 用于存储已拷贝对象的WeakMap(处理循环引用)
* @return {*} 拷贝后的值
*/
function deepClone(target, map = new WeakMap()) {
// 处理基本数据类型和null/undefined
if (target === null || typeof target !== 'object') {
return target;
}
// 处理循环引用
if (map.has(target)) {
return map.get(target);
}
// 处理Date对象
if (target instanceof Date) {
return new Date(target);
}
// 处理RegExp对象
if (target instanceof RegExp) {
return new RegExp(target);
}
// 处理Map对象
if (target instanceof Map) {
const cloneMap = new Map();
map.set(target, cloneMap);
target.forEach((value, key) => {
cloneMap.set(deepClone(key, map), deepClone(value, map));
});
return cloneMap;
}
// 处理Set对象
if (target instanceof Set) {
const cloneSet = new Set();
map.set(target, cloneSet);
target.forEach(value => {
cloneSet.add(deepClone(value, map));
});
return cloneSet;
}
// 处理数组和普通对象
const cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
// 使用Reflect.ownKeys获取所有自有属性(包括Symbol)
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], map);
});
return cloneTarget;
}
使用示例:
// 测试循环引用
const obj = {
a: 1,
b: {
c: 2,
d: [3, 4]
}
};
obj.circularRef = obj; // 循环引用
// 测试特殊对象
const testObj = {
date: new Date(),
regex: /abc/gi,
map: new Map([['key1', 'value1'], ['key2', obj]]),
set: new Set([1, 2, 3, obj]),
[Symbol('unique')]: 'symbolValue'
};
const clonedObj = deepClone(testObj);
console.log(clonedObj);
console.log(clonedObj !== testObj); // true
console.log(clonedObj.map !== testObj.map); // true
console.log(clonedObj.circularRef === clonedObj); // true (循环引用保持)
实现说明
基本数据类型:直接返回,不需要拷贝
循环引用处理:使用 WeakMap 存储已拷贝对象,遇到相同引用时直接返回
特殊对象处理:
Date 对象:创建新的 Date 实例
RegExp 对象:创建新的 RegExp 实例
Map/Set:递归拷贝其内容
数组和对象:
区分数组和普通对象
使用 Reflect.ownKeys 获取所有自有属性(包括 Symbol)
递归拷贝每个属性
性能考虑:
使用 WeakMap 而不是 Map,避免内存泄漏
只在必要时创建新对象
注意事项
该实现不处理函数(函数通常不需要深拷贝)
不处理 DOM 节点(DOM 节点通常也不应该被深拷贝)
不处理 Buffer、ArrayBuffer 等二进制数据
对于非常复杂的对象结构,可能需要特殊处理