如果你也对春招感兴趣,欢迎加我的个人微信:
SanL1ng
,我们有一个春招面试交流群,可以进群讨论大家面试过程中遇到的问题,我们一起解决。
前言
在一个疯狂星期四的下午两点,海投了一下滴滴,很快阿两点半就要了简历,但是当时的感受是会像大部分大厂一样,要了简历后就石沉大海了无音讯了,原本以为这个亦是如此,结果出乎意料的在四点中收到了人事的约面,时间就在下周二晚….
于是,激动的心颤抖的手开启了这一次模拟面经的过程,主要内容来源于请教之前的学长们的经历,以及自己最近面试过程中经常遇到或者不太熟悉的知识点,其中Vue内容太多就先不加了,这篇文章侧重于的就是CSS/JS/手写,仅供参考,具体内容还等我下周二的面经,大佬们轻喷~
CSS
聊聊网格布局(Grid Layout)和响应式布局(Responsive Layout)
网格布局(Grid Layout)
是什么:
网格布局
是一种二维布局系统,将页面划分为行
和列
,使得元素可以放置在这个二维网格中的任意位置。
用法:
- 使用
display: grid;
属性将一个元素设为网格容器。- 使用
grid-template-rows
和grid-template-columns
属性定义网格的行和列。- 使用
grid-template-areas
属性定义网格区域,方便地放置元素。- 使用
grid-gap
属性定义网格行和列的间距。
使用场景:
水平和垂直居中
嵌套网格
自适应容器大小
响应式布局(Responsive Layout)
是什么:
响应式布局
是一种设计方法
,通过使用媒体查询(Media Queries)
、相对单位
和弹性布局
等技术,可以实现网页的响应式调整
。
用法:
- 使用
媒体查询(Media Queries)
来根据设备的特性和屏幕尺寸应用不同的 CSS 样式。- 使用
相对单位(如百分比、vw、vh)
和弹性布局(如 Flexbox、Grid)
来实现页面元素的自适应和响应式排列。
元素隐藏的方式,有什么区别?
display:none
脱离文档流 无法响应事件 回流重绘
visibility:hidden
占据文档流 无法响应事件 重绘
opacity:0
占据文档流 响应事件 重绘 || 不回流
position:absolute
脱离文档流 无法响应事件 回流重绘
clip-path: circle(0)
占据文档流 无法响应事件 重绘
flex 布局
是什么
当谈到Flex布局时,我感觉它是一种非常强大而且灵活的布局方式。我个人非常喜欢使用Flex布局的一个原因就是它在各种屏幕尺寸下都能够自适应。
在Flex布局中,父容器被称为Flex容器,其中的子元素被称为Flex项。 通过在父容器上设置display
属性为"flex"
,就可以启用Flex布局。
用法
- 主轴和交叉轴:Flexbox 布局存在主轴和交叉轴,主轴决定了项目的排列方向,而交叉轴则与主轴垂直。
- 弹性容器属性:通过设置容器的属性,如
flex-direction
、justify-content
和align-items
,可以控制项目在容器内的排列和对齐方式。 - 弹性项目属性:通过设置项目的属性,如
flex-grow
、flex-shrink
和flex-basis
,可以控制项目的大小和行为。
水平垂直居中的方式
- position:
absolute + translate || margin负值(已知宽高)
- flex:
justify-content: center + align-items: center
- grid:
justify-content: center + align-items: center
- table-cell:
text-align: center + vertical-align: middle;
(子容器不能是块级)- margin:负值(已知宽高,但是通常不建议这样使用)
伪元素和伪类
伪元素(Pseudo-elements)
伪元素让我感觉自己就像是在给元素“增加子女”一样。比如说,我可以在一个元素前面或后面添加一段文字,或者插入一张图片,而这些都不需要我去修改HTML代码。通常使用::
双冒号来表示伪元素,常见的伪元素包括:
::before
:在元素内容之前插入虚拟内容。::after
:在元素内容之后插入虚拟内容。::first-line
:选择元素的第一行文本。::first-letter
:选择元素的第一个字母。
示例
/* 在段落前插入引用图标 */
p::before {
content: url('quote.png');
}
/* 为每个列表项前添加编号 */
li::before {
counter-increment: item;
content: counter(item) ".";
}
伪类(Pseudo-classes)
伪类让我觉得自己像是在给元素“打标签”,告诉它们处于的状态或位置。比如说,我可以用伪类来让鼠标悬停在链接上时改变链接的颜色,或者让点击按钮时它的背景颜色发生变化。使用单冒号(:
)来表示伪类,常见的伪类包括:
:hover
:鼠标悬停在元素上时应用的样式。:active
:元素被激活时(通常是鼠标点击)应用的样式。:first-child
:选择作为父元素的第一个子元素的元素。:nth-child()
:选择指定位置的子元素。:not()
:排除特定选择器匹配的元素。
示例
/* 悬停在链接上时改变颜色 */
a:hover {
color: #ff0000;
}
/* 点击按钮时改变背景颜色 */
button:active {
background-color: #00ff00;
}
/* 选中表格的奇数行并设置背景颜色 */
tr:nth-child(odd) {
background-color: #f0f0f0;
}
如何实现九宫格布局
这里我想到的方法有三种,其中最常见也是我认为最好的一种就是设置display:grid
,通过网格布局来实现,还有两种就是通过弹性布局
和浮动
实现。
首先,假设我有九个元素在页面上:
<div class="container">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div class="item">6</div>
<div class="item">7</div>
<div class="item">8</div>
<div class="item">9</div>
</div>
网格布局
.container {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 将容器划分为三列 */
grid-gap: 10px; /* 设置列之间的间隔 */
}
.item {
background-color: #ccc; /* 设置背景颜色 */
padding: 20px; /* 设置内边距 */
text-align: center; /* 设置文本居中 */
}
因为网格布局允许我们设置容器划分为3列,并且每列都是等分的,然后当我们放入九个元素就可以实现九宫格,很简单并且也符合响应式布局。
弹性布局
.container {
display: flex;
flex-wrap: wrap; /* 设置自动换行 */
}
.item {
flex-basis: calc(33.333% - 20px); /* 设置每个项的宽度为三分之一,并减去间距 */
background-color: #ccc;
padding: 20px;
box-sizing: border-box; /* 盒模型设置为 border-box */
margin: 10px; /* 设置间距 */
text-align: center;
}
当我们设置父容器为弹性容器,并且设置子元素自动换行,然后设置每个子元素的宽度和间距就可以实现九宫格,不过这需要我们设置盒子模型为IE盒模型这样才可以让我们设置的子元素的宽度为准确宽度,并且需要我们额外在设置宽度的时候减去边距,所以有点麻烦。
浮动
.item {
width: calc(33.333% - 20px); /* 设置每个项的宽度为三分之一,并减去间距 */
float: left; /* 设置浮动 */
background-color: #ccc;
padding: 20px;
box-sizing: border-box; /* 盒模型设置为 border-box */
margin: 10px; /* 设置间距 */
text-align: center;
}
我们只需要让每个子元素浮动脱离文档流后,设置他们的宽度和边距也可以实现九宫格的效果,缺点也是有点麻烦并且还需要考虑可能造成父容器高度塌陷等问题,还需要后续做清除浮动处理。
JS
闭包
是什么
根据JS的词法作用域规则,内部函数是可以访问其外部函数的变量,当一个函数的内部函数用到其外部函数的变量时,如果外部函数之外还用到了这些变量时,就算外部函数执行完毕后被移除调用栈,这些变量仍然会被保存在内存中,这就是闭包。
作用以及缺点
闭包可以实现一个
变量私有化
、延长变量的生命周期
等等优点,然后大家经常会说闭包一定会造成资源泄露
,但是当我们合理使用闭包,手动释放内存,通过WeakSet
|WeakMap
来合理声明变量等操作也可以避免内存泄露的缺点。
使用场景
- 封装私有变量和方法: 闭包可以用于创建私有作用域,从而封装变量和方法,防止它们被外部访问和修改。这在模块化开发中特别有用,可以避免全局命名空间的污染。
function createCounter() {
let count = 0; // 私有变量
return function() {
count++;
console.log(count);
}
}
const counter = createCounter();
counter(); // 输出:1
counter(); // 输出:2
- 广告弹窗的 Z-Index 管理: 当页面中有多个弹窗或者浮动广告时,需要动态管理它们的 Z-Index 以确保正确的叠放顺序。使用闭包可以在弹窗的事件处理程序中保存当前最高的 Z-Index 值,并且每次新弹窗出现时递增该值。
const createPopup = () => {
let zIndex = 1000; // 初始 Z-Index 值
return () => {
const popup = document.createElement('div');
popup.style.zIndex = zIndex++;
// 其他弹窗相关代码...
document.body.appendChild(popup);
};
};
const popupManager = createPopup();
document.getElementById('openPopupBtn').addEventListener('click', popupManager);
- 模块化开发: 在大型应用程序中,模块化开发是必不可少的,闭包可以用于创建私有的作用域和封装变量,防止它们被全局污染。
const module = (() => {
let privateVariable = 0;
const privateFunction = () => {
// 私有方法实现...
};
return {
publicFunction: () => {
// 对外暴露的公共方法...
}
};
})();
箭头函数
是什么
箭头函数是
ES6
新引入的一种函数声明语法
,主要是提供了一种更加简洁
的函数书写方式
,通常情况下,我们不会为其具名,也就是一个匿名函数,不过我们也可以为其具名使其成为一个具名箭头函数。
语法
箭头函数的语法相对于传统的函数声明更加简洁明了,尤其是在回调函数或者单行函数表达式的情况下。它使用
=>
符号来表示函数定义。
// 传统函数声明
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
特点
箭头函数没有
this
绑定
箭头函数没有自己的 this
绑定,它会继承外部作用域中的 this
值。这一点与传统的函数表达式有所不同,传统的函数表达式中的 this
值取决于函数是如何被调用的。
const obj = {
value: 42,
getValue: function() {
return this.value;
},
getValueArrow: () => {
return this.value; // 这里的 this 指向的是外部的全局对象,而不是 obj
}
};
console.log(obj.getValue()); // 输出:42
console.log(obj.getValueArrow()); // 输出:undefined
适用于简短的单行函数
箭头函数特别适合于编写简短的、单行的函数,它们提供了更紧凑的语法。
// 传统函数声明
const double = function(x) {
return x * 2;
};
// 箭头函数
const double = x => x * 2;
箭头函数不能作为构造函数
与传统函数不同,箭头函数不能用作构造函数来创建对象实例。尝试使用 new
关键字调用箭头函数会导致错误。
const Example = () => {
this.value = 42; // 这里会抛出错误
};
const obj = new Example(); // TypeError: Example is not a constructor
this
这里就要聊到this的
默认绑定
、隐式绑定
、显示绑定
、new绑定
、以及箭头函数中的this
。
总结如下:
# this的绑定规则
1. 默认绑定 --- 函数在哪个词法作用域里生效,this就指向哪里
2. 隐式绑定 --- 当函数被一个对象所拥有,再调用时,此时this会指向该对象
3. 隐式丢失 --- 当函数被多个对象链式调用时,this指向引用函数的对象
4. 显示绑定 --- call,apply,bind
5. new绑定 --- 当使用 new 关键字调用构造函数时,会创建一个新的对象,并将这个新对象绑定到 this 上。
# 箭头函数 (ES6新增函数)
箭头函数没有this这个概念,写在箭头函数中的this也是它外层函数的this
总之。JavaScript中的
this
可以根据执行上下文
的不同
而有不同的指向
,需要根据具体的使用场景来理解和确定它的值。对于箭头函数
来说,它会继承
外层作用域的this
值,而不会被上述规则所影响。
事件循环、宏任务微任务
由于在浏览器环境下JS是单线程语言,所以异步编程是实现无阻塞操作的关键。JavaScript通过引入宏任务
和微任务
的概念,以及事件循环机制
,使其可以在等待异步操作完成的同时继续执行其他任务。
宏任务与微任务
在事件循环中,任务分为两种:宏任务(Macrotask)
和微任务(Microtask)
。
宏任务包括: script 标签中的代码、定时器、I/O 操作等
微任务包括: promise.then 方法、MutationObserver、Process.nextTick(在 Node.js 中)等。
1. 宏任务(Macrotask)
宏任务是由浏览器提供的异步任务,包括 script 标签中的代码、定时器(setTimeout)
、I/O 操作和 UI 渲染等。宏任务会被依次推入宏任务队列,按照顺序执行。
2. 微任务(Microtask)
微任务是在当前任务执行完成后立即执行的任务,包括 promise.then
方法、MutationObserver 和 Process.nextTick(在 Node.js 中)。微任务会在宏任务执行前被处理。
事件循环(Event Loop)
异步操作将被分发到宏任务队列和微任务队列中,然后由事件循环按照特定的顺序执行:
执行同步代码(宏任务) :事件循环的执行过程始于同步代码的执行。从调用栈中执行同步任务,这其实就相当于开启第一轮的宏任务。
查询异步代码并执行微任:当执行栈为空时,事件循环会查询是否有异步代码需要执行。如果有,将执行微任务队列中的任务。
渲染页面:在执行微任务后,如果需要,会渲染页面。这一步通常发生在宏任务执行前,以确保用户界面的及时响应。
执行宏任务:最后,事件循环会执行宏任务队列中的任务。这个过程就是事件循环的一个完整轮回。执行完宏任务后,再次查询是否有微任务需要执行,依此类推。
Promise以及每种方法的区别
是什么?
Promise是后来ES6推出提供解决异步的一个对象,解决了之前回调地狱的问题,Promise有三种状态:
- Pending(进行中) : 初始状态,表示操作正在进行中。
- Fulfilled(已成功) : 表示操作已经成功完成。
- Rejected(已失败) : 表示操作失败。
并且,Promise的状态一经改变就无法逆转,然后Promise中有个then
和catch
方法,这样也就保证了 then 和 catch 不可能同时触发。
- .then:默认返回一个状态为
Fulfilled
的Promise对象。并且接受两个回调函数作为参数
- 当then前面的promise状态为
fulfilled
,then里面的回调直接执行- 当then前面的promise状态为
rejected
,then里面第二个回调直接执行- 当then前面的promise状态为
pending
,then里面的回调需要被缓存起来交给resolve或者reject执行
并且Promise内部还有一个resolve
和reject
函数,Promise接受resolve
和reject
作为参数。
- 当调用resolve函数时,会将当前Promise的状态更改为
Fulfilled
,并且可以携带参数,将参数保存供.then中第一个回调函数使用。- 当调用reject函数时,会将当前Promise的状态更改为
Rejected
,并且可以携带参数,将参数保存供.then中第二个回调函数使用,或者触发.catch中的回调函数。
Promise常用方法
Promise.resolve()
:返回一个状态为resolved
的 Promise 对象,可以将一个值转换为 Promise 对象。
Promise.reject()
:返回一个状态为rejected
的 Promise 对象,可以将一个错误或拒绝原因转换为 Promise 对象。
Promise.then()
:注册在 Promise 对象状态变为resolved
时的回调函数。
Promise.catch()
:注册在 Promise 对象状态变为rejected
时的回调函数。
Promise.all()
:接收一个Promise 数组
,当所有 Promise都成功时
返回成功
,否则返回第一个失败的 Promise
,并且后面的还会依旧继续执行。
Promise.race()
:接收一个Promise 数组
,返回第一个完成
的 Promise 的结果或拒绝原因。
Promise.finally()
:注册在 Promise 对象状态变为 resolved 或 rejected 时的回调函数,无论最终状态是成功
还是失败
,都会执行
。
Promise.allSettled()
:接收一个Promise 数组
,返回一个 Promise 对象,该对象在所有 Promise 都settled
(状态变为 resolved 或 rejected)后才 settled。
原型以及原型链
原型是什么
原型(prototype)是函数天生就具有的属性,它定义了构造函数制造出的对象的公共祖先。通过该
构造函数
产生的对象
可以隐式继承
到原型上的属性
和方法
。原型的意义是提取公共属性,简化代码执行。
关于原型的增删改查中:
实例对象是无法
修改
原型的属性和方法的实例对象是无法
删除
原型的属性和方法的
隐式原型是什么
除了构造函数,我们每次实例化一个对象时,这个对象也有自己的
隐式原型
,这个实例对象会通过自己的隐式原型
去继承构造函数显示原型上
的属性
和方法
。
当访问对象属性时,先找对象
显示具有
的属性,没找到再去找对象的隐式原型
。实例对象的隐式原型 === 构造函数的显示原型
原型链是什么
顺着对象的隐式原型不断地向上查找上一级地隐式原型,直到找到目标或者一直到null,这种查找关系叫做原型链
JS中判定数据类型是靠将该变量转换为2进制数据,如果前三位是000判定其为引用数据类型,但是null很特殊,虽然它是原始数据类型,但是它二进制转换后全是0,自然也被读成了引用数据类型,所以它是唯一一个没有隐式原型的对象
手写
实现instanceOf
/**
* 用法:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
* 思路:
* 1、通过 Object.getPrototypeOf 获取 obj 的原型
* 2、循环判断 objProtoType 是否和 constructor 的原型相等
* 2.1、如果相等就返回 true
* 2.2、如果不相等 就重新赋值一下 obj 的原型 进入下一次循环
* 3、判断是 objProtoType 是否为空 如果为空就说明不存在 返回 false
* @param {Object} obj 需要判断的数据
* @param {Function} constructor 构造函数
* @return {boolean} 如果 obj 是构造函数 constructor 的实例,则返回 true;否则返回 false。
*/
function myInstanceof(obj, constructor) {
let objPrototype = Object.getPrototypeOf(obj); // 获取 obj 的原型
while (objPrototype) { // 循环直到 objPrototype 为 null
if (objPrototype === constructor.prototype) { // 检查 objPrototype 是否与构造函数的原型相等
return true; // 如果相等,则返回 true
}
objPrototype = Object.getPrototypeOf(objPrototype); // 否则,将 obj 的原型设置为其原型的原型,进入下一次循环
}
return false; // 如果 objPrototype 为 null,则返回 false,表示未找到匹配的原型
}
防抖
/**
* 用法:函数在 n 毫秒后执行,如果在这 n 毫秒内被再次触发,则重新计时,保证最后一次触发事件 n 毫秒后才执行。
* 思路:
* 1、保存一个变量 timer 用于记录定时器
* 2、返回一个闭包函数,在函数内部判断是否需要立即执行
* 2.1、如果需要立即执行,判断是否已经执行过,若未执行过则立即执行函数
* 3、设置定时器,在 wait 毫秒后执行函数,并清空定时器
* @param {Function} fn 要执行的函数
* @param {number} wait 等待时间(毫秒)
* @param {boolean} [immediate=false] 是否立即执行
* @return {Function} 返回一个防抖函数
*/
function debounce(fn, wait, immediate = false) {
let timer = null; // 记录定时器
return function () {
const context = this; // 保存函数执行上下文
const args = arguments; // 保存函数参数
// 清空之前的定时器
if (timer) {
clearTimeout(timer);
timer = null;
}
if (immediate) { // 如果需要立即执行
const callNow = !timer; // 判断是否已经执行过
timer = setTimeout(() => {
timer = null;
}, wait);
if (callNow) { // 如果未执行过,则立即执行函数
fn.apply(context, args);
}
} else { // 如果不需要立即执行,则延迟执行函数
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
};
}
节流
/**
* 用法:函数在 n 毫秒内只执行一次,如果多次触发,则忽略执行。
* 思路:
* 1、记录函数上一次执行的时间戳 startTime
* 2、返回一个闭包函数,当被调用时会记录当前时间戳 nowTime
* 3、比较两次执行时间间隔是否超过了 wait 时间
* 4、如果已经超过 wait 时间,则执行函数并更新 startTime
* @param {Function} fn 执行函数
* @param {Number} wait 等待时间(毫秒)
* @return {Function} 返回一个节流函数
*/
function throttle(fn, wait) {
let startTime = Date.now(); // 记录上一次执行的时间戳
return function () {
const nowTime = Date.now(); // 当前时间戳
// 计算两次执行的间隔时间是否大于等于 wait 时间
if (nowTime - startTime >= wait) {
startTime = nowTime; // 更新 startTime
return fn.apply(this, arguments); // 执行函数
}
};
}
数组扁平化
/**
* 手写数组扁平化函数
* 用法:将多维数组转化为一维数组
* 思路:
* 1、使用递归函数来处理数组的每一项
* 2、遍历数组的每一项,如果是数组,则递归调用扁平化函数,直到处理到非数组项为止
* 3、将处理后的数组项添加到结果数组中
* 4、返回结果数组
* @param {Array} arr 需要扁平化的数组
* @return {Array} 扁平化后的数组
*/
function flattenArray(arr) {
let result = []; // 存放扁平化后的结果数组
// 递归函数,用于处理数组的每一项
function flatten(item) {
// 遍历数组的每一项
item.forEach(function (element) {
// 如果是数组,则递归调用 flatten 函数处理
if (Array.isArray(element)) {
flatten(element);
} else {
// 如果不是数组,则直接添加到结果数组中
result.push(element);
}
});
}
// 初始调用递归函数,处理传入的数组
flatten(arr);
return result;
}
结语
根据这么多天面试的经验,总结出中小厂面试流程:自我介绍+项目+八股+算法+手写。其中Vue的八股在之前的面经里基本都涵盖了,但是我觉得CSS如果要问深一点仍然是我的薄弱点,以及从学长那了解到的JS考点和手写题总结出了这篇文章,仅供学习参考。
原文链接:https://juejin.cn/post/7351612144528179241 作者:sAnL1ng