提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
文章目录
前言
一、::before 和 :after中双冒号和单冒号 有什么区别?
单冒号与双冒号的差
二、清除浮动的方式都有哪些,优缺点是什么?
1. 空div清除法
2. 父元素设置overflow属性
3. 使用伪元素清除浮动(推荐)
4. 父元素浮动
5. 使用display: flow-root
三、垂直水平居中的几种方式?
1.使用 Flexbox 布局
2.使用 Grid 布局
3.使用绝对定位和 transform
4.使用绝对定位和 margin
5.使用 line-height
四、盒模型的理解,什么是标准盒模型,什么是怪异盒模型?
标准盒模型(W3C盒模型)
怪异盒模型(IE盒模型)
五、事件代理是什么?冒泡和捕获?
事件冒泡
事件捕获
完整事件流顺序
六、如何解决跨域问题?
跨域问题的原因
JSONP方式
CORS(跨域资源共享)
代理服务器
WebSocket协议
postMessage API
修改document.domain
注意事项
七、for循环和 forEach循环,哪个性能更好?
for循环与forEach循环的性能对比
原生for循环
Array.prototype.forEach
性能测试数据
其他考虑因素
结论
八、变量提升的理解?
变量提升的概念
变量声明的提升
函数声明的提升
let和const的暂时性死区
函数表达式与提升
九、防抖和节流的理解?
防抖(Debounce)
节流(Throttle)
区别对比
十、JavaScript原型,原型链
JavaScript原型与原型链的概念
原型(Prototype)
原型链(Prototype Chain)
原型链的继承机制
原型链的终点
修改原型的影响
原型链与性能
原型链的应用场景
注意事项
总结
前言
希望可以帮助到正在面试前端工作岗位的大神们
提示:以下是本篇文章正文内容,下面案例可供参考
一、::before 和 :after中双冒号和单冒号 有什么区别?
::before/::after 里双冒号(::)和单冒号(:)的核心区别,这其实是 CSS 不同版本规范的语法差异问题。
单冒号与双冒号的差
单冒号(:)是CSS2中引入的伪元素和伪类语法,而双冒号(::)是CSS3中新增的语法,用于明确区分伪元素和伪类。CSS3规范建议伪元素使用双冒号,伪类仍使用单冒号。
伪元素(如::before、::after)表示DOM中实际不存在的虚拟元素,用于插入内容或样式。伪类(如:hover、:active)描述元素的特定状态或特征。
现代浏览器普遍支持双冒号语法,但旧版浏览器(如IE8及以下)仅支持单冒号。若需兼容旧浏览器,可统一使用单冒号,但新项目中推荐使用双冒号以符合规范。
.element::before {
content: "前缀";
}
.element:hover {
color: red; /* 伪类仍用单冒号 */
}
若需兼容旧代码或环境,可暂时保留单冒号,但新代码应遵循CSS3标准。两者功能相同,双冒号更清晰地表达语义。
二、清除浮动的方式都有哪些,优缺点是什么?
1. 空div清除法
在浮动元素后添加一个空的div,并设置clear: both;样式。
<div style="clear: both;"></div>
优点:简单直接,兼容性好。 缺点:增加无意义的空标签,破坏HTML结构
2. 父元素设置overflow属性
为父元素设置overflow: hidden;或overflow: auto;。
.parent {
overflow: hidden;
}
优点:代码简洁,无需额外标签。 缺点:可能隐藏内容或触发滚动条。
3. 使用伪元素清除浮动(推荐)
通过::after伪元素为父元素添加清除浮动样式。
.clearfix::after {
content: "";
display: block;
clear: both;
visibility: hidden;
height: 0;
}
.clearfix {
*zoom: 1; /* 兼容IE6/7 */
}
优点:语义化好,无冗余标签。 缺点:代码稍复杂,需处理旧浏览器兼容性。
4. 父元素浮动
将父元素也设置为浮动
.parent {
float: left;
}
优点:简单易用。 缺点:可能影响布局,需额外处理父元素的浮动。
5. 使用display: flow-root
现代CSS方案,为父元素设置display: flow-root。
.parent {
display: flow-root;
}
优点:无副作用,语义明确。 缺点:兼容性较差(不支持IE及旧版浏览器)
三、垂直水平居中的几种方式?
1.使用 Flexbox 布局
通过 Flexbox 可以轻松实现元素的垂直水平居中。设置父容器为 Flex 布局,并使用 justify-content 和 align-items 属性。
.parent {
display: flex;
justify-content: center;
align-items: center;
}
2.使用 Grid 布局
Grid 布局同样支持快速居中,通过设置父容器为 Grid 并利用 place-items 属性。
.parent {
display: flex;
justify-content: center;
align-items: center;
}
3.使用绝对定位和 transform
结合绝对定位和 transform 属性,通过调整元素的偏移实现居中。
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
4.使用绝对定位和 margin
在已知子元素宽高的情况下,可以通过绝对定位和负边距实现居中。
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
5.使用 line-height
对于单行文本或行内元素,可以通过设置 line-height 等于容器高度实现垂直居中。
.parent {
height: 200px;
line-height: 200px;
text-align: center;
}
.child {
display: inline-block;
line-height: normal;
vertical-align: middle;
}
四、盒模型的理解,什么是标准盒模型,什么是怪异盒模型?
标准盒模型(W3C盒模型)
- 宽度和高度的计算方式:元素的width和height属性仅定义内容区域(content)的尺寸。
- 总尺寸计算: 总宽度 = width + padding-left + padding-right + border-left + border-right + margin-left + margin-right 总高度同理。
- 触发方式:默认情况下或通过box-sizing: content-box显式声明。
怪异盒模型(IE盒模型)
- 宽度和高度的计算方式:元素的width和height属性包含内容区域、内边距和边框的尺寸。
- 总尺寸计算: 总宽度 = width + margin-left + margin-right 总高度同理(内边距和边框不额外增加总尺寸)。
- 触发方式:通过box-sizing: border-box显式声明。
五、事件代理是什么?冒泡和捕获?
事件代理是一种利用事件冒泡机制将子元素的事件处理委托给父元素的编程模式。通过将事件监听器绑定到父元素,可以动态管理子元素的事件,无需为每个子元素单独绑定监听器。
优点包括减少内存占用(尤其适合大量子元素场景)、动态新增子元素无需重新绑定事件、代码更简洁。
实现方式:父元素通过event.target识别实际触发事件的子元素,再执行对应逻辑。例如:
document.getElementById('parent').addEventListener('click', function(event) {
if (event.target.matches('button.child')) {
console.log('子元素被点击');
}
});
事件冒泡
事件冒泡是事件传播的默认阶段,事件从触发的最深层元素向上层祖先元素逐级传播。例如点击<button>时,事件会依次触发:button → div → body → html → document。
阻止冒泡:调用event.stopPropagation()可中断事件向上传播。
事件捕获
事件捕获是事件传播的早期阶段,与冒泡方向相反。事件从最外层祖先元素向下级子元素传播,直到到达实际触发元素。现代浏览器默认不启用捕获阶段。
启用捕获:在addEventListener中设置第三个参数为true:
element.addEventListener('click', handler, true);
完整事件流顺序
通过理解这两种机制,可以更灵活地控制事件处理逻辑。例如表单验证适合用捕获阶段提前拦截,而动态内容管理更适合用冒泡阶段的事件代理。
六、如何解决跨域问题?
跨域问题的原因
跨域问题源于浏览器的同源策略,该策略限制了不同源(协议、域名、端口)之间的资源交互。常见的场景包括前端调用不同域的API或加载第三方资源。
JSONP方式
JSONP利用<script>标签不受同源策略限制的特性,通过动态创建脚本标签实现跨域请求。服务器返回的数据需包装在回调函数中。 示例代码:
function handleResponse(data) {
console.log(data);
}
const script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';
document.body.appendChild(script);
局限性:仅支持GET请求,且存在安全风险。
CORS(跨域资源共享)
服务端设置响应头允许跨域访问:
- Access-Control-Allow-Origin: * 允许所有域
- Access-Control-Allow-Methods: GET, POST, PUT 指定允许的HTTP方法
- Access-Control-Allow-Headers: Content-Type 允许的请求头
Node.js示例:
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
代理服务器
通过同域的后端服务转发请求到目标服务器,绕过浏览器限制。 Nginx配置示例:
location /api {
proxy_pass http://target-server.com;
proxy_set_header Host $host;
}
WebSocket协议
WebSocket不受同源策略限制,适用于实时通信场景。 前端示例:
const socket = new WebSocket('ws://example.com/socket');
socket.onmessage = (event) => {
console.log(event.data);
};
postMessage API
适用于窗口间通信,如iframe与父页面跨域交互。 父页面代码:
window.frames[0].postMessage('data', 'http://child-domain.com');
子页面监听:
window.addEventListener('message', (event) => {
if (event.origin === 'http://parent-domain.com') {
console.log(event.data);
}
});
修改document.domain
适用于主域相同子域不同的场景,需双方页面设置相同的主域:
document.domain = 'example.com';
注意事项
- CORS需服务端配合,适用于现代浏览器
- JSONP适合旧浏览器但不安全
- 生产环境推荐使用CORS或代理服务器
- 涉及敏感操作时需严格限制Access-Control-Allow-Origin
七、for循环和 forEach循环,哪个性能更好?
for循环与forEach循环的性能对比
在JavaScript中,for循环和forEach循环的性能差异主要取决于使用场景和数据规模。以下是两者的对比分析:
原生for循环
- 性能优势:原生for循环(尤其是传统的for (let i = 0; i < array.length; i++))通常性能更高,因为它直接通过索引访问数组元素,没有额外的函数调用开销。
- 适用场景:适合处理大规模数据或对性能要求严格的场景(如游戏、高频计算等)。
- 代码示例: const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
Array.prototype.forEach
- 性能劣势:forEach是数组的高阶函数,每次迭代会调用一个回调函数,产生额外的函数调用开销,因此在性能上略逊于原生for循环。
- 适用场景:适合代码可读性优先的场景,或需要链式调用其他数组方法(如map、filter)时。
- 代码示例: const arr = [1, 2, 3];
arr.forEach(item => console.log(item));
性能测试数据
- 在大多数基准测试中,for循环比forEach快约 20%-30%(具体差异取决于JavaScript引擎优化)。
- 现代引擎(如V8)对forEach有优化,但在极端性能敏感场景下仍推荐使用for循环。
其他考虑因素
- 可读性:forEach更符合函数式编程风格,代码更简洁。
- 中断循环:for循环可以通过break或return提前终止,而forEach无法中断(除非抛出异常)。
结论
- 性能优先:选择原生for循环。
- 可读性优先:选择forEach。
- 对于小型数组或非性能关键场景,两者差异可忽略。
八、变量提升的理解?
变量提升的概念
变量提升(Hoisting)是JavaScript中的一种机制,指在代码执行前,JavaScript引擎会将变量和函数的声明部分提升到当前作用域的顶部。需要注意的是,只有声明会被提升,而赋值操作不会提升。
变量声明的提升
对于使用var声明的变量,变量名会被提升到作用域顶部,但赋值仍保留在原有位置。例如:
console.log(a); // 输出undefined,而非报错
var a = 10;
实际执行顺序相当于:
var a; // 声明提升
console.log(a); // 此时a未赋值
a = 10; // 赋值保留原位
函数声明的提升
函数声明会整体提升,包括函数体。因此可以在声明前调用函数:
foo(); // 输出"Hello"
function foo() {
console.log("Hello");
}
let和const的暂时性死区
使用let和const声明的变量也存在提升,但不会初始化为undefined,在声明前访问会触发“暂时性死区”(TDZ)错误:
console.log(b); // 报错:Cannot access 'b' before initialization
let b = 20;
函数表达式与提升
函数表达式遵循变量提升规则。若使用var声明,变量名提升但函数体不提升:
bar(); // 报错:bar is not a function
var bar = function() {
console.log("World");
};
九、防抖和节流的理解?
防抖(Debounce)
防抖是一种技术,用于限制函数在短时间内被频繁触发。其核心思想是:在事件被触发后,等待一段时间再执行函数。如果在等待期间事件再次被触发,则重新计时。
适用场景:输入框实时搜索、窗口大小调整等高频触发但需要稳定后执行的操作。
实现原理:
- 设置一个定时器,延迟执行目标函数。
- 每次触发事件时,清除之前的定时器并重新设置。
- 只有最后一次触发后等待时间结束,函数才会执行。
示例代码(JavaScript):
function debounce(func, delay) {
let timeoutId;
return function(…args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
节流(Throttle)
节流是一种技术,用于确保函数在一定时间内只执行一次。其核心思想是:无论事件触发多么频繁,函数都会按照固定的时间间隔执行。
适用场景:滚动事件、鼠标移动事件等需要均匀执行的操作。
实现原理:
- 记录上一次函数执行的时间戳。
- 每次触发事件时,检查当前时间与上次执行时间的差值是否大于设定的间隔。
- 如果满足条件,执行函数并更新上次执行时间。
示例代码(JavaScript):
function throttle(func, interval) {
let lastTime = 0;
return function(…args) {
const now = Date.now();
if (now – lastTime >= interval) {
func.apply(this, args);
lastTime = now;
}
};
}
区别对比
- 防抖:关注最后一次触发,适合处理高频但只需最终结果的场景。
- 节流:关注均匀执行,适合处理高频且需定期反馈的场景。
实际选择时需根据具体需求决定使用哪种技术。
十、JavaScript原型,原型链
JavaScript原型与原型链的概念
在JavaScript中,每个对象都有一个原型(prototype),原型本身也是一个对象。原型链是由对象的原型组成的链式结构,用于实现继承和属性查找。
原型(Prototype)
每个JavaScript对象(除null外)都有一个__proto__属性(非标准,但被广泛支持),指向其原型对象。构造函数(如function Person() {})的prototype属性是其实例的原型。
function Person() {}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
原型链(Prototype Chain)
当访问对象的属性时,如果对象本身没有该属性,JavaScript会沿着原型链向上查找,直到找到该属性或到达原型链的顶端(Object.prototype.__proto__为null)。
function Person() {}
Person.prototype.name = "Alice";
const person = new Person();
console.log(person.name); // "Alice"(从原型继承)
原型链的继承机制
通过修改构造函数的prototype属性,可以实现继承。子类的原型指向父类的实例,从而形成原型链。
function Parent() {
this.parentProp = "Parent";
}
function Child() {
this.childProp = "Child";
}
Child.prototype = new Parent(); // 继承
const child = new Child();
console.log(child.parentProp); // "Parent"(通过原型链继承)
原型链的终点
所有对象的原型链最终指向Object.prototype,而Object.prototype.__proto__为null,表示原型链的终点。
console.log(Object.prototype.__proto__); // null
修改原型的影响
修改原型会影响所有基于该原型的实例。动态修改原型会导致已创建的实例也能访问新增的属性或方法。
function Person() {}
const person1 = new Person();
Person.prototype.sayHello = function() {
console.log("Hello");
};
person1.sayHello(); // "Hello"(动态添加的方法)
原型链与性能
频繁的原型链查找可能影响性能。应尽量避免过长的原型链,或使用Object.hasOwnProperty检查属性是否存在。
const obj = { key: "value" };
console.log(obj.hasOwnProperty("key")); // true
console.log(obj.hasOwnProperty("toString")); // false(继承自Object.prototype)
原型链的应用场景
Array.prototype.customMethod = function() {
console.log("Custom method");
};
const arr = [1, 2, 3];
arr.customMethod(); // "Custom method"
注意事项
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
总结
这就是今日分享的十道经典且常见的前端面试题,以后每天我都会按时更新,大家多多关注!




